SPB Git

spb/tendril

Public

Tendril — web ingestion platform (scrape/crawl/map/search) on macOS Apple Silicon: WebKit fidelity, authenticated pages, deterministic testable extraction. A self-hosted Firecrawl alternative.

feat(api): serve interactive playground + docs platform at /

Self-contained landing page served via @fastify/static (apps/api/public):
- hero with Tendril identity (botanical/terminal theme, animated vine SVG)
- live playground: scrape/map/status, formats & options, latency, rendered
  markdown preview, links/metadata/structured tabs, curl/python/js snippets
  with copy, live /v1/status pill
- collapsible API reference with param tables + examples, §15 error table
Explicit API routes keep priority over the static wildcard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed (Aug 10, 2026) parent 62ea614 Browse files

4 changed files +780 −0

modified apps/api/package.json +1 −0
@@ -16,6 +16,7 @@
16 "@tendril/extract": "workspace:*", 16 "@tendril/extract": "workspace:*",
17 "@tendril/frontier": "workspace:*", 17 "@tendril/frontier": "workspace:*",
18 "@fastify/compress": "^8.0.1", 18 "@fastify/compress": "^8.0.1",
19+ "@fastify/static": "^8.0.3",
19 "fastify": "^5.2.0", 20 "fastify": "^5.2.0",
20 "zod": "^3.24.1" 21 "zod": "^3.24.1"
21 } 22 }
added apps/api/public/index.html +552 −0
@@ -0,0 +1,552 @@
1+<!doctype html>
2+<!-- author: simon-pierre boucher <contact@spboucher.ai> -->
3+<html lang="en" data-theme="dark">
4+<head>
5+<meta charset="utf-8" />
6+<meta name="viewport" content="width=device-width, initial-scale=1" />
7+<title>Tendril — web ingestion that reaches what others can't</title>
8+<meta name="description" content="Tendril is a web ingestion platform (scrape · map) running on residential macOS in WebKit. Deterministic, testable extraction. Try it in the playground." />
9+<meta property="og:title" content="Tendril" />
10+<meta property="og:description" content="Web ingestion that reaches what others can't. Scrape and map the web with deterministic, testable extraction." />
11+<meta property="og:type" content="website" />
12+<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Ctext y='26' font-size='26'%3E%F0%9F%8C%B1%3C/text%3E%3C/svg%3E" />
13+<link rel="preconnect" href="https://fonts.googleapis.com" />
14+<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
15+<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=Inter:wght@400;450;500;600&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
16+<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/styles/github-dark.min.css" />
17+<style>
18+ :root {
19+ --bg: #07100c;
20+ --bg-2: #0b1712;
21+ --panel: rgba(18, 32, 26, 0.72);
22+ --panel-solid: #101b16;
23+ --line: rgba(120, 200, 160, 0.16);
24+ --line-strong: rgba(120, 200, 160, 0.32);
25+ --ink: #e7f3ec;
26+ --muted: #8fae9f;
27+ --dim: #5f7a6d;
28+ --vine: #4ade80;
29+ --vine-2: #2dd4bf;
30+ --amber: #f5c451;
31+ --danger: #ff6b6b;
32+ --radius: 16px;
33+ --mono: "JetBrains Mono", ui-monospace, monospace;
34+ --sans: "Inter", system-ui, sans-serif;
35+ --display: "Space Grotesk", var(--sans);
36+ --shadow: 0 24px 60px -30px rgba(0, 0, 0, 0.9);
37+ }
38+ * { box-sizing: border-box; }
39+ html { scroll-behavior: smooth; }
40+ body {
41+ margin: 0; background: var(--bg); color: var(--ink); font-family: var(--sans);
42+ font-size: 15px; line-height: 1.6; -webkit-font-smoothing: antialiased;
43+ background-image:
44+ radial-gradient(1100px 600px at 82% -8%, rgba(45, 212, 191, 0.10), transparent 60%),
45+ radial-gradient(900px 520px at 4% 6%, rgba(74, 222, 128, 0.09), transparent 55%);
46+ background-attachment: fixed;
47+ }
48+ a { color: inherit; text-decoration: none; }
49+ .wrap { max-width: 1180px; margin: 0 auto; padding: 0 24px; }
50+
51+ /* header */
52+ header {
53+ position: sticky; top: 0; z-index: 50; backdrop-filter: blur(14px);
54+ background: linear-gradient(to bottom, rgba(7,16,12,0.9), rgba(7,16,12,0.55));
55+ border-bottom: 1px solid var(--line);
56+ }
57+ .nav { display: flex; align-items: center; gap: 20px; height: 62px; }
58+ .brand { display: flex; align-items: center; gap: 10px; font-family: var(--display); font-weight: 700; letter-spacing: -0.01em; font-size: 18px; }
59+ .brand .leaf { font-size: 20px; filter: drop-shadow(0 0 10px rgba(74,222,128,0.5)); }
60+ .brand b { color: var(--vine); }
61+ .nav .spacer { flex: 1; }
62+ .nav a.link { color: var(--muted); font-weight: 500; font-size: 14px; padding: 8px 10px; border-radius: 9px; }
63+ .nav a.link:hover { color: var(--ink); background: rgba(120,200,160,0.08); }
64+ .pill {
65+ font-family: var(--mono); font-size: 12px; color: var(--vine);
66+ border: 1px solid var(--line-strong); border-radius: 999px; padding: 5px 11px;
67+ display: inline-flex; align-items: center; gap: 7px; background: rgba(74,222,128,0.06);
68+ }
69+ .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--vine); box-shadow: 0 0 9px var(--vine); animation: pulse 2s infinite; }
70+ @keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.35} }
71+
72+ /* hero */
73+ .hero { padding: 74px 0 40px; position: relative; overflow: hidden; }
74+ .kicker { font-family: var(--mono); font-size: 12.5px; letter-spacing: 0.18em; text-transform: uppercase; color: var(--vine-2); margin-bottom: 18px; }
75+ h1 { font-family: var(--display); font-size: clamp(38px, 6vw, 68px); line-height: 1.02; letter-spacing: -0.03em; margin: 0 0 18px; font-weight: 700; }
76+ h1 .grad { background: linear-gradient(120deg, var(--vine), var(--vine-2) 60%, var(--amber)); -webkit-background-clip: text; background-clip: text; color: transparent; }
77+ .lede { font-size: 19px; color: var(--muted); max-width: 640px; margin: 0 0 28px; }
78+ .cta-row { display: flex; gap: 12px; flex-wrap: wrap; align-items: center; }
79+ .btn {
80+ font-family: var(--display); font-weight: 600; font-size: 15px; cursor: pointer;
81+ border-radius: 12px; padding: 12px 20px; border: 1px solid var(--line-strong);
82+ background: var(--panel-solid); color: var(--ink); transition: .15s; display: inline-flex; align-items: center; gap: 9px;
83+ }
84+ .btn:hover { transform: translateY(-1px); border-color: var(--vine); }
85+ .btn.primary { background: linear-gradient(120deg, var(--vine), var(--vine-2)); color: #05130c; border: none; box-shadow: 0 14px 34px -14px rgba(74,222,128,0.6); }
86+ .btn.primary:hover { filter: brightness(1.06); }
87+ .stats { display: flex; gap: 30px; flex-wrap: wrap; margin-top: 40px; }
88+ .stat .n { font-family: var(--display); font-size: 26px; font-weight: 700; color: var(--ink); }
89+ .stat .l { font-size: 12.5px; color: var(--dim); font-family: var(--mono); text-transform: uppercase; letter-spacing: 0.08em; }
90+ .vine-svg { position: absolute; right: -40px; top: 20px; width: 420px; max-width: 46%; opacity: 0.5; pointer-events: none; }
91+ .vine-svg path { fill: none; stroke: url(#vg); stroke-width: 2; stroke-linecap: round; stroke-dasharray: 1400; stroke-dashoffset: 1400; animation: grow 3.2s ease forwards; }
92+ @keyframes grow { to { stroke-dashoffset: 0; } }
93+
94+ section { padding: 40px 0; }
95+ .sec-head { display: flex; align-items: baseline; gap: 14px; margin-bottom: 22px; }
96+ .sec-head h2 { font-family: var(--display); font-size: 26px; letter-spacing: -0.02em; margin: 0; }
97+ .sec-head .tag { font-family: var(--mono); font-size: 12px; color: var(--dim); }
98+
99+ /* playground */
100+ .pg { display: grid; grid-template-columns: 1fr 1.15fr; gap: 20px; }
101+ @media (max-width: 900px){ .pg { grid-template-columns: 1fr; } .vine-svg{ display:none } }
102+ .card { background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); backdrop-filter: blur(8px); }
103+ .card .hd { padding: 15px 18px; border-bottom: 1px solid var(--line); display: flex; align-items: center; gap: 10px; }
104+ .card .hd h3 { font-family: var(--display); font-size: 15px; margin: 0; letter-spacing: -0.01em; }
105+ .card .bd { padding: 18px; }
106+ .seg { display: inline-flex; background: rgba(10,20,15,0.7); border: 1px solid var(--line); border-radius: 11px; padding: 4px; gap: 3px; }
107+ .seg button { font-family: var(--mono); font-size: 13px; color: var(--muted); background: none; border: 0; padding: 7px 14px; border-radius: 8px; cursor: pointer; }
108+ .seg button.on { background: linear-gradient(120deg, rgba(74,222,128,0.9), rgba(45,212,191,0.9)); color: #05130c; font-weight: 600; }
109+ label.f { display: block; font-size: 12.5px; color: var(--muted); font-family: var(--mono); margin: 16px 0 7px; letter-spacing: 0.02em; }
110+ input[type=text], input[type=number], select {
111+ width: 100%; background: rgba(6,14,10,0.85); border: 1px solid var(--line); color: var(--ink);
112+ border-radius: 10px; padding: 11px 13px; font-family: var(--mono); font-size: 13.5px; outline: none; transition: .15s;
113+ }
114+ input:focus, select:focus { border-color: var(--vine); box-shadow: 0 0 0 3px rgba(74,222,128,0.12); }
115+ .chips { display: flex; flex-wrap: wrap; gap: 8px; }
116+ .chk { font-family: var(--mono); font-size: 12.5px; display: inline-flex; align-items: center; gap: 7px; padding: 8px 12px; border: 1px solid var(--line); border-radius: 9px; cursor: pointer; color: var(--muted); user-select: none; }
117+ .chk input { accent-color: var(--vine); }
118+ .chk.on { border-color: var(--vine); color: var(--ink); background: rgba(74,222,128,0.07); }
119+ .row2 { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
120+ .send-row { display: flex; align-items: center; gap: 12px; margin-top: 20px; }
121+ .meta-line { font-family: var(--mono); font-size: 12px; color: var(--dim); }
122+ .badge { font-family: var(--mono); font-size: 11px; padding: 3px 8px; border-radius: 7px; border: 1px solid var(--line); color: var(--muted); }
123+ .badge.ok { color: var(--vine); border-color: var(--line-strong); }
124+ .badge.err { color: var(--danger); border-color: rgba(255,107,107,0.4); }
125+
126+ .tabs { display: flex; gap: 4px; flex-wrap: wrap; }
127+ .tabs button { font-family: var(--mono); font-size: 12.5px; color: var(--muted); background: none; border: 1px solid transparent; padding: 6px 11px; border-radius: 8px; cursor: pointer; }
128+ .tabs button.on { color: var(--ink); border-color: var(--line); background: rgba(10,20,15,0.6); }
129+ .out { min-height: 260px; max-height: 560px; overflow: auto; }
130+ .out pre { margin: 0; }
131+ pre code.hljs { background: transparent !important; font-family: var(--mono); font-size: 12.8px; line-height: 1.55; padding: 16px 18px; display: block; }
132+ .rendered { padding: 18px; font-size: 14.5px; }
133+ .rendered h1,.rendered h2,.rendered h3 { font-family: var(--display); line-height: 1.2; }
134+ .rendered a { color: var(--vine-2); border-bottom: 1px solid rgba(45,212,191,0.3); }
135+ .rendered pre { background: rgba(6,14,10,0.9); border: 1px solid var(--line); border-radius: 10px; padding: 12px; overflow: auto; }
136+ .rendered table { border-collapse: collapse; width: 100%; font-size: 13px; }
137+ .rendered th, .rendered td { border: 1px solid var(--line); padding: 6px 10px; text-align: left; }
138+ .rendered img { max-width: 100%; border-radius: 8px; }
139+ .placeholder { color: var(--dim); font-family: var(--mono); font-size: 13px; padding: 40px 18px; text-align: center; }
140+ .linkrow { display: flex; align-items: center; gap: 10px; padding: 9px 14px; border-bottom: 1px solid var(--line); font-family: var(--mono); font-size: 12.5px; }
141+ .linkrow a { color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
142+ .src { font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.06em; padding: 2px 7px; border-radius: 6px; border: 1px solid var(--line); color: var(--vine-2); }
143+ .copybtn { font-family: var(--mono); font-size: 11.5px; cursor: pointer; background: rgba(10,20,15,0.6); border: 1px solid var(--line); color: var(--muted); padding: 4px 9px; border-radius: 7px; }
144+ .copybtn:hover { color: var(--vine); border-color: var(--vine); }
145+ .spin { width: 15px; height: 15px; border: 2px solid rgba(255,255,255,0.2); border-top-color: var(--vine); border-radius: 50%; animation: sp .7s linear infinite; }
146+ @keyframes sp { to { transform: rotate(360deg); } }
147+
148+ /* docs */
149+ .ep { border: 1px solid var(--line); border-radius: var(--radius); margin-bottom: 16px; overflow: hidden; background: var(--panel); }
150+ .ep .top { display: flex; align-items: center; gap: 12px; padding: 15px 18px; cursor: pointer; }
151+ .method { font-family: var(--mono); font-weight: 600; font-size: 12px; padding: 4px 9px; border-radius: 7px; background: rgba(74,222,128,0.14); color: var(--vine); }
152+ .method.get { background: rgba(45,212,191,0.14); color: var(--vine-2); }
153+ .path { font-family: var(--mono); font-size: 14px; }
154+ .ep .desc { color: var(--muted); font-size: 13px; margin-left: auto; }
155+ .ep .body { padding: 0 18px 18px; display: none; }
156+ .ep.open .body { display: block; }
157+ .ptable { width: 100%; border-collapse: collapse; font-size: 13px; margin: 8px 0 4px; }
158+ .ptable th { text-align: left; color: var(--dim); font-family: var(--mono); font-weight: 500; font-size: 11.5px; text-transform: uppercase; letter-spacing: 0.06em; padding: 6px 8px; border-bottom: 1px solid var(--line); }
159+ .ptable td { padding: 7px 8px; border-bottom: 1px solid var(--line); vertical-align: top; }
160+ .ptable code { font-family: var(--mono); color: var(--vine-2); font-size: 12.5px; }
161+ .req-opt { font-size: 10.5px; color: var(--amber); font-family: var(--mono); }
162+ .errtbl { width: 100%; border-collapse: collapse; font-size: 12.5px; }
163+ .errtbl td, .errtbl th { padding: 6px 10px; border-bottom: 1px solid var(--line); text-align: left; }
164+ .errtbl code { color: var(--danger); font-family: var(--mono); }
165+ footer { border-top: 1px solid var(--line); padding: 34px 0 60px; color: var(--dim); font-size: 13px; margin-top: 30px; }
166+ footer a { color: var(--muted); } footer a:hover { color: var(--vine); }
167+ .grid3 { display: grid; grid-template-columns: repeat(3,1fr); gap: 16px; }
168+ @media (max-width:760px){ .grid3{ grid-template-columns:1fr } .row2{ grid-template-columns:1fr } }
169+ .feat { padding: 18px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--panel); }
170+ .feat .i { font-size: 22px; margin-bottom: 10px; }
171+ .feat h4 { font-family: var(--display); margin: 0 0 6px; font-size: 16px; }
172+ .feat p { margin: 0; color: var(--muted); font-size: 13.5px; }
173+ .note { font-size: 12.5px; color: var(--amber); font-family: var(--mono); background: rgba(245,196,81,0.07); border: 1px solid rgba(245,196,81,0.25); border-radius: 10px; padding: 10px 13px; margin-top: 14px; }
174+</style>
175+</head>
176+<body>
177+<header>
178+ <div class="wrap nav">
179+ <a class="brand" href="#top"><span class="leaf">🌱</span> ten<b>·</b>dril</a>
180+ <span class="spacer"></span>
181+ <a class="link" href="#playground">Playground</a>
182+ <a class="link" href="#docs">Docs</a>
183+ <a class="link" href="https://git.spboucher.ai/tendril" target="_blank" rel="noopener">Source</a>
184+ <span class="pill" id="statusPill"><span class="dot"></span> checking…</span>
185+ </div>
186+</header>
187+
188+<a id="top"></a>
189+<div class="hero">
190+ <div class="wrap">
191+ <svg class="vine-svg" viewBox="0 0 300 300" aria-hidden="true">
192+ <defs><linearGradient id="vg" x1="0" y1="0" x2="1" y2="1">
193+ <stop offset="0" stop-color="#4ade80"/><stop offset="0.6" stop-color="#2dd4bf"/><stop offset="1" stop-color="#f5c451"/>
194+ </linearGradient></defs>
195+ <path d="M150 300 C150 240 120 220 150 180 C180 140 120 120 150 80 C170 50 150 30 150 0" />
196+ <path d="M150 220 C120 210 100 190 85 200 C70 210 80 230 100 225" />
197+ <path d="M150 150 C185 140 205 120 220 132 C235 144 224 165 202 158" />
198+ <path d="M150 95 C120 86 104 66 90 78 C78 88 86 108 106 102" />
199+ </svg>
200+ <div class="kicker">Web ingestion · scrape · map</div>
201+ <h1>Reach what other<br />crawlers <span class="grad">can't.</span></h1>
202+ <p class="lede">Tendril runs on a residential Mac, in WebKit — Safari's real engine — behind a real IP. Extraction is <strong>deterministic and testable</strong>: the same page always yields the same markdown. No cloud fingerprint, no LLM lottery.</p>
203+ <div class="cta-row">
204+ <a class="btn primary" href="#playground">▶ Open the playground</a>
205+ <a class="btn" href="#docs">Read the docs</a>
206+ </div>
207+ <div class="stats">
208+ <div class="stat"><div class="n">3</div><div class="l">escalation tiers</div></div>
209+ <div class="stat"><div class="n">0</div><div class="l">LLMs in extraction</div></div>
210+ <div class="stat"><div class="n">~50ms</div><div class="l">Tier 0 p50</div></div>
211+ <div class="stat"><div class="n">WebKit</div><div class="l">real Safari engine</div></div>
212+ </div>
213+ </div>
214+</div>
215+
216+<section class="wrap">
217+ <div class="grid3">
218+ <div class="feat"><div class="i">🕸️</div><h4>Browser fidelity</h4><p>WebKit on macOS ARM behind a residential IP — not a headless Chromium signature. Sites that block others often don't block Tendril.</p></div>
219+ <div class="feat"><div class="i">📐</div><h4>Deterministic extraction</h4><p>JSON-LD, OpenGraph, Readability + density fallback, hand-written Turndown rules. Versioned, golden-file testable output.</p></div>
220+ <div class="feat"><div class="i">🧭</div><h4>Map without rendering</h4><p>Merge sitemaps, robots directives, homepage links and <code>/llms.txt</code> into one deduplicated URL list in seconds.</p></div>
221+ </div>
222+</section>
223+
224+<section class="wrap" id="playground">
225+ <div class="sec-head"><h2>Playground</h2><span class="tag">// live requests against this deployment</span></div>
226+ <div class="pg">
227+ <div class="card">
228+ <div class="hd"><h3>Request</h3><span class="spacer" style="flex:1"></span>
229+ <div class="seg" id="epseg">
230+ <button data-ep="scrape" class="on">scrape</button>
231+ <button data-ep="map">map</button>
232+ <button data-ep="status">status</button>
233+ </div>
234+ </div>
235+ <div class="bd">
236+ <div id="urlblock">
237+ <label class="f">Target URL</label>
238+ <input type="text" id="url" value="https://www.iana.org" spellcheck="false" placeholder="https://example.com" />
239+ </div>
240+
241+ <div id="scrapeOpts">
242+ <label class="f">Formats</label>
243+ <div class="chips" id="formats">
244+ <label class="chk on"><input type="checkbox" value="markdown" checked>markdown</label>
245+ <label class="chk"><input type="checkbox" value="links">links</label>
246+ <label class="chk"><input type="checkbox" value="structured">structured</label>
247+ <label class="chk"><input type="checkbox" value="html">html</label>
248+ </div>
249+ <div class="row2">
250+ <div><label class="f">Tier</label>
251+ <select id="tier"><option value="auto">auto</option><option value="http">http (Tier 0)</option><option value="webkit">webkit</option><option value="safari">safari</option></select>
252+ </div>
253+ <div><label class="f">Options</label>
254+ <div class="chips">
255+ <label class="chk on"><input type="checkbox" id="onlyMain" checked>onlyMainContent</label>
256+ <label class="chk on"><input type="checkbox" id="respRobots" checked>respectRobots</label>
257+ </div>
258+ </div>
259+ </div>
260+ </div>
261+
262+ <div id="mapOpts" style="display:none">
263+ <div class="row2">
264+ <div><label class="f">Search filter</label><input type="text" id="mapSearch" placeholder="optional substring" spellcheck="false"></div>
265+ <div><label class="f">Limit</label><input type="number" id="mapLimit" value="30" min="1" max="50000"></div>
266+ </div>
267+ <label class="f">Options</label>
268+ <div class="chips">
269+ <label class="chk"><input type="checkbox" id="sitemapOnly">sitemapOnly</label>
270+ <label class="chk"><input type="checkbox" id="ignoreSitemap">ignoreSitemap</label>
271+ <label class="chk"><input type="checkbox" id="subdomains">includeSubdomains</label>
272+ </div>
273+ </div>
274+
275+ <div id="statusOpts" style="display:none">
276+ <p class="meta-line">No parameters. Returns per-tier availability and pipeline version.</p>
277+ </div>
278+
279+ <div class="send-row">
280+ <button class="btn primary" id="send">▶ Send request</button>
281+ <span id="reqmeta" class="meta-line"></span>
282+ </div>
283+ <div class="note" id="autoNote" style="display:none">Tiers 1 &amp; 2 (WebKit/Safari) aren't deployed yet — a page needing escalation under <b>tier: auto</b> returns <code>ERR_TARGET_BLOCKED</code>. Force <b>tier: http</b> for raw Tier 0 output.</div>
284+ </div>
285+ </div>
286+
287+ <div class="card">
288+ <div class="hd"><h3>Response</h3><span class="spacer" style="flex:1"></span><span id="respStatus"></span></div>
289+ <div class="bd" style="padding-top:14px">
290+ <div class="tabs" id="tabs"></div>
291+ <div class="out" id="out"><div class="placeholder">Send a request to see the response, rendered markdown, extracted links and ready-to-copy snippets.</div></div>
292+ </div>
293+ </div>
294+ </div>
295+</section>
296+
297+<section class="wrap" id="docs">
298+ <div class="sec-head"><h2>API reference</h2><span class="tag">// base: this origin · Bearer auth coming with accounts</span></div>
299+ <div id="epdocs"></div>
300+
301+ <div class="sec-head" style="margin-top:30px"><h2>Errors</h2><span class="tag">// §15 taxonomy — one code per failure</span></div>
302+ <div class="card"><div class="bd">
303+ <table class="errtbl"><tbody>
304+ <tr><td><code>ERR_INVALID_URL</code></td><td>400</td><td>Malformed or unsupported scheme</td></tr>
305+ <tr><td><code>ERR_SSRF_BLOCKED</code></td><td>400</td><td>Resolved to a private/forbidden address</td></tr>
306+ <tr><td><code>ERR_ROBOTS_DENIED</code></td><td>403</td><td>robots.txt disallows the path</td></tr>
307+ <tr><td><code>ERR_TARGET_BLOCKED</code></td><td>502</td><td>Tier 0 insufficient; higher tiers unavailable</td></tr>
308+ <tr><td><code>ERR_UNSUPPORTED_TYPE</code></td><td>415</td><td>Content type not handled</td></tr>
309+ <tr><td><code>ERR_TIER_TIMEOUT</code></td><td>504</td><td>Render exceeded timeout</td></tr>
310+ </tbody></table>
311+ <p class="meta-line" style="margin-top:12px">Every response carries a <code>requestId</code> and an <code>X-Request-Id</code> header for support.</p>
312+ </div></div>
313+</section>
314+
315+<footer>
316+ <div class="wrap">
317+ <div style="display:flex;gap:20px;flex-wrap:wrap;align-items:center">
318+ <span class="brand"><span class="leaf">🌱</span> ten·dril</span>
319+ <span class="spacer" style="flex:1"></span>
320+ <a href="https://git.spboucher.ai/tendril" target="_blank" rel="noopener">Source</a>
321+ <a href="/healthz">/healthz</a>
322+ <a href="mailto:contact@spboucher.ai">contact@spboucher.ai</a>
323+ </div>
324+ <p style="margin-top:16px;max-width:640px">Tendril identifies itself with a bot User-Agent and a contact URL, honours <code>robots.txt</code> by default, and rate-limits per host. Stealth here means matching a real browser's fingerprint — not hiding who we are. Built by Simon-Pierre Boucher.</p>
325+ </div>
326+</footer>
327+
328+<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js"></script>
329+<script src="https://cdn.jsdelivr.net/npm/marked@12.0.0/marked.min.js"></script>
330+<script>
331+const $ = (s, r=document) => r.querySelector(s);
332+const $$ = (s, r=document) => [...r.querySelectorAll(s)];
333+let endpoint = "scrape";
334+let lastResult = null;
335+let activeTab = "preview";
336+
337+// status pill
338+fetch("/v1/status").then(r=>r.json()).then(j=>{
339+ const p = $("#statusPill");
340+ const http = j?.data?.tiers?.http?.available;
341+ p.innerHTML = `<span class="dot"></span> ${http ? "live" : "degraded"} · v${j?.data?.pipelineVersion||"?"}`;
342+}).catch(()=>{ $("#statusPill").innerHTML = '<span class="dot" style="background:#ff6b6b;box-shadow:0 0 9px #ff6b6b"></span> offline'; });
343+
344+// endpoint switch
345+$$("#epseg button").forEach(b => b.onclick = () => {
346+ $$("#epseg button").forEach(x=>x.classList.remove("on")); b.classList.add("on");
347+ endpoint = b.dataset.ep;
348+ $("#scrapeOpts").style.display = endpoint==="scrape" ? "" : "none";
349+ $("#mapOpts").style.display = endpoint==="map" ? "" : "none";
350+ $("#statusOpts").style.display = endpoint==="status" ? "" : "none";
351+ $("#urlblock").style.display = endpoint==="status" ? "none" : "";
352+});
353+
354+// chip toggles
355+$$(".chk").forEach(c => { const i = c.querySelector("input"); if(i) i.onchange = () => c.classList.toggle("on", i.checked); });
356+$("#tier").onchange = e => $("#autoNote").style.display = e.target.value==="auto" ? "" : "none";
357+
358+function buildBody() {
359+ if (endpoint === "status") return null;
360+ const url = $("#url").value.trim();
361+ if (endpoint === "scrape") {
362+ const formats = $$("#formats input:checked").map(i=>i.value);
363+ return { url, formats, tier: $("#tier").value, onlyMainContent: $("#onlyMain").checked, respectRobots: $("#respRobots").checked };
364+ }
365+ const b = { url, limit: Number($("#mapLimit").value)||30, sitemapOnly: $("#sitemapOnly").checked, ignoreSitemap: $("#ignoreSitemap").checked, includeSubdomains: $("#subdomains").checked };
366+ const s = $("#mapSearch").value.trim(); if (s) b.search = s;
367+ return b;
368+}
369+
370+function snippets(path, body) {
371+ const origin = location.origin;
372+ if (!body) return {
373+ curl: `curl -s ${origin}${path}`,
374+ python: `import requests\nprint(requests.get("${origin}${path}").json())`,
375+ js: `const r = await fetch("${origin}${path}");\nconsole.log(await r.json());`
376+ };
377+ const j = JSON.stringify(body, null, 2);
378+ return {
379+ curl: `curl -s -X POST ${origin}${path} \\\n -H 'content-type: application/json' \\\n -d '${JSON.stringify(body)}'`,
380+ python: `import requests\nr = requests.post("${origin}${path}", json=${j.replace(/true/g,"True").replace(/false/g,"False")})\nprint(r.json())`,
381+ js: `const r = await fetch("${origin}${path}", {\n method: "POST",\n headers: { "content-type": "application/json" },\n body: JSON.stringify(${j})\n});\nconsole.log(await r.json());`
382+ };
383+}
384+
385+function setTabs(tabs) {
386+ const t = $("#tabs"); t.innerHTML = "";
387+ tabs.forEach(name => {
388+ const b = document.createElement("button");
389+ b.textContent = name; b.className = name===activeTab ? "on" : "";
390+ b.onclick = () => { activeTab = name; render(); };
391+ t.appendChild(b);
392+ });
393+}
394+
395+function code(text, lang) {
396+ const pre = document.createElement("pre");
397+ const c = document.createElement("code");
398+ c.className = "hljs" + (lang ? " language-"+lang : "");
399+ c.textContent = text;
400+ pre.appendChild(c);
401+ try { hljs.highlightElement(c); } catch(e){}
402+ return pre;
403+}
404+function copyBar(text) {
405+ const bar = document.createElement("div");
406+ bar.style.cssText = "display:flex;justify-content:flex-end;padding:8px 12px 0";
407+ const b = document.createElement("button"); b.className="copybtn"; b.textContent="⧉ copy";
408+ b.onclick = () => { navigator.clipboard.writeText(text); b.textContent="✓ copied"; setTimeout(()=>b.textContent="⧉ copy",1200); };
409+ bar.appendChild(b); return bar;
410+}
411+
412+function render() {
413+ const out = $("#out"); out.innerHTML = "";
414+ if (!lastResult) return;
415+ const { data, raw, snips } = lastResult;
416+
417+ if (activeTab === "preview") {
418+ if (endpoint === "map") {
419+ const links = data?.data?.links || [];
420+ if (!links.length) { out.innerHTML = '<div class="placeholder">No links found.</div>'; return; }
421+ const head = document.createElement("div"); head.style.cssText="padding:10px 14px;color:var(--dim);font-family:var(--mono);font-size:12px";
422+ head.textContent = `${data.data.count} URLs discovered`;
423+ out.appendChild(head);
424+ links.forEach(l => {
425+ const r = document.createElement("div"); r.className="linkrow";
426+ r.innerHTML = `<span class="src">${l.source}</span><a href="${l.url}" target="_blank" rel="noopener">${l.url}</a>`;
427+ out.appendChild(r);
428+ });
429+ } else if (endpoint === "status") {
430+ out.appendChild(code(JSON.stringify(data, null, 2), "json"));
431+ } else {
432+ const d = data?.data;
433+ if (d?.markdown) {
434+ const div = document.createElement("div"); div.className="rendered";
435+ div.innerHTML = marked.parse(d.markdown);
436+ out.appendChild(div);
437+ } else if (d?.html) {
438+ out.appendChild(code(d.html.slice(0, 20000), "xml"));
439+ } else {
440+ out.appendChild(code(JSON.stringify(data, null, 2), "json"));
441+ }
442+ }
443+ } else if (activeTab === "links") {
444+ const links = lastResult.data?.data?.links || [];
445+ if (!links.length) { out.innerHTML = '<div class="placeholder">No links in this response.</div>'; return; }
446+ links.forEach(l => {
447+ const r = document.createElement("div"); r.className="linkrow";
448+ const badge = l.isInternal!==undefined ? `<span class="src">${l.isInternal?"internal":"external"}</span>` : "";
449+ r.innerHTML = `${badge}<a href="${l.url}" target="_blank" rel="noopener">${l.url}</a>`;
450+ out.appendChild(r);
451+ });
452+ } else if (activeTab === "metadata") {
453+ out.appendChild(code(JSON.stringify(lastResult.data?.data?.metadata || {}, null, 2), "json"));
454+ } else if (activeTab === "structured") {
455+ out.appendChild(code(JSON.stringify(lastResult.data?.data?.structured || {}, null, 2), "json"));
456+ } else if (activeTab === "json") {
457+ out.appendChild(code(raw, "json"));
458+ } else if (["curl","python","js"].includes(activeTab)) {
459+ out.appendChild(copyBar(snips[activeTab]));
460+ out.appendChild(code(snips[activeTab], activeTab==="curl"?"bash":activeTab==="python"?"python":"javascript"));
461+ }
462+}
463+
464+async function send() {
465+ const path = "/v1/" + endpoint;
466+ const body = buildBody();
467+ const snips = snippets(path, body);
468+ const btn = $("#send"); btn.disabled = true; btn.innerHTML = '<span class="spin"></span> running';
469+ $("#reqmeta").textContent = "";
470+ const t0 = performance.now();
471+ try {
472+ const res = await fetch(path, endpoint==="status"
473+ ? { method:"GET" }
474+ : { method:"POST", headers:{ "content-type":"application/json" }, body: JSON.stringify(body) });
475+ const raw = await res.text();
476+ let data; try { data = JSON.parse(raw); } catch { data = raw; }
477+ const ms = Math.round(performance.now() - t0);
478+ const ok = res.ok && data?.success !== false;
479+ $("#respStatus").innerHTML = `<span class="badge ${ok?"ok":"err"}">${res.status}</span> <span class="meta-line">${ms} ms</span>`;
480+ $("#reqmeta").innerHTML = ok ? `<span class="badge ok">success</span>` : `<span class="badge err">${data?.error?.code||"error"}</span>`;
481+ const pretty = typeof data === "string" ? data : JSON.stringify(data, null, 2);
482+ lastResult = { data, raw: pretty, snips };
483+
484+ let tabs;
485+ if (endpoint === "scrape") {
486+ tabs = ["preview"];
487+ if (data?.data?.links) tabs.push("links");
488+ if (data?.data?.metadata) tabs.push("metadata");
489+ if (data?.data?.structured) tabs.push("structured");
490+ tabs.push("json","curl","python","js");
491+ } else if (endpoint === "map") {
492+ tabs = ["preview","json","curl","python","js"];
493+ } else {
494+ tabs = ["preview","json","curl","python","js"];
495+ }
496+ activeTab = "preview";
497+ setTabs(tabs); render();
498+ } catch (e) {
499+ $("#respStatus").innerHTML = `<span class="badge err">network</span>`;
500+ $("#out").innerHTML = `<div class="placeholder">Request failed: ${e.message}</div>`;
501+ } finally {
502+ btn.disabled = false; btn.innerHTML = "▶ Send request";
503+ }
504+}
505+$("#send").onclick = send;
506+
507+// docs
508+const EPS = [
509+ { m:"POST", path:"/v1/scrape", desc:"Fetch one page and extract it",
510+ params:[
511+ ["url","string","required","The page to fetch"],
512+ ["formats","string[]","markdown","Any of markdown · html · rawHtml · links · structured"],
513+ ["tier","enum","auto","auto · http · webkit · safari"],
514+ ["onlyMainContent","boolean","true","Strip nav/footer/ads, keep the article"],
515+ ["respectRobots","boolean","true","Honour robots.txt (refused if disallowed)"],
516+ ["timeout","number","30000","Milliseconds"],
517+ ],
518+ ex:{ url:"https://www.iana.org", formats:["markdown","structured"] } },
519+ { m:"POST", path:"/v1/map", desc:"Discover a site's URLs without rendering",
520+ params:[
521+ ["url","string","required","The site root to map"],
522+ ["search","string","—","Substring filter, ranked by match position"],
523+ ["limit","number","5000","Max URLs (cap 50000)"],
524+ ["sitemapOnly","boolean","false","Only sitemaps, skip homepage links"],
525+ ["ignoreSitemap","boolean","false","Skip sitemaps, links only"],
526+ ["includeSubdomains","boolean","false","Include subdomains of the apex"],
527+ ],
528+ ex:{ url:"https://www.iana.org", limit:20 } },
529+ { m:"GET", path:"/v1/status", desc:"Per-tier availability + pipeline version", params:[], ex:null },
530+];
531+const box = $("#epdocs");
532+EPS.forEach(ep => {
533+ const el = document.createElement("div"); el.className = "ep";
534+ const rows = ep.params.map(p => `<tr><td><code>${p[0]}</code></td><td>${p[1]}</td><td>${p[2]==="required"?'<span class="req-opt">required</span>':p[2]}</td><td>${p[3]}</td></tr>`).join("");
535+ el.innerHTML = `
536+ <div class="top"><span class="method ${ep.m.toLowerCase()}">${ep.m}</span><span class="path">${ep.path}</span><span class="desc">${ep.desc}</span></div>
537+ <div class="body">
538+ ${ep.params.length ? `<table class="ptable"><thead><tr><th>Param</th><th>Type</th><th>Default</th><th>Notes</th></tr></thead><tbody>${rows}</tbody></table>` : '<p class="meta-line">No parameters.</p>'}
539+ ${ep.ex ? `<div style="margin-top:12px">${""}</div>` : ""}
540+ </div>`;
541+ el.querySelector(".top").onclick = () => el.classList.toggle("open");
542+ if (ep.ex) {
543+ const pre = code(`curl -s -X POST ${location.origin}${ep.path} \\\n -H 'content-type: application/json' \\\n -d '${JSON.stringify(ep.ex)}'`, "bash");
544+ el.querySelector(".body > div:last-child")?.appendChild(pre);
545+ el.querySelector(".body").appendChild(pre);
546+ }
547+ box.appendChild(el);
548+});
549+$$(".ep")[0]?.classList.add("open");
550+</script>
551+</body>
552+</html>
modified apps/api/src/app.ts +10 −0
@@ -1,7 +1,9 @@
1 // author: simon-pierre boucher <contact@spboucher.ai> 1 // author: simon-pierre boucher <contact@spboucher.ai>
2 import { randomUUID } from "node:crypto"; 2 import { randomUUID } from "node:crypto";
3+import { fileURLToPath } from "node:url";
3 import Fastify, { type FastifyInstance } from "fastify"; 4 import Fastify, { type FastifyInstance } from "fastify";
4 import compress from "@fastify/compress"; 5 import compress from "@fastify/compress";
6+import fastifyStatic from "@fastify/static";
5 import { tendrilError } from "@tendril/shared"; 7 import { tendrilError } from "@tendril/shared";
6 import { mapSite } from "@tendril/frontier"; 8 import { mapSite } from "@tendril/frontier";
7 import { toApiError } from "./errors.js"; 9 import { toApiError } from "./errors.js";
@@ -21,6 +23,14 @@ export function buildApp(): FastifyInstance {
21 23
22 app.register(compress, { global: true, encodings: ["br", "gzip", "deflate"], threshold: 1024 }); 24 app.register(compress, { global: true, encodings: ["br", "gzip", "deflate"], threshold: 1024 });
23 25
26+ app.register(fastifyStatic, {
27+ root: fileURLToPath(new URL("../public", import.meta.url)),
28+ prefix: "/",
29+ index: ["index.html"],
30+ cacheControl: true,
31+ maxAge: "5m",
32+ });
33+
24 app.addHook("onSend", async (req, reply) => { 34 app.addHook("onSend", async (req, reply) => {
25 reply.header("X-Request-Id", req.id); 35 reply.header("X-Request-Id", req.id);
26 }); 36 });
modified pnpm-lock.yaml +217 −0
@@ -54,6 +54,9 @@ importers:
54 '@fastify/compress': 54 '@fastify/compress':
55 specifier: ^8.0.1 55 specifier: ^8.0.1
56 version: 8.3.1 56 version: 8.3.1
57+ '@fastify/static':
58+ specifier: ^8.0.3
59+ version: 8.3.0
57 '@tendril/egress': 60 '@tendril/egress':
58 specifier: workspace:* 61 specifier: workspace:*
59 version: link:../../packages/egress 62 version: link:../../packages/egress
@@ -459,9 +462,23 @@ packages:
459 '@fastify/proxy-addr@5.1.0': 462 '@fastify/proxy-addr@5.1.0':
460 resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==} 463 resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==}
461 464
465+ '@fastify/send@4.1.1':
466+ resolution: {integrity: sha512-BYo+EiaKwlxH+WetGk6hAs1d39iP0y1gqB8lGF/qwkJ9ZZ/cBY1vx5NvExb9Sc3yRMFjD5X4Eyh4e4+TzRkzdw==}
467+
468+ '@fastify/static@8.3.0':
469+ resolution: {integrity: sha512-yKxviR5PH1OKNnisIzZKmgZSus0r2OZb8qCSbqmw34aolT4g3UlzYfeBRym+HJ1J471CR8e2ldNub4PubD1coA==}
470+
471+ '@isaacs/cliui@9.0.0':
472+ resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==}
473+ engines: {node: '>=18'}
474+
462 '@jridgewell/sourcemap-codec@1.5.5': 475 '@jridgewell/sourcemap-codec@1.5.5':
463 resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} 476 resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
464 477
478+ '@lukeed/ms@2.0.2':
479+ resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==}
480+ engines: {node: '>=8'}
481+
465 '@mixmark-io/domino@2.2.0': 482 '@mixmark-io/domino@2.2.0':
466 resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==} 483 resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==}
467 484
@@ -684,6 +701,10 @@ packages:
684 avvio@9.3.0: 701 avvio@9.3.0:
685 resolution: {integrity: sha512-g2tQ7LE7oOSqDfwEm3M+ZCMTJc7KiZCdJ4UwyZJb5ckTKyYu50OYmvv0mCFXPuYXoM4zkSt8zM9XQ9KCvxA74A==} 702 resolution: {integrity: sha512-g2tQ7LE7oOSqDfwEm3M+ZCMTJc7KiZCdJ4UwyZJb5ckTKyYu50OYmvv0mCFXPuYXoM4zkSt8zM9XQ9KCvxA74A==}
686 703
704+ balanced-match@4.0.4:
705+ resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
706+ engines: {node: 18 || 20 || >=22}
707+
687 base64-js@1.5.1: 708 base64-js@1.5.1:
688 resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} 709 resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
689 710
@@ -691,6 +712,10 @@ packages:
691 resolution: {integrity: sha512-DkVaaQHymRhpYEYo9x1oo7Q7B0Y6KJUsjm3c9eTyFDby4MHLBTwZ6ZDWBel5zrYxj1WsZgC5oLpiz+93MluXeA==} 712 resolution: {integrity: sha512-DkVaaQHymRhpYEYo9x1oo7Q7B0Y6KJUsjm3c9eTyFDby4MHLBTwZ6ZDWBel5zrYxj1WsZgC5oLpiz+93MluXeA==}
692 engines: {node: '>=20.19.0'} 713 engines: {node: '>=20.19.0'}
693 714
715+ brace-expansion@5.0.9:
716+ resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==}
717+ engines: {node: 20 || >=22}
718+
694 buffer-from@1.1.2: 719 buffer-from@1.1.2:
695 resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} 720 resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
696 721
@@ -709,6 +734,10 @@ packages:
709 resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} 734 resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==}
710 engines: {node: '>= 16'} 735 engines: {node: '>= 16'}
711 736
737+ content-disposition@0.5.4:
738+ resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==}
739+ engines: {node: '>= 0.6'}
740+
712 cookie@1.1.1: 741 cookie@1.1.1:
713 resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} 742 resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
714 engines: {node: '>=18'} 743 engines: {node: '>=18'}
@@ -716,6 +745,10 @@ packages:
716 core-util-is@1.0.3: 745 core-util-is@1.0.3:
717 resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} 746 resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
718 747
748+ cross-spawn@7.0.6:
749+ resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
750+ engines: {node: '>= 8'}
751+
719 css-select@7.0.0: 752 css-select@7.0.0:
720 resolution: {integrity: sha512-snmjEVXy+1LnwXdxhYvTMj1d9tOh4HxkA1YmoayVBeeyR2C14Pum7fcxJIm4SswYspVy866eYNwlH6xC3/VH5g==} 753 resolution: {integrity: sha512-snmjEVXy+1LnwXdxhYvTMj1d9tOh4HxkA1YmoayVBeeyR2C14Pum7fcxJIm4SswYspVy866eYNwlH6xC3/VH5g==}
721 engines: {node: '>=20.19.0'} 754 engines: {node: '>=20.19.0'}
@@ -740,6 +773,10 @@ packages:
740 resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} 773 resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
741 engines: {node: '>=6'} 774 engines: {node: '>=6'}
742 775
776+ depd@2.0.0:
777+ resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
778+ engines: {node: '>= 0.8'}
779+
743 dequal@2.0.3: 780 dequal@2.0.3:
744 resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} 781 resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
745 engines: {node: '>=6'} 782 engines: {node: '>=6'}
@@ -807,6 +844,9 @@ packages:
807 engines: {node: '>=18'} 844 engines: {node: '>=18'}
808 hasBin: true 845 hasBin: true
809 846
847+ escape-html@1.0.3:
848+ resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
849+
810 estree-walker@3.0.3: 850 estree-walker@3.0.3:
811 resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} 851 resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
812 852
@@ -853,11 +893,21 @@ packages:
853 resolution: {integrity: sha512-f2JHn75x2JlwUwLenZypgczR7YWMb/uO9BvUXtus+JMgkbIkLADd38cI4EiV+OQqrGo1Zlq6V8wnqMJ8e62wUQ==} 893 resolution: {integrity: sha512-f2JHn75x2JlwUwLenZypgczR7YWMb/uO9BvUXtus+JMgkbIkLADd38cI4EiV+OQqrGo1Zlq6V8wnqMJ8e62wUQ==}
854 engines: {node: '>=20'} 894 engines: {node: '>=20'}
855 895
896+ foreground-child@3.3.1:
897+ resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
898+ engines: {node: '>=14'}
899+
856 fsevents@2.3.3: 900 fsevents@2.3.3:
857 resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 901 resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
858 engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 902 engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
859 os: [darwin] 903 os: [darwin]
860 904
905+ glob@11.1.0:
906+ resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==}
907+ engines: {node: 20 || >=22}
908+ deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
909+ hasBin: true
910+
861 globrex@0.1.2: 911 globrex@0.1.2:
862 resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} 912 resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==}
863 913
@@ -867,6 +917,10 @@ packages:
867 htmlparser2@10.1.0: 917 htmlparser2@10.1.0:
868 resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==} 918 resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==}
869 919
920+ http-errors@2.0.1:
921+ resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
922+ engines: {node: '>= 0.8'}
923+
870 ieee754@1.2.1: 924 ieee754@1.2.1:
871 resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} 925 resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
872 926
@@ -880,6 +934,13 @@ packages:
880 isarray@1.0.0: 934 isarray@1.0.0:
881 resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} 935 resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
882 936
937+ isexe@2.0.0:
938+ resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
939+
940+ jackspeak@4.2.3:
941+ resolution: {integrity: sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==}
942+ engines: {node: 20 || >=22}
943+
883 json-schema-ref-resolver@3.0.0: 944 json-schema-ref-resolver@3.0.0:
884 resolution: {integrity: sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==} 945 resolution: {integrity: sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==}
885 946
@@ -901,6 +962,10 @@ packages:
901 loupe@3.2.1: 962 loupe@3.2.1:
902 resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} 963 resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==}
903 964
965+ lru-cache@11.5.2:
966+ resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==}
967+ engines: {node: 20 || >=22}
968+
904 magic-string@0.30.21: 969 magic-string@0.30.21:
905 resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} 970 resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
906 971
@@ -908,6 +973,15 @@ packages:
908 resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} 973 resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==}
909 engines: {node: '>= 0.6'} 974 engines: {node: '>= 0.6'}
910 975
976+ mime@3.0.0:
977+ resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==}
978+ engines: {node: '>=10.0.0'}
979+ hasBin: true
980+
981+ minimatch@10.2.6:
982+ resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==}
983+ engines: {node: 18 || 20 || >=22}
984+
911 minipass@7.1.3: 985 minipass@7.1.3:
912 resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} 986 resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==}
913 engines: {node: '>=16 || 14 >=14.17'} 987 engines: {node: '>=16 || 14 >=14.17'}
@@ -931,6 +1005,17 @@ packages:
931 once@1.4.0: 1005 once@1.4.0:
932 resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} 1006 resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
933 1007
1008+ package-json-from-dist@1.0.1:
1009+ resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
1010+
1011+ path-key@3.1.1:
1012+ resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
1013+ engines: {node: '>=8'}
1014+
1015+ path-scurry@2.0.2:
1016+ resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==}
1017+ engines: {node: 18 || 20 || >=22}
1018+
934 pathe@1.1.2: 1019 pathe@1.1.2:
935 resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} 1020 resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==}
936 1021
@@ -1040,9 +1125,24 @@ packages:
1040 set-cookie-parser@2.7.2: 1125 set-cookie-parser@2.7.2:
1041 resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} 1126 resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==}
1042 1127
1128+ setprototypeof@1.2.0:
1129+ resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
1130+
1131+ shebang-command@2.0.0:
1132+ resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
1133+ engines: {node: '>=8'}
1134+
1135+ shebang-regex@3.0.0:
1136+ resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
1137+ engines: {node: '>=8'}
1138+
1043 siginfo@2.0.0: 1139 siginfo@2.0.0:
1044 resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} 1140 resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
1045 1141
1142+ signal-exit@4.1.0:
1143+ resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
1144+ engines: {node: '>=14'}
1145+
1046 sonic-boom@4.2.1: 1146 sonic-boom@4.2.1:
1047 resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} 1147 resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==}
1048 1148
@@ -1057,6 +1157,10 @@ packages:
1057 stackback@0.0.2: 1157 stackback@0.0.2:
1058 resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} 1158 resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
1059 1159
1160+ statuses@2.0.2:
1161+ resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
1162+ engines: {node: '>= 0.8'}
1163+
1060 std-env@3.10.0: 1164 std-env@3.10.0:
1061 resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} 1165 resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
1062 1166
@@ -1097,6 +1201,10 @@ packages:
1097 resolution: {integrity: sha512-m1TdR/rvT7kgGJZhspNtXdsdYk0fddFpJJFlG5s+UkPFo6lkLoZ3YLOaovPYjq1R75NP5JfeTlSHaOsE09peCg==} 1201 resolution: {integrity: sha512-m1TdR/rvT7kgGJZhspNtXdsdYk0fddFpJJFlG5s+UkPFo6lkLoZ3YLOaovPYjq1R75NP5JfeTlSHaOsE09peCg==}
1098 engines: {node: '>=20'} 1202 engines: {node: '>=20'}
1099 1203
1204+ toidentifier@1.0.1:
1205+ resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
1206+ engines: {node: '>=0.6'}
1207+
1100 tsconfck@3.1.6: 1208 tsconfck@3.1.6:
1101 resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==} 1209 resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==}
1102 engines: {node: ^18 || >=20} 1210 engines: {node: ^18 || >=20}
@@ -1204,6 +1312,11 @@ packages:
1204 jsdom: 1312 jsdom:
1205 optional: true 1313 optional: true
1206 1314
1315+ which@2.0.2:
1316+ resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
1317+ engines: {node: '>= 8'}
1318+ hasBin: true
1319+
1207 why-is-node-running@2.3.0: 1320 why-is-node-running@2.3.0:
1208 resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} 1321 resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
1209 engines: {node: '>=8'} 1322 engines: {node: '>=8'}
@@ -1404,8 +1517,29 @@ snapshots:
1404 '@fastify/forwarded': 3.0.2 1517 '@fastify/forwarded': 3.0.2
1405 ipaddr.js: 2.5.0 1518 ipaddr.js: 2.5.0
1406 1519
1520+ '@fastify/send@4.1.1':
1521+ dependencies:
1522+ '@lukeed/ms': 2.0.2
1523+ escape-html: 1.0.3
1524+ fast-decode-uri-component: 1.0.1
1525+ http-errors: 2.0.1
1526+ mime: 3.0.0
1527+
1528+ '@fastify/static@8.3.0':
1529+ dependencies:
1530+ '@fastify/accept-negotiator': 2.1.0
1531+ '@fastify/send': 4.1.1
1532+ content-disposition: 0.5.4
1533+ fastify-plugin: 5.1.0
1534+ fastq: 1.20.1
1535+ glob: 11.1.0
1536+
1537+ '@isaacs/cliui@9.0.0': {}
1538+
1407 '@jridgewell/sourcemap-codec@1.5.5': {} 1539 '@jridgewell/sourcemap-codec@1.5.5': {}
1408 1540
1541+ '@lukeed/ms@2.0.2': {}
1542+
1409 '@mixmark-io/domino@2.2.0': {} 1543 '@mixmark-io/domino@2.2.0': {}
1410 1544
1411 '@mozilla/readability@0.5.0': {} 1545 '@mozilla/readability@0.5.0': {}
@@ -1564,10 +1698,16 @@ snapshots:
1564 '@fastify/error': 4.2.0 1698 '@fastify/error': 4.2.0
1565 fastq: 1.20.1 1699 fastq: 1.20.1
1566 1700
1701+ balanced-match@4.0.4: {}
1702+
1567 base64-js@1.5.1: {} 1703 base64-js@1.5.1: {}
1568 1704
1569 boolbase@2.0.0: {} 1705 boolbase@2.0.0: {}
1570 1706
1707+ brace-expansion@5.0.9:
1708+ dependencies:
1709+ balanced-match: 4.0.4
1710+
1571 buffer-from@1.1.2: {} 1711 buffer-from@1.1.2: {}
1572 1712
1573 buffer@6.0.3: 1713 buffer@6.0.3:
@@ -1587,10 +1727,20 @@ snapshots:
1587 1727
1588 check-error@2.1.3: {} 1728 check-error@2.1.3: {}
1589 1729
1730+ content-disposition@0.5.4:
1731+ dependencies:
1732+ safe-buffer: 5.2.1
1733+
1590 cookie@1.1.1: {} 1734 cookie@1.1.1: {}
1591 1735
1592 core-util-is@1.0.3: {} 1736 core-util-is@1.0.3: {}
1593 1737
1738+ cross-spawn@7.0.6:
1739+ dependencies:
1740+ path-key: 3.1.1
1741+ shebang-command: 2.0.0
1742+ which: 2.0.2
1743+
1594 css-select@7.0.0: 1744 css-select@7.0.0:
1595 dependencies: 1745 dependencies:
1596 boolbase: 2.0.0 1746 boolbase: 2.0.0
@@ -1609,6 +1759,8 @@ snapshots:
1609 1759
1610 deep-eql@5.0.2: {} 1760 deep-eql@5.0.2: {}
1611 1761
1762+ depd@2.0.0: {}
1763+
1612 dequal@2.0.3: {} 1764 dequal@2.0.3: {}
1613 1765
1614 dom-serializer@2.0.0: 1766 dom-serializer@2.0.0:
@@ -1728,6 +1880,8 @@ snapshots:
1728 '@esbuild/win32-ia32': 0.28.2 1880 '@esbuild/win32-ia32': 0.28.2
1729 '@esbuild/win32-x64': 0.28.2 1881 '@esbuild/win32-x64': 0.28.2
1730 1882
1883+ escape-html@1.0.3: {}
1884+
1731 estree-walker@3.0.3: 1885 estree-walker@3.0.3:
1732 dependencies: 1886 dependencies:
1733 '@types/estree': 1.0.9 1887 '@types/estree': 1.0.9
@@ -1789,9 +1943,23 @@ snapshots:
1789 fast-querystring: 1.1.2 1943 fast-querystring: 1.1.2
1790 safe-regex2: 5.1.1 1944 safe-regex2: 5.1.1
1791 1945
1946+ foreground-child@3.3.1:
1947+ dependencies:
1948+ cross-spawn: 7.0.6
1949+ signal-exit: 4.1.0
1950+
1792 fsevents@2.3.3: 1951 fsevents@2.3.3:
1793 optional: true 1952 optional: true
1794 1953
1954+ glob@11.1.0:
1955+ dependencies:
1956+ foreground-child: 3.3.1
1957+ jackspeak: 4.2.3
1958+ minimatch: 10.2.6
1959+ minipass: 7.1.3
1960+ package-json-from-dist: 1.0.1
1961+ path-scurry: 2.0.2
1962+
1795 globrex@0.1.2: {} 1963 globrex@0.1.2: {}
1796 1964
1797 html-escaper@3.0.3: {} 1965 html-escaper@3.0.3: {}
@@ -1803,6 +1971,14 @@ snapshots:
1803 domutils: 3.2.2 1971 domutils: 3.2.2
1804 entities: 7.0.1 1972 entities: 7.0.1
1805 1973
1974+ http-errors@2.0.1:
1975+ dependencies:
1976+ depd: 2.0.0
1977+ inherits: 2.0.4
1978+ setprototypeof: 1.2.0
1979+ statuses: 2.0.2
1980+ toidentifier: 1.0.1
1981+
1806 ieee754@1.2.1: {} 1982 ieee754@1.2.1: {}
1807 1983
1808 inherits@2.0.4: {} 1984 inherits@2.0.4: {}
@@ -1811,6 +1987,12 @@ snapshots:
1811 1987
1812 isarray@1.0.0: {} 1988 isarray@1.0.0: {}
1813 1989
1990+ isexe@2.0.0: {}
1991+
1992+ jackspeak@4.2.3:
1993+ dependencies:
1994+ '@isaacs/cliui': 9.0.0
1995+
1814 json-schema-ref-resolver@3.0.0: 1996 json-schema-ref-resolver@3.0.0:
1815 dependencies: 1997 dependencies:
1816 dequal: 2.0.3 1998 dequal: 2.0.3
@@ -1833,12 +2015,20 @@ snapshots:
1833 2015
1834 loupe@3.2.1: {} 2016 loupe@3.2.1: {}
1835 2017
2018+ lru-cache@11.5.2: {}
2019+
1836 magic-string@0.30.21: 2020 magic-string@0.30.21:
1837 dependencies: 2021 dependencies:
1838 '@jridgewell/sourcemap-codec': 1.5.5 2022 '@jridgewell/sourcemap-codec': 1.5.5
1839 2023
1840 mime-db@1.54.0: {} 2024 mime-db@1.54.0: {}
1841 2025
2026+ mime@3.0.0: {}
2027+
2028+ minimatch@10.2.6:
2029+ dependencies:
2030+ brace-expansion: 5.0.9
2031+
1842 minipass@7.1.3: {} 2032 minipass@7.1.3: {}
1843 2033
1844 ms@2.1.3: {} 2034 ms@2.1.3: {}
@@ -1855,6 +2045,15 @@ snapshots:
1855 dependencies: 2045 dependencies:
1856 wrappy: 1.0.2 2046 wrappy: 1.0.2
1857 2047
2048+ package-json-from-dist@1.0.1: {}
2049+
2050+ path-key@3.1.1: {}
2051+
2052+ path-scurry@2.0.2:
2053+ dependencies:
2054+ lru-cache: 11.5.2
2055+ minipass: 7.1.3
2056+
1858 pathe@1.1.2: {} 2057 pathe@1.1.2: {}
1859 2058
1860 pathval@2.0.1: {} 2059 pathval@2.0.1: {}
@@ -1996,8 +2195,18 @@ snapshots:
1996 2195
1997 set-cookie-parser@2.7.2: {} 2196 set-cookie-parser@2.7.2: {}
1998 2197
2198+ setprototypeof@1.2.0: {}
2199+
2200+ shebang-command@2.0.0:
2201+ dependencies:
2202+ shebang-regex: 3.0.0
2203+
2204+ shebang-regex@3.0.0: {}
2205+
1999 siginfo@2.0.0: {} 2206 siginfo@2.0.0: {}
2000 2207
2208+ signal-exit@4.1.0: {}
2209+
2001 sonic-boom@4.2.1: 2210 sonic-boom@4.2.1:
2002 dependencies: 2211 dependencies:
2003 atomic-sleep: 1.0.0 2212 atomic-sleep: 1.0.0
@@ -2008,6 +2217,8 @@ snapshots:
2008 2217
2009 stackback@0.0.2: {} 2218 stackback@0.0.2: {}
2010 2219
2220+ statuses@2.0.2: {}
2221+
2011 std-env@3.10.0: {} 2222 std-env@3.10.0: {}
2012 2223
2013 stream-shift@1.0.3: {} 2224 stream-shift@1.0.3: {}
@@ -2041,6 +2252,8 @@ snapshots:
2041 2252
2042 toad-cache@3.7.4: {} 2253 toad-cache@3.7.4: {}
2043 2254
2255+ toidentifier@1.0.1: {}
2256+
2044 tsconfck@3.1.6(typescript@5.9.3): 2257 tsconfck@3.1.6(typescript@5.9.3):
2045 optionalDependencies: 2258 optionalDependencies:
2046 typescript: 5.9.3 2259 typescript: 5.9.3
@@ -2138,6 +2351,10 @@ snapshots:
2138 - supports-color 2351 - supports-color
2139 - terser 2352 - terser
2140 2353
2354+ which@2.0.2:
2355+ dependencies:
2356+ isexe: 2.0.0
2357+
2141 why-is-node-running@2.3.0: 2358 why-is-node-running@2.3.0:
2142 dependencies: 2359 dependencies:
2143 siginfo: 2.0.0 2360 siginfo: 2.0.0
2144 2361
Menu

Theme