@grove-dev/astro 0.2.19 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/template-routes.test.d.ts +2 -0
- package/dist/template-routes.test.d.ts.map +1 -0
- package/dist/template-routes.test.js +63 -0
- package/dist/template-routes.test.js.map +1 -0
- package/dist/theme.test.js +19 -0
- package/dist/theme.test.js.map +1 -1
- package/package.json +4 -4
- package/src/components/CategoryGrid.astro +2 -11
- package/src/components/ContributorsGrid.astro +2 -11
- package/src/components/CurationGrid.astro +1 -1
- package/src/components/DecisionRow.astro +1 -1
- package/src/components/ExploreByCategory.astro +3 -3
- package/src/components/ExploreByStack.astro +3 -3
- package/src/components/FilterGroupMenu.astro +5 -5
- package/src/components/Hero.astro +8 -8
- package/src/components/Icon.astro +56 -53
- package/src/components/IndexRow.astro +5 -5
- package/src/components/ItemCard.astro +5 -16
- package/src/components/MinimalAbout.astro +2 -2
- package/src/components/OriginalCollection.astro +4 -4
- package/src/components/Pagination.astro +2 -2
- package/src/components/RecordSection.astro +1 -1
- package/src/components/RefinePanel.astro +1 -1
- package/src/components/SmartLensTabs.astro +3 -3
- package/src/components/StackGrid.astro +6 -15
- package/src/components/WhyThisExists.astro +3 -3
- package/src/layouts/Container.astro +4 -4
- package/src/layouts/Footer.astro +3 -3
- package/src/layouts/Header.astro +5 -5
- package/src/layouts/ThemeToggle.astro +1 -1
- package/src/styles.css +33 -339
- package/src/template-routes.test.ts +103 -0
- package/src/theme.test.ts +24 -0
- package/templates/default/.github/workflows/build.yml +1 -3
- package/templates/default/.github/workflows/cleanup-stale-records.yml +1 -3
- package/templates/default/.github/workflows/daily-refresh.yml +1 -3
- package/templates/default/.github/workflows/sync-contributors.yml +2 -4
- package/templates/default/.github/workflows/sync-github-metadata.yml +1 -3
- package/templates/default/.github/workflows/validate-data.yml +1 -3
- package/templates/default/grove.config.ts +1 -1
- package/templates/default/package.json +9 -4
- package/templates/default/public/icons/stacks/rag.svg +7 -0
- package/templates/default/public/llms-full.txt +63 -186
- package/templates/default/public/llms.txt +6 -107
- package/templates/default/public/sitemap.xml +10 -10
- package/templates/default/src/data/records.ts +80 -2
- package/templates/default/src/pages/404.astro +5 -5
- package/templates/default/src/pages/[slug]/[recordSlug].astro +36 -38
- package/templates/default/src/pages/[slug]/index.astro +223 -19
- package/templates/default/src/pages/about.astro +17 -10
- package/templates/default/src/pages/apps/[recordSlug].astro +2 -1
- package/templates/default/src/pages/contributors.astro +8 -17
- package/templates/default/src/pages/index.astro +11 -3
- package/templates/default/src/pages/sitemap.xml.ts +3 -3
- package/templates/default/src/pages/submit.astro +28 -36
- package/templates/default/tsconfig.json +9 -0
- package/templates/default/scripts/build-llms.mjs +0 -123
- package/templates/default/scripts/build-records-json.mjs +0 -27
- package/templates/default/scripts/build-sitemap.mjs +0 -20
- package/templates/default/scripts/cleanup-stale-records.mjs +0 -20
- package/templates/default/scripts/enrich-github-metadata.mjs +0 -99
- package/templates/default/scripts/fetch-icons.mjs +0 -137
- package/templates/default/scripts/migrate-legacy-to-schema-v1.mjs +0 -86
- package/templates/default/scripts/parse-legacy-readme.mjs +0 -59
- package/templates/default/scripts/refresh-records-activity.mjs +0 -24
- package/templates/default/scripts/repair-license-format.mjs +0 -72
- package/templates/default/scripts/report-cleanup-candidates.mjs +0 -21
- package/templates/default/scripts/seed-from-github.mjs +0 -62
- package/templates/default/scripts/sync-contributors.mjs +0 -145
- package/templates/default/scripts/sync-github-metadata.mjs +0 -28
- package/templates/default/scripts/validate-records.mjs +0 -27
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import siteConfig from "../../data/generated/site-config.json";
|
|
3
|
-
import {
|
|
3
|
+
import { fullProjects, itemLabel } from "../data/records";
|
|
4
4
|
import BaseLayout from "@grove-dev/astro/layouts/BaseLayout.astro";
|
|
5
5
|
import Container from "@grove-dev/astro/layouts/Container.astro";
|
|
6
6
|
|
|
@@ -10,12 +10,12 @@ const description = `Submit a new ${singular} to ${siteConfig.name}.`;
|
|
|
10
10
|
const repoUrl = siteConfig.repoUrl ?? "https://github.com/tortuvshin/grove";
|
|
11
11
|
|
|
12
12
|
const values = (key: "category" | "stack") =>
|
|
13
|
-
[...new Set(
|
|
13
|
+
[...new Set(fullProjects.map((item) => String(item[key] ?? "")).filter(Boolean))]
|
|
14
14
|
.sort((a, b) => a.localeCompare(b));
|
|
15
15
|
const categories = values("category");
|
|
16
16
|
const stacks = values("stack");
|
|
17
|
-
const existingSlugs =
|
|
18
|
-
const existingFullNames =
|
|
17
|
+
const existingSlugs = fullProjects.map((item) => item.slug).filter(Boolean);
|
|
18
|
+
const existingFullNames = fullProjects
|
|
19
19
|
.map((item) => {
|
|
20
20
|
const url = String(item.repoUrl ?? item.links?.github ?? "");
|
|
21
21
|
const match = url.match(/github\.com\/([^/]+)\/([^/?#]+)/i);
|
|
@@ -60,7 +60,7 @@ const pleaseAvoid = [
|
|
|
60
60
|
["2", "Edit the draft", "Fix the category, stack, platforms, and notes."],
|
|
61
61
|
["3", "Open a pull request", "Reviewers will validate and merge the record."],
|
|
62
62
|
].map(([number, heading, copy]) => (
|
|
63
|
-
<li class="
|
|
63
|
+
<li class="rounded-[calc(var(--radius)+0.25rem)] border-0 bg-muted shadow-none transition-colors flex items-start gap-2 p-3">
|
|
64
64
|
<span class="mt-0.5 inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-full border border-ink-300 font-mono font-semibold text-ink-700 dark:border-ink-700 dark:text-ink-300">{number}</span>
|
|
65
65
|
<div>
|
|
66
66
|
<div class="font-medium text-ink-900 dark:text-ink-100">{heading}</div>
|
|
@@ -71,43 +71,43 @@ const pleaseAvoid = [
|
|
|
71
71
|
</ol>
|
|
72
72
|
|
|
73
73
|
<div class="mt-8 grid grid-cols-1 gap-6 lg:grid-cols-[minmax(0,1fr)_minmax(360px,0.8fr)]">
|
|
74
|
-
<form id="submit-form" class="
|
|
74
|
+
<form id="submit-form" class="rounded-[calc(var(--radius)+0.25rem)] border-0 bg-muted shadow-none transition-colors p-5">
|
|
75
75
|
<div class="grid gap-5">
|
|
76
76
|
<label class="grid gap-1.5">
|
|
77
|
-
<span class="
|
|
77
|
+
<span class="text-2xs font-medium uppercase tracking-[.05em] text-muted-foreground">GitHub URL</span>
|
|
78
78
|
<div class="flex flex-col gap-2 sm:flex-row">
|
|
79
|
-
<input id="repo-url" type="url" required placeholder="https://github.com/owner/repo" class="
|
|
80
|
-
<button id="fetch-repo" type="button" class="
|
|
79
|
+
<input id="repo-url" type="url" required placeholder="https://github.com/owner/repo" class="min-h-10 flex-1 rounded-[var(--radius)] border border-input bg-background px-3 py-2 text-sm text-foreground outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/35" />
|
|
80
|
+
<button id="fetch-repo" type="button" class="inline-flex h-8 cursor-pointer items-center justify-center gap-1.5 whitespace-nowrap rounded-[var(--radius)] border border-transparent bg-primary px-3 text-sm font-medium leading-none text-primary-foreground no-underline outline-none transition-colors hover:bg-primary/90 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/35 min-h-10 justify-center">Generate draft</button>
|
|
81
81
|
</div>
|
|
82
|
-
<span class="
|
|
82
|
+
<span class="text-2xs text-muted-foreground">The repository must be public. We never write to it.</span>
|
|
83
83
|
</label>
|
|
84
84
|
|
|
85
85
|
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
86
|
-
<label class="grid gap-1.5"><span class="
|
|
87
|
-
<label class="grid gap-1.5"><span class="
|
|
86
|
+
<label class="grid gap-1.5"><span class="text-2xs font-medium uppercase tracking-[.05em] text-muted-foreground">Name</span><input id="item-name" type="text" class="min-h-10 rounded-[var(--radius)] border border-input bg-background px-3 py-2 text-sm text-foreground outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/35" /></label>
|
|
87
|
+
<label class="grid gap-1.5"><span class="text-2xs font-medium uppercase tracking-[.05em] text-muted-foreground">Slug</span><input id="item-slug" type="text" class="min-h-10 rounded-[var(--radius)] border border-input bg-background px-3 py-2 font-mono text-sm text-foreground outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/35" /><span class="text-2xs text-muted-foreground">URL fragment, auto-filled from the repository name.</span></label>
|
|
88
88
|
</div>
|
|
89
89
|
|
|
90
90
|
<label class="grid gap-1.5">
|
|
91
|
-
<span class="
|
|
92
|
-
<textarea id="item-description" rows="3" class="
|
|
93
|
-
<span class="
|
|
91
|
+
<span class="text-2xs font-medium uppercase tracking-[.05em] text-muted-foreground">Description</span>
|
|
92
|
+
<textarea id="item-description" rows="3" class="min-h-10 resize-y rounded-[var(--radius)] border border-input bg-background px-3 py-2 text-sm text-foreground outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/35"></textarea>
|
|
93
|
+
<span class="text-2xs text-muted-foreground">One or two sentences in plain language.</span>
|
|
94
94
|
</label>
|
|
95
95
|
|
|
96
96
|
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
97
97
|
<label class="grid gap-1.5">
|
|
98
|
-
<span class="
|
|
99
|
-
<input id="item-category" list="category-options" class="
|
|
98
|
+
<span class="text-2xs font-medium uppercase tracking-[.05em] text-muted-foreground">Category</span>
|
|
99
|
+
<input id="item-category" list="category-options" class="min-h-10 rounded-[var(--radius)] border border-input bg-background px-3 py-2 text-sm text-foreground outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/35" />
|
|
100
100
|
<datalist id="category-options">{categories.map((value) => <option value={value} />)}</datalist>
|
|
101
101
|
</label>
|
|
102
102
|
<label class="grid gap-1.5">
|
|
103
|
-
<span class="
|
|
104
|
-
<input id="primary-stack" list="stack-options" class="
|
|
103
|
+
<span class="text-2xs font-medium uppercase tracking-[.05em] text-muted-foreground">Primary stack</span>
|
|
104
|
+
<input id="primary-stack" list="stack-options" class="min-h-10 rounded-[var(--radius)] border border-input bg-background px-3 py-2 text-sm text-foreground outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/35" />
|
|
105
105
|
<datalist id="stack-options">{stacks.map((value) => <option value={value} />)}</datalist>
|
|
106
106
|
</label>
|
|
107
107
|
</div>
|
|
108
108
|
|
|
109
109
|
<fieldset class="grid gap-2">
|
|
110
|
-
<legend class="
|
|
110
|
+
<legend class="text-2xs font-medium uppercase tracking-[.05em] text-muted-foreground">Platforms</legend>
|
|
111
111
|
<div class="grid grid-cols-2 gap-2 sm:grid-cols-3">
|
|
112
112
|
{platformOptions.map((platform) => (
|
|
113
113
|
<label class="flex min-h-9 items-center gap-2 rounded-md border border-ink-200 px-2 text-sm dark:border-ink-800">
|
|
@@ -118,14 +118,14 @@ const pleaseAvoid = [
|
|
|
118
118
|
</div>
|
|
119
119
|
</fieldset>
|
|
120
120
|
|
|
121
|
-
<label class="grid gap-1.5"><span class="
|
|
122
|
-
<label class="grid gap-1.5"><span class="
|
|
123
|
-
<label class="grid gap-1.5"><span class="
|
|
121
|
+
<label class="grid gap-1.5"><span class="text-2xs font-medium uppercase tracking-[.05em] text-muted-foreground">Tags</span><input id="item-tags" type="text" placeholder="self-hosted, developer-tools, offline-first" class="min-h-10 rounded-[var(--radius)] border border-input bg-background px-3 py-2 text-sm text-foreground outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/35" /></label>
|
|
122
|
+
<label class="grid gap-1.5"><span class="text-2xs font-medium uppercase tracking-[.05em] text-muted-foreground">Website</span><input id="item-website" type="url" placeholder="https://example.com" class="min-h-10 rounded-[var(--radius)] border border-input bg-background px-3 py-2 text-sm text-foreground outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/35" /></label>
|
|
123
|
+
<label class="grid gap-1.5"><span class="text-2xs font-medium uppercase tracking-[.05em] text-muted-foreground">Best for</span><textarea id="best-for" rows="3" class="min-h-10 resize-y rounded-[var(--radius)] border border-input bg-background px-3 py-2 text-sm text-foreground outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/35" placeholder="One use case per line"></textarea></label>
|
|
124
124
|
</div>
|
|
125
125
|
</form>
|
|
126
126
|
|
|
127
127
|
<aside class="flex flex-col gap-4">
|
|
128
|
-
<div class="
|
|
128
|
+
<div class="rounded-[calc(var(--radius)+0.25rem)] border-0 bg-muted shadow-none transition-colors p-5">
|
|
129
129
|
<h2 class="m-0 text-[0.9375rem] font-semibold text-ink-900 dark:text-ink-100">Submission criteria</h2>
|
|
130
130
|
<p class="m-0 mt-1 text-2xs text-ink-500 dark:text-ink-400">Use this checklist before opening the draft.</p>
|
|
131
131
|
<div class="mt-4 grid grid-cols-1 gap-4 text-2xs sm:grid-cols-2 lg:grid-cols-1 xl:grid-cols-2">
|
|
@@ -140,12 +140,12 @@ const pleaseAvoid = [
|
|
|
140
140
|
</div>
|
|
141
141
|
</div>
|
|
142
142
|
|
|
143
|
-
<div class="
|
|
143
|
+
<div class="rounded-[calc(var(--radius)+0.25rem)] border-0 bg-muted shadow-none transition-colors flex min-h-[420px] flex-col p-5">
|
|
144
144
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
|
145
145
|
<h2 class="m-0 text-base font-semibold text-ink-900 dark:text-ink-100">YAML draft</h2>
|
|
146
146
|
<div class="flex items-center gap-2">
|
|
147
|
-
<button id="copy-yaml" type="button" class="
|
|
148
|
-
<a id="open-pr-link" href="#" target="_blank" rel="noopener noreferrer" class="
|
|
147
|
+
<button id="copy-yaml" type="button" class="inline-flex h-8 cursor-pointer items-center justify-center gap-1.5 whitespace-nowrap rounded-[var(--radius)] border border-border bg-secondary px-3 text-sm font-medium leading-none text-foreground no-underline outline-none transition-colors hover:bg-accent focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/35 pointer-events-none opacity-50" disabled>Copy YAML</button>
|
|
148
|
+
<a id="open-pr-link" href="#" target="_blank" rel="noopener noreferrer" class="inline-flex h-8 cursor-pointer items-center justify-center gap-1.5 whitespace-nowrap rounded-[var(--radius)] border border-border bg-secondary px-3 text-sm font-medium leading-none text-foreground no-underline outline-none transition-colors hover:bg-accent focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/35 pointer-events-none opacity-50">Open PR draft</a>
|
|
149
149
|
</div>
|
|
150
150
|
</div>
|
|
151
151
|
<pre class="mt-4 min-h-0 flex-1 overflow-auto rounded-md border border-ink-200 bg-ink-50 p-3 text-xs leading-relaxed text-ink-800 dark:border-ink-800 dark:bg-ink-900/60 dark:text-ink-200"><code id="yaml-preview">Paste a GitHub URL to generate a draft.</code></pre>
|
|
@@ -156,14 +156,6 @@ const pleaseAvoid = [
|
|
|
156
156
|
</section>
|
|
157
157
|
</Container>
|
|
158
158
|
|
|
159
|
-
<style>
|
|
160
|
-
.field { min-height: 2.5rem; border-radius: .375rem; border: 1px solid var(--color-ink-200); background: white; padding: .5rem .75rem; color: var(--color-ink-900); font-size: .875rem; outline: none; }
|
|
161
|
-
.field:focus { border-color: var(--color-ink-500); }
|
|
162
|
-
:global(.dark) .field { border-color: var(--color-ink-800); background: var(--color-ink-950); color: var(--color-ink-100); }
|
|
163
|
-
.label { font-size: var(--text-2xs); font-weight: 500; text-transform: uppercase; letter-spacing: .05em; color: var(--color-ink-500); }
|
|
164
|
-
.help { font-size: var(--text-2xs); color: var(--color-ink-500); }
|
|
165
|
-
</style>
|
|
166
|
-
|
|
167
159
|
<script is:inline define:vars={{ existingSlugs, existingFullNames, repoUrl }}>
|
|
168
160
|
// @ts-nocheck
|
|
169
161
|
const $ = (selector) => document.querySelector(selector);
|
|
@@ -225,7 +217,7 @@ const pleaseAvoid = [
|
|
|
225
217
|
...(inputs.website.value ? [` website: ${inputs.website.value}`] : []),
|
|
226
218
|
"bestFor:", bestFor.length ? lines(bestFor) : " []",
|
|
227
219
|
"source:",
|
|
228
|
-
" type:
|
|
220
|
+
" type: manual",
|
|
229
221
|
` owner: ${parsed.owner}`,
|
|
230
222
|
` repo: ${parsed.repo}`,
|
|
231
223
|
"curation:",
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// SPDX-License-Identifier: MIT
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* build-llms.mjs — emit `public/llms-full.txt` (per-record
|
|
6
|
-
* markdown mirror) only.
|
|
7
|
-
*
|
|
8
|
-
* `public/llms.txt` is a static file committed to the template —
|
|
9
|
-
* it does NOT get regenerated by this script. The build pipeline
|
|
10
|
-
* treats the committed version as the canonical, hand-curated
|
|
11
|
-
* LLM-facing index. The script only refreshes the per-record
|
|
12
|
-
* `llms-full.txt` from the latest `data/generated/records.full.json`.
|
|
13
|
-
*
|
|
14
|
-
* This is intentional: openapps' `llms.txt` is also a
|
|
15
|
-
* hand-curated 30+ line document, not an auto-generated stub.
|
|
16
|
-
*
|
|
17
|
-
* Usage: node scripts/build-llms.mjs
|
|
18
|
-
*/
|
|
19
|
-
import { mkdir, readFile, writeFile, access } from "node:fs/promises";
|
|
20
|
-
import { dirname, join, resolve } from "node:path";
|
|
21
|
-
import { fileURLToPath } from "node:url";
|
|
22
|
-
|
|
23
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
24
|
-
const ROOT = resolve(__dirname, "..");
|
|
25
|
-
const SOURCE = join(ROOT, "data", "generated", "records.full.json");
|
|
26
|
-
const OUT_DIR = join(ROOT, "public");
|
|
27
|
-
const FULL = join(OUT_DIR, "llms-full.txt");
|
|
28
|
-
|
|
29
|
-
const SITE = process.env.GROVE_SITE ?? "https://example.com";
|
|
30
|
-
|
|
31
|
-
function compactStars(n) {
|
|
32
|
-
if (typeof n !== "number" || !Number.isFinite(n)) return "—";
|
|
33
|
-
if (n >= 1_000_000) return (n / 1_000_000).toFixed(1).replace(/\.0$/, "") + "M";
|
|
34
|
-
if (n >= 1_000) {
|
|
35
|
-
const k = n / 1_000;
|
|
36
|
-
return (k >= 10 ? Math.round(k).toString() : k.toFixed(1).replace(/\.0$/, "")) + "k";
|
|
37
|
-
}
|
|
38
|
-
return n.toString();
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function relativeTime(iso) {
|
|
42
|
-
if (!iso) return "—";
|
|
43
|
-
const d = new Date(iso);
|
|
44
|
-
if (isNaN(d.valueOf())) return "—";
|
|
45
|
-
const days = Math.floor((Date.now() - d.valueOf()) / 86_400_000);
|
|
46
|
-
if (days === 0) return "today";
|
|
47
|
-
if (days === 1) return "1 day ago";
|
|
48
|
-
if (days < 30) return `${days} days ago`;
|
|
49
|
-
if (days < 365) return `${Math.floor(days / 30)} months ago`;
|
|
50
|
-
return `${Math.floor(days / 365)} years ago`;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function buildFullTxt(records) {
|
|
54
|
-
const lines = [];
|
|
55
|
-
lines.push(`# ${SITE.replace(/^https?:\/\//, "")} — full directory`);
|
|
56
|
-
lines.push("");
|
|
57
|
-
lines.push(`Generated: ${new Date().toISOString()}`);
|
|
58
|
-
lines.push(`Records: ${records.length}`);
|
|
59
|
-
lines.push("");
|
|
60
|
-
lines.push("---");
|
|
61
|
-
lines.push("");
|
|
62
|
-
|
|
63
|
-
for (const r of records) {
|
|
64
|
-
const name = r.name ?? r.title ?? r.slug;
|
|
65
|
-
const stack = r.stack ?? (Array.isArray(r.stacks) ? r.stacks.join(", ") : "");
|
|
66
|
-
const platforms = Array.isArray(r.platforms) ? r.platforms.join(", ") : "";
|
|
67
|
-
const tags = Array.isArray(r.tags) ? r.tags.join(", ") : "";
|
|
68
|
-
const stars = compactStars(r.stars ?? r.github?.stars ?? 0);
|
|
69
|
-
const updated = relativeTime(r.lastCommitAt ?? r.github?.pushedAt ?? null);
|
|
70
|
-
const repoUrl = r.repoUrl ?? r.links?.github ?? "";
|
|
71
|
-
const url = `${SITE.replace(/\/$/, "")}/${r.slug}`;
|
|
72
|
-
const desc = r.description ?? "";
|
|
73
|
-
|
|
74
|
-
lines.push(`## ${name}`);
|
|
75
|
-
lines.push("");
|
|
76
|
-
if (desc) lines.push(desc);
|
|
77
|
-
lines.push("");
|
|
78
|
-
lines.push(`- **URL**: ${url}`);
|
|
79
|
-
if (repoUrl) lines.push(`- **Repository**: ${repoUrl}`);
|
|
80
|
-
if (stack) lines.push(`- **Stack**: ${stack}`);
|
|
81
|
-
if (platforms) lines.push(`- **Platforms**: ${platforms}`);
|
|
82
|
-
if (r.category) lines.push(`- **Category**: ${r.category}`);
|
|
83
|
-
if (tags) lines.push(`- **Tags**: ${tags}`);
|
|
84
|
-
if (r.license) lines.push(`- **License**: ${r.license}`);
|
|
85
|
-
lines.push(`- **Stars**: ${stars}`);
|
|
86
|
-
lines.push(`- **Last commit**: ${updated}`);
|
|
87
|
-
lines.push("");
|
|
88
|
-
if (Array.isArray(r.bestFor) && r.bestFor.length) {
|
|
89
|
-
lines.push("**Best for:**");
|
|
90
|
-
for (const b of r.bestFor) lines.push(`- ${b}`);
|
|
91
|
-
lines.push("");
|
|
92
|
-
}
|
|
93
|
-
if (Array.isArray(r.whyListed) && r.whyListed.length) {
|
|
94
|
-
lines.push("**Why listed:**");
|
|
95
|
-
for (const w of r.whyListed) lines.push(`- ${w}`);
|
|
96
|
-
lines.push("");
|
|
97
|
-
}
|
|
98
|
-
lines.push("---");
|
|
99
|
-
lines.push("");
|
|
100
|
-
}
|
|
101
|
-
return lines.join("\n");
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
async function main() {
|
|
105
|
-
if (!(await access(SOURCE).then(() => true).catch(() => false))) {
|
|
106
|
-
console.error(`No ${SOURCE} found. Run \`grove generate\` first.`);
|
|
107
|
-
process.exit(1);
|
|
108
|
-
}
|
|
109
|
-
const raw = JSON.parse(await readFile(SOURCE, "utf8"));
|
|
110
|
-
const records = (raw.records ?? []).filter(
|
|
111
|
-
(r) => r.visibility !== "hide" && r.visibility !== "remove",
|
|
112
|
-
);
|
|
113
|
-
|
|
114
|
-
await mkdir(OUT_DIR, { recursive: true });
|
|
115
|
-
await writeFile(FULL, buildFullTxt(records), "utf8");
|
|
116
|
-
console.log(`[llms] ${records.length} indexed → ${FULL}`);
|
|
117
|
-
console.log(`[llms] public/llms.txt is committed (NOT regenerated) — see scripts/build-llms.mjs`);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
main().catch((err) => {
|
|
121
|
-
console.error("build-llms failed:", err);
|
|
122
|
-
process.exit(1);
|
|
123
|
-
});
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// SPDX-License-Identifier: MIT
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* build-records-json.mjs — yml → normalized JSON.
|
|
6
|
-
*
|
|
7
|
-
* Wraps `@grove-dev/cli generate`, which produces:
|
|
8
|
-
* - data/generated/records.full.json (every record, all fields)
|
|
9
|
-
* - data/generated/records.index.json (slim projection, visible only)
|
|
10
|
-
* - data/generated/records.json (alias of records.full.json)
|
|
11
|
-
* - data/generated/site-config.json (current blueprint + stats)
|
|
12
|
-
*
|
|
13
|
-
* Run by `pnpm run build:data` (also pre-build).
|
|
14
|
-
*/
|
|
15
|
-
import { spawnSync } from "node:child_process";
|
|
16
|
-
|
|
17
|
-
const result = spawnSync("grove", ["generate"], {
|
|
18
|
-
stdio: "inherit",
|
|
19
|
-
env: process.env,
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
if (result.error) {
|
|
23
|
-
console.error("Failed to spawn `grove generate`:", result.error.message);
|
|
24
|
-
process.exit(1);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
process.exit(result.status ?? 0);
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// SPDX-License-Identifier: MIT
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* build-sitemap.mjs — emit public/sitemap.xml from the generated
|
|
6
|
-
* index JSON. Thin wrapper around `@grove-dev/cli sitemap`.
|
|
7
|
-
*/
|
|
8
|
-
import { spawnSync } from "node:child_process";
|
|
9
|
-
|
|
10
|
-
const result = spawnSync("grove", ["sitemap"], {
|
|
11
|
-
stdio: "inherit",
|
|
12
|
-
env: process.env,
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
if (result.error) {
|
|
16
|
-
console.error("Failed to spawn `grove sitemap`:", result.error.message);
|
|
17
|
-
process.exit(1);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
process.exit(result.status ?? 0);
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// SPDX-License-Identifier: MIT
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* cleanup-stale-records.mjs — flag records that look like they
|
|
6
|
-
* should be archived. Wrapper around `grove cleanup stale`.
|
|
7
|
-
*/
|
|
8
|
-
import { spawnSync } from "node:child_process";
|
|
9
|
-
|
|
10
|
-
const result = spawnSync("grove", ["cleanup", "stale"], {
|
|
11
|
-
stdio: "inherit",
|
|
12
|
-
env: process.env,
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
if (result.error) {
|
|
16
|
-
console.error("Failed to spawn `grove cleanup stale`:", result.error.message);
|
|
17
|
-
process.exit(1);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
process.exit(result.status ?? 0);
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// SPDX-License-Identifier: MIT
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* enrich-github-metadata.mjs — HTML fallback for records that
|
|
6
|
-
* haven't been touched by the API sync. Walks the public GitHub
|
|
7
|
-
* HTML page for each repo and pulls topics, language, license,
|
|
8
|
-
* homepage into the yml file.
|
|
9
|
-
*
|
|
10
|
-
* Lightweight (no auth required), but slower and less reliable
|
|
11
|
-
* than `sync-github-metadata.mjs` (the API version). Use this as
|
|
12
|
-
* a fallback when the API rate-limit is exhausted.
|
|
13
|
-
*/
|
|
14
|
-
import { readFile, writeFile, readdir } from "node:fs/promises";
|
|
15
|
-
import { parse, stringify } from "yaml";
|
|
16
|
-
import { dirname, join } from "node:path";
|
|
17
|
-
import { fileURLToPath } from "node:url";
|
|
18
|
-
|
|
19
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
|
-
const ROOT = join(__dirname, "..");
|
|
21
|
-
const RECORDS_DIR = join(ROOT, "data", "records");
|
|
22
|
-
const DRY_RUN = process.env.DRY_RUN === "1";
|
|
23
|
-
|
|
24
|
-
const HEADERS = {
|
|
25
|
-
Accept: "text/html,application/xhtml+xml",
|
|
26
|
-
"User-Agent": "grove-enricher",
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
function ownerRepo(repoUrl) {
|
|
30
|
-
if (!repoUrl) return null;
|
|
31
|
-
const m = String(repoUrl).match(/github\.com\/([^/]+)\/([^/?#]+)/);
|
|
32
|
-
if (!m) return null;
|
|
33
|
-
return { owner: m[1], repo: m[2].replace(/\.git$/, "") };
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
async function fetchHtml(owner, repo) {
|
|
37
|
-
try {
|
|
38
|
-
const res = await fetch(`https://github.com/${owner}/${repo}`, { headers: HEADERS });
|
|
39
|
-
if (!res.ok) return null;
|
|
40
|
-
return await res.text();
|
|
41
|
-
} catch {
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function extractMeta(html) {
|
|
47
|
-
if (!html) return {};
|
|
48
|
-
const out = {};
|
|
49
|
-
const topicMatches = html.matchAll(/data-octo-click="topic_click"[^>]*>([^<]+)</g);
|
|
50
|
-
if (topicMatches) {
|
|
51
|
-
const topics = [...topicMatches].map((m) => m[1].trim()).filter(Boolean);
|
|
52
|
-
if (topics.length) out.topics = topics;
|
|
53
|
-
}
|
|
54
|
-
const lang = html.match(/<span class="color-fg-default text-bold mr-1"[^>]*>([^<]+)</);
|
|
55
|
-
if (lang) out.language = lang[1].trim();
|
|
56
|
-
const license = html.match(/This repository is under the ([^<.]+) License/i);
|
|
57
|
-
if (license) out.license = license[1].trim();
|
|
58
|
-
return out;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
async function main() {
|
|
62
|
-
let files = [];
|
|
63
|
-
try {
|
|
64
|
-
files = (await readdir(RECORDS_DIR)).filter((f) => f.endsWith(".yml")).sort();
|
|
65
|
-
} catch (err) {
|
|
66
|
-
console.error("No data/records directory. Run `grove init` first.");
|
|
67
|
-
process.exit(1);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
let enriched = 0;
|
|
71
|
-
for (const file of files) {
|
|
72
|
-
const path = join(RECORDS_DIR, file);
|
|
73
|
-
const text = await readFile(path, "utf8");
|
|
74
|
-
const rec = parse(text) ?? {};
|
|
75
|
-
const { owner, repo } = ownerRepo(rec.repoUrl ?? rec.links?.github) ?? {};
|
|
76
|
-
if (!owner) continue;
|
|
77
|
-
const html = await fetchHtml(owner, repo);
|
|
78
|
-
const meta = extractMeta(html);
|
|
79
|
-
const before = JSON.stringify(rec);
|
|
80
|
-
if (meta.topics && !rec.topics) rec.topics = meta.topics;
|
|
81
|
-
if (meta.language && !rec.language) rec.language = meta.language;
|
|
82
|
-
if (meta.license && !rec.license) rec.license = meta.license;
|
|
83
|
-
const after = JSON.stringify(rec);
|
|
84
|
-
if (before === after) continue;
|
|
85
|
-
if (DRY_RUN) {
|
|
86
|
-
console.log(` ~ ${file} (dry-run)`);
|
|
87
|
-
} else {
|
|
88
|
-
await writeFile(path, stringify(rec), "utf8");
|
|
89
|
-
console.log(` ✓ ${file}`);
|
|
90
|
-
}
|
|
91
|
-
enriched++;
|
|
92
|
-
}
|
|
93
|
-
console.log(`Enriched ${enriched} record${enriched === 1 ? "" : "s"}.`);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
main().catch((err) => {
|
|
97
|
-
console.error("enrich-github-metadata failed:", err);
|
|
98
|
-
process.exit(1);
|
|
99
|
-
});
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// SPDX-License-Identifier: MIT
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* fetch-icons.mjs — download brand icons from the public
|
|
6
|
-
* `xandemon/developer-icons` repository (CC0 licensed) and save them
|
|
7
|
-
* to `public/icons/` so Astro can serve them as static assets.
|
|
8
|
-
*
|
|
9
|
-
* Run automatically before dev/build via the npm scripts.
|
|
10
|
-
*
|
|
11
|
-
* Icon map below mirrors the openapps catalogue. Add or remove
|
|
12
|
-
* entries as the blueprint's taxonomy evolves.
|
|
13
|
-
*/
|
|
14
|
-
import { mkdir, writeFile, access } from "node:fs/promises";
|
|
15
|
-
import { dirname, join, resolve } from "node:path";
|
|
16
|
-
import { fileURLToPath } from "node:url";
|
|
17
|
-
|
|
18
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
|
-
const ROOT = resolve(__dirname, "..");
|
|
20
|
-
const OUT_BASE = join(ROOT, "public", "icons");
|
|
21
|
-
|
|
22
|
-
const SOURCE = "https://raw.githubusercontent.com/xandemon/developer-icons/main/icons";
|
|
23
|
-
|
|
24
|
-
// Stack icons (xandemon/developer-icons naming).
|
|
25
|
-
// Names suffixed with `(custom)` are hand-drawn SVGs committed
|
|
26
|
-
// to the repo because the upstream xandemon/developer-icons
|
|
27
|
-
// collection doesn't include them. The script preserves them on
|
|
28
|
-
// subsequent runs (it never overwrites an existing file unless
|
|
29
|
-
// the URL fetch succeeds).
|
|
30
|
-
const STACK_ICONS = [
|
|
31
|
-
"android",
|
|
32
|
-
"apple-dark",
|
|
33
|
-
"apple-light",
|
|
34
|
-
"bun", // custom
|
|
35
|
-
"capacitor",
|
|
36
|
-
"clojure", // custom
|
|
37
|
-
"clojurescript", // custom
|
|
38
|
-
"dart",
|
|
39
|
-
"deno", // custom
|
|
40
|
-
"django", // custom
|
|
41
|
-
"docker", // custom
|
|
42
|
-
"firebase",
|
|
43
|
-
"flutter",
|
|
44
|
-
"go", // custom
|
|
45
|
-
"graphql",
|
|
46
|
-
"ionic",
|
|
47
|
-
"java",
|
|
48
|
-
"javascript",
|
|
49
|
-
"kotlin",
|
|
50
|
-
"llm", // custom
|
|
51
|
-
"mongodb",
|
|
52
|
-
"node.js", // xandemon uses "nodejs"; we ship "node.js" for the home page link
|
|
53
|
-
"nodejs", // (alias — both should resolve to the same file)
|
|
54
|
-
"python",
|
|
55
|
-
"react",
|
|
56
|
-
"react-native",
|
|
57
|
-
"rust",
|
|
58
|
-
"solidity",
|
|
59
|
-
"sveltekit", // custom
|
|
60
|
-
"swift",
|
|
61
|
-
"tensorflow",
|
|
62
|
-
"typescript",
|
|
63
|
-
"vue", // custom
|
|
64
|
-
];
|
|
65
|
-
|
|
66
|
-
// Platform icons (custom paths under /icons/platforms/).
|
|
67
|
-
const PLATFORM_ICONS = [
|
|
68
|
-
"android",
|
|
69
|
-
"apple-dark",
|
|
70
|
-
"apple-light",
|
|
71
|
-
"chrome",
|
|
72
|
-
"chromeos",
|
|
73
|
-
"desktop",
|
|
74
|
-
"embedded",
|
|
75
|
-
"ios",
|
|
76
|
-
"linux",
|
|
77
|
-
"macos",
|
|
78
|
-
"ubuntu",
|
|
79
|
-
"web",
|
|
80
|
-
"windows",
|
|
81
|
-
];
|
|
82
|
-
|
|
83
|
-
async function exists(p) {
|
|
84
|
-
try { await access(p); return true; } catch { return false; }
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
async function fetchAndSave(url, dest) {
|
|
88
|
-
const res = await fetch(url);
|
|
89
|
-
if (!res.ok) throw new Error(`Failed to fetch ${url}: ${res.status}`);
|
|
90
|
-
const buf = Buffer.from(await res.arrayBuffer());
|
|
91
|
-
await mkdir(dirname(dest), { recursive: true });
|
|
92
|
-
await writeFile(dest, buf);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
async function main() {
|
|
96
|
-
// Stack icons
|
|
97
|
-
const stackDir = join(OUT_BASE, "stacks");
|
|
98
|
-
if (!(await exists(stackDir))) await mkdir(stackDir, { recursive: true });
|
|
99
|
-
for (const name of STACK_ICONS) {
|
|
100
|
-
const url = `${SOURCE}/${name}/${name}-original.svg`;
|
|
101
|
-
const dest = join(stackDir, `${name}.svg`);
|
|
102
|
-
try {
|
|
103
|
-
await fetchAndSave(url, dest);
|
|
104
|
-
console.log(` ✓ stack/${name}`);
|
|
105
|
-
} catch (err) {
|
|
106
|
-
console.warn(` ✗ stack/${name}: ${err.message}`);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Platform icons (apple-dark / apple-light already downloaded as
|
|
111
|
-
// stack icons; we re-use them under platforms/).
|
|
112
|
-
const platDir = join(OUT_BASE, "platforms");
|
|
113
|
-
if (!(await exists(platDir))) await mkdir(platDir, { recursive: true });
|
|
114
|
-
for (const name of PLATFORM_ICONS) {
|
|
115
|
-
const src = join(stackDir, `${name}.svg`);
|
|
116
|
-
const dest = join(platDir, `${name}.svg`);
|
|
117
|
-
if (await exists(src)) {
|
|
118
|
-
const buf = await (await import("node:fs/promises")).readFile(src);
|
|
119
|
-
await writeFile(dest, buf);
|
|
120
|
-
console.log(` ✓ platform/${name} (from stack cache)`);
|
|
121
|
-
} else {
|
|
122
|
-
// Try a direct fetch as a fallback.
|
|
123
|
-
const url = `${SOURCE}/${name}/${name}-original.svg`;
|
|
124
|
-
try {
|
|
125
|
-
await fetchAndSave(url, dest);
|
|
126
|
-
console.log(` ✓ platform/${name}`);
|
|
127
|
-
} catch (err) {
|
|
128
|
-
console.warn(` ✗ platform/${name}: ${err.message}`);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
main().catch((err) => {
|
|
135
|
-
console.error("fetch-icons failed:", err);
|
|
136
|
-
process.exit(1);
|
|
137
|
-
});
|