@grove-dev/astro 0.2.20 → 0.3.2

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.
Files changed (171) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +9 -75
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +11 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/lib/display.d.ts +1 -26
  7. package/dist/lib/display.d.ts.map +1 -1
  8. package/dist/lib/display.js +1 -103
  9. package/dist/lib/display.js.map +1 -1
  10. package/dist/lib/format.d.ts +1 -28
  11. package/dist/lib/format.d.ts.map +1 -1
  12. package/dist/lib/format.js +1 -71
  13. package/dist/lib/format.js.map +1 -1
  14. package/dist/lib/lenses.d.ts +1 -57
  15. package/dist/lib/lenses.d.ts.map +1 -1
  16. package/dist/lib/lenses.js +1 -198
  17. package/dist/lib/lenses.js.map +1 -1
  18. package/dist/lib/repo.d.ts +1 -19
  19. package/dist/lib/repo.d.ts.map +1 -1
  20. package/dist/lib/repo.js +1 -30
  21. package/dist/lib/repo.js.map +1 -1
  22. package/dist/lib/scores.d.ts +1 -36
  23. package/dist/lib/scores.d.ts.map +1 -1
  24. package/dist/lib/scores.js +1 -75
  25. package/dist/lib/scores.js.map +1 -1
  26. package/dist/lib/search.d.ts +1 -113
  27. package/dist/lib/search.d.ts.map +1 -1
  28. package/dist/lib/search.js +1 -379
  29. package/dist/lib/search.js.map +1 -1
  30. package/dist/lib/taxonomy-counts.d.ts +1 -10
  31. package/dist/lib/taxonomy-counts.d.ts.map +1 -1
  32. package/dist/lib/taxonomy-counts.js +1 -21
  33. package/dist/lib/taxonomy-counts.js.map +1 -1
  34. package/package.json +22 -10
  35. package/src/components/ContributorsGrid.astro +3 -3
  36. package/src/components/CurationGrid.astro +2 -2
  37. package/src/components/DirectoryIndexClient.astro +240 -0
  38. package/src/components/FilterGroupMenu.astro +8 -4
  39. package/src/components/FinalCta.astro +41 -0
  40. package/src/components/Hero.astro +15 -18
  41. package/src/components/Icon.astro +99 -27
  42. package/src/components/RefinePanel.astro +7 -7
  43. package/src/components/SmartLensTabs.astro +5 -4
  44. package/src/components/StackGrid.astro +1 -1
  45. package/src/components/SubmissionClient.astro +133 -0
  46. package/src/index.ts +11 -1
  47. package/src/layouts/BaseLayout.astro +19 -1
  48. package/src/layouts/Footer.astro +25 -56
  49. package/src/layouts/Header.astro +33 -15
  50. package/src/layouts/ThemeToggle.astro +7 -3
  51. package/src/lib/display.ts +1 -111
  52. package/src/lib/format.ts +1 -64
  53. package/src/lib/lenses.ts +1 -220
  54. package/src/lib/repo.ts +1 -32
  55. package/src/lib/scores.ts +1 -90
  56. package/src/lib/search.ts +1 -418
  57. package/src/lib/taxonomy-counts.ts +1 -26
  58. package/{templates/default/src/data/records.ts → src/server/directory.ts} +90 -8
  59. package/src/server/index.ts +2 -0
  60. package/src/server/models.ts +432 -0
  61. package/dist/theme.test.d.ts +0 -2
  62. package/dist/theme.test.d.ts.map +0 -1
  63. package/dist/theme.test.js +0 -55
  64. package/dist/theme.test.js.map +0 -1
  65. package/src/theme.test.ts +0 -67
  66. package/templates/default/.github/ISSUE_TEMPLATE/bug_report.md +0 -33
  67. package/templates/default/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
  68. package/templates/default/.github/ISSUE_TEMPLATE/record_submission.md +0 -48
  69. package/templates/default/.github/pull_request_template.md +0 -27
  70. package/templates/default/.github/workflows/build.yml +0 -55
  71. package/templates/default/.github/workflows/cleanup-stale-records.yml +0 -48
  72. package/templates/default/.github/workflows/daily-refresh.yml +0 -53
  73. package/templates/default/.github/workflows/sync-contributors.yml +0 -53
  74. package/templates/default/.github/workflows/sync-github-metadata.yml +0 -58
  75. package/templates/default/.github/workflows/validate-data.yml +0 -35
  76. package/templates/default/astro.config.mjs +0 -25
  77. package/templates/default/content/pages/about.md +0 -66
  78. package/templates/default/content/pages/methodology.md +0 -103
  79. package/templates/default/data/records/.gitkeep +0 -0
  80. package/templates/default/data/records/coolify.yml +0 -46
  81. package/templates/default/data/records/gitea.yml +0 -46
  82. package/templates/default/data/records/hoppscotch.yml +0 -47
  83. package/templates/default/data/records/inventree.yml +0 -47
  84. package/templates/default/data/records/label-studio.yml +0 -46
  85. package/templates/default/data/records/logseq.yml +0 -48
  86. package/templates/default/data/records/private-gpt.yml +0 -47
  87. package/templates/default/data/records/wakapi.yml +0 -44
  88. package/templates/default/data/taxonomy/categories.yml +0 -47
  89. package/templates/default/data/taxonomy/distribution-channels.yml +0 -43
  90. package/templates/default/data/taxonomy/platforms.yml +0 -27
  91. package/templates/default/data/taxonomy/stacks.yml +0 -41
  92. package/templates/default/grove.config.ts +0 -36
  93. package/templates/default/package.json +0 -38
  94. package/templates/default/public/icons/platforms/android.svg +0 -1
  95. package/templates/default/public/icons/platforms/apple-dark.svg +0 -1
  96. package/templates/default/public/icons/platforms/apple-light.svg +0 -1
  97. package/templates/default/public/icons/platforms/chrome.svg +0 -1
  98. package/templates/default/public/icons/platforms/chromeos.svg +0 -5
  99. package/templates/default/public/icons/platforms/desktop.svg +0 -4
  100. package/templates/default/public/icons/platforms/embedded.svg +0 -5
  101. package/templates/default/public/icons/platforms/ios.svg +0 -1
  102. package/templates/default/public/icons/platforms/linux.svg +0 -1
  103. package/templates/default/public/icons/platforms/macos.svg +0 -1
  104. package/templates/default/public/icons/platforms/ubuntu.svg +0 -1
  105. package/templates/default/public/icons/platforms/web.svg +0 -5
  106. package/templates/default/public/icons/platforms/windows.svg +0 -6
  107. package/templates/default/public/icons/stacks/android.svg +0 -1
  108. package/templates/default/public/icons/stacks/apple-dark.svg +0 -1
  109. package/templates/default/public/icons/stacks/apple-light.svg +0 -1
  110. package/templates/default/public/icons/stacks/bun.svg +0 -1
  111. package/templates/default/public/icons/stacks/capacitor.svg +0 -14
  112. package/templates/default/public/icons/stacks/clojure.svg +0 -1
  113. package/templates/default/public/icons/stacks/clojurescript.svg +0 -1
  114. package/templates/default/public/icons/stacks/dart.svg +0 -1
  115. package/templates/default/public/icons/stacks/deno.svg +0 -1
  116. package/templates/default/public/icons/stacks/django.svg +0 -1
  117. package/templates/default/public/icons/stacks/docker.svg +0 -1
  118. package/templates/default/public/icons/stacks/firebase.svg +0 -1
  119. package/templates/default/public/icons/stacks/flutter.svg +0 -1
  120. package/templates/default/public/icons/stacks/go.svg +0 -1
  121. package/templates/default/public/icons/stacks/graphql.svg +0 -1
  122. package/templates/default/public/icons/stacks/ionic.svg +0 -4
  123. package/templates/default/public/icons/stacks/java.svg +0 -1
  124. package/templates/default/public/icons/stacks/javascript.svg +0 -1
  125. package/templates/default/public/icons/stacks/kotlin.svg +0 -1
  126. package/templates/default/public/icons/stacks/llm.svg +0 -1
  127. package/templates/default/public/icons/stacks/mongodb.svg +0 -1
  128. package/templates/default/public/icons/stacks/node.js.svg +0 -1
  129. package/templates/default/public/icons/stacks/nodejs.svg +0 -1
  130. package/templates/default/public/icons/stacks/python.svg +0 -1
  131. package/templates/default/public/icons/stacks/rag.svg +0 -7
  132. package/templates/default/public/icons/stacks/react-native.svg +0 -1
  133. package/templates/default/public/icons/stacks/react.svg +0 -1
  134. package/templates/default/public/icons/stacks/rust.svg +0 -1
  135. package/templates/default/public/icons/stacks/solidity.svg +0 -1
  136. package/templates/default/public/icons/stacks/sveltekit.svg +0 -1
  137. package/templates/default/public/icons/stacks/swift.svg +0 -1
  138. package/templates/default/public/icons/stacks/tensorflow.svg +0 -1
  139. package/templates/default/public/icons/stacks/typescript.svg +0 -1
  140. package/templates/default/public/icons/stacks/vue.svg +0 -1
  141. package/templates/default/public/llms-full.txt +0 -206
  142. package/templates/default/public/llms.txt +0 -112
  143. package/templates/default/public/og-image.svg +0 -48
  144. package/templates/default/public/robots.txt +0 -53
  145. package/templates/default/public/sitemap.xml +0 -63
  146. package/templates/default/scripts/build-llms.mjs +0 -123
  147. package/templates/default/scripts/build-records-json.mjs +0 -27
  148. package/templates/default/scripts/build-sitemap.mjs +0 -20
  149. package/templates/default/scripts/cleanup-stale-records.mjs +0 -20
  150. package/templates/default/scripts/enrich-github-metadata.mjs +0 -99
  151. package/templates/default/scripts/fetch-icons.mjs +0 -137
  152. package/templates/default/scripts/migrate-legacy-to-schema-v1.mjs +0 -86
  153. package/templates/default/scripts/parse-legacy-readme.mjs +0 -59
  154. package/templates/default/scripts/refresh-records-activity.mjs +0 -24
  155. package/templates/default/scripts/repair-license-format.mjs +0 -72
  156. package/templates/default/scripts/report-cleanup-candidates.mjs +0 -21
  157. package/templates/default/scripts/seed-from-github.mjs +0 -62
  158. package/templates/default/scripts/sync-contributors.mjs +0 -145
  159. package/templates/default/scripts/sync-github-metadata.mjs +0 -28
  160. package/templates/default/scripts/validate-records.mjs +0 -27
  161. package/templates/default/src/pages/404.astro +0 -64
  162. package/templates/default/src/pages/[slug]/[recordSlug].astro +0 -689
  163. package/templates/default/src/pages/[slug]/index.astro +0 -278
  164. package/templates/default/src/pages/about.astro +0 -182
  165. package/templates/default/src/pages/apps/[recordSlug].astro +0 -33
  166. package/templates/default/src/pages/contributors.astro +0 -175
  167. package/templates/default/src/pages/index.astro +0 -225
  168. package/templates/default/src/pages/sitemap.xml.ts +0 -89
  169. package/templates/default/src/pages/submit.astro +0 -281
  170. package/templates/default/src/styles/global.css +0 -33
  171. package/templates/default/wrangler.jsonc +0 -28
@@ -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
- });
@@ -1,86 +0,0 @@
1
- #!/usr/bin/env node
2
- // SPDX-License-Identifier: MIT
3
-
4
- /**
5
- * migrate-legacy-to-schema-v1.mjs — best-effort mapper from the
6
- * legacy OpenSourceApp shape (openapps v0 schema) into the
7
- * Grove V1 record shape (kind: project + Resource union).
8
- *
9
- * Reads `data/legacy/*.yml` (if present), writes
10
- * `data/records/*.yml`. Field-by-field mapping is documented
11
- * inline.
12
- */
13
- import { readFile, writeFile, readdir, mkdir } from "node:fs/promises";
14
- import { parse, stringify } from "yaml";
15
- import { dirname, basename, join } from "node:path";
16
- import { fileURLToPath } from "node:url";
17
-
18
- const __dirname = dirname(fileURLToPath(import.meta.url));
19
- const ROOT = join(__dirname, "..");
20
- const LEGACY_DIR = join(ROOT, "data", "legacy");
21
- const RECORDS_DIR = join(ROOT, "data", "records");
22
- const DRY_RUN = process.env.DRY_RUN === "1";
23
-
24
- function map(legacy) {
25
- // Best-effort: openapps → grove V1. Fields with no clear
26
- // equivalent are left for a human to fill in.
27
- return {
28
- kind: "project",
29
- slug: legacy.slug ?? basename(legacy.__file ?? "unknown", ".yml"),
30
- name: legacy.name ?? legacy.title ?? "",
31
- description: legacy.description ?? legacy.summary ?? "",
32
- repoUrl: legacy.repoUrl ?? legacy.source?.url ?? "",
33
- homepageUrl: legacy.homepageUrl ?? legacy.links?.website ?? "",
34
- category: legacy.category ?? "tools",
35
- stack: legacy.stack ?? legacy.stacks?.[0] ?? "",
36
- stacks: Array.isArray(legacy.stacks) ? legacy.stacks.slice(1).filter(Boolean) : [],
37
- platforms: legacy.platforms ?? [],
38
- tags: legacy.tags ?? [],
39
- stars: legacy.stars,
40
- license: legacy.license,
41
- status: legacy.status,
42
- bestFor: legacy.bestFor ?? [],
43
- whyListed: legacy.whyListed ?? [],
44
- caveats: legacy.caveats ?? [],
45
- curation: {
46
- reviewed: Boolean(legacy.curation?.reviewed),
47
- by: legacy.curation?.by,
48
- date: legacy.curation?.date,
49
- labels: legacy.labels ?? legacy.curation?.labels ?? [],
50
- },
51
- };
52
- }
53
-
54
- async function main() {
55
- let files = [];
56
- try {
57
- files = (await readdir(LEGACY_DIR)).filter((f) => f.endsWith(".yml")).sort();
58
- } catch {
59
- console.error("No data/legacy directory. Drop legacy yml files in there and re-run.");
60
- process.exit(1);
61
- }
62
-
63
- if (!DRY_RUN) await mkdir(RECORDS_DIR, { recursive: true });
64
- let migrated = 0;
65
- for (const file of files) {
66
- const text = await readFile(join(LEGACY_DIR, file), "utf8");
67
- const raw = parse(text) ?? {};
68
- raw.__file = file;
69
- const rec = map(raw);
70
- const slug = rec.slug || basename(file, ".yml");
71
- const dest = join(RECORDS_DIR, `${slug}.yml`);
72
- if (DRY_RUN) {
73
- console.log(` ~ ${file} → ${slug}.yml (dry-run)`);
74
- } else {
75
- await writeFile(dest, stringify(rec), "utf8");
76
- console.log(` ✓ ${file} → ${slug}.yml`);
77
- }
78
- migrated++;
79
- }
80
- console.log(`Migrated ${migrated} legacy record${migrated === 1 ? "" : "s"}.`);
81
- }
82
-
83
- main().catch((err) => {
84
- console.error("migrate-legacy-to-schema-v1 failed:", err);
85
- process.exit(1);
86
- });
@@ -1,59 +0,0 @@
1
- #!/usr/bin/env node
2
- // SPDX-License-Identifier: MIT
3
-
4
- /**
5
- * parse-legacy-readme.mjs — best-effort parser for legacy yml
6
- * records that ship a `readme` block (Markdown body + frontmatter)
7
- * instead of the V1 schema fields.
8
- *
9
- * Walks `data/records/*.yml`, splits `readme` into a `content`
10
- * file under `content/records/<slug>.md`, and produces a
11
- * `migration-report.md` summarizing which fields it filled in
12
- * (or could not).
13
- */
14
- import { readFile, writeFile, readdir, mkdir } from "node:fs/promises";
15
- import { dirname, basename, join, resolve } from "node:path";
16
- import { fileURLToPath } from "node:url";
17
-
18
- const __dirname = dirname(fileURLToPath(import.meta.url));
19
- const ROOT = join(__dirname, "..");
20
- const RECORDS_DIR = join(ROOT, "data", "records");
21
- const CONTENT_DIR = join(ROOT, "content", "records");
22
- const DRY_RUN = process.env.DRY_RUN === "1";
23
-
24
- async function main() {
25
- let files = [];
26
- try {
27
- files = (await readdir(RECORDS_DIR)).filter((f) => f.endsWith(".yml")).sort();
28
- } catch {
29
- console.error("No data/records directory.");
30
- process.exit(1);
31
- }
32
-
33
- const report = [];
34
- for (const file of files) {
35
- const path = join(RECORDS_DIR, file);
36
- const text = await readFile(path, "utf8");
37
- if (!/^readme:\s*\|/m.test(text)) {
38
- report.push(`${file}: no legacy readme block`);
39
- continue;
40
- }
41
- const slug = basename(file, ".yml");
42
- const outPath = join(CONTENT_DIR, `${slug}.md`);
43
- if (!DRY_RUN) {
44
- await mkdir(dirname(outPath), { recursive: true });
45
- const body = text
46
- .split(/^readme:\s*\|\s*$/m)[1] ?? ""
47
- .split(/^[a-z]+:/m)[0] ?? "";
48
- await writeFile(outPath, body.trim(), "utf8");
49
- }
50
- report.push(`${file}: extracted readme → content/records/${slug}.md`);
51
- }
52
-
53
- console.log(report.join("\n"));
54
- }
55
-
56
- main().catch((err) => {
57
- console.error("parse-legacy-readme failed:", err);
58
- process.exit(1);
59
- });
@@ -1,24 +0,0 @@
1
- #!/usr/bin/env node
2
- // SPDX-License-Identifier: MIT
3
-
4
- /**
5
- * refresh-records-activity.mjs — daily activity refresh.
6
- *
7
- * Wrapper around `grove sync github` (the same command used by
8
- * the weekly metadata sync, but the action wires it to a daily
9
- * cron so the directory stays current with each app's recent
10
- * commit history and GitHub stars).
11
- */
12
- import { spawnSync } from "node:child_process";
13
-
14
- const result = spawnSync("grove", ["sync", "github"], {
15
- stdio: "inherit",
16
- env: process.env,
17
- });
18
-
19
- if (result.error) {
20
- console.error("Failed to spawn `grove sync github`:", result.error.message);
21
- process.exit(1);
22
- }
23
-
24
- process.exit(result.status ?? 0);
@@ -1,72 +0,0 @@
1
- #!/usr/bin/env node
2
- // SPDX-License-Identifier: MIT
3
-
4
- /**
5
- * repair-license-format.mjs — normalize the `license` field on
6
- * every record yml. Maps common GitHub-shaped license keys
7
- * ("agpl-3.0", "Apache-2.0", "mit") to their SPDX IDs.
8
- */
9
- import { readFile, writeFile, readdir } from "node:fs/promises";
10
- import { parse, stringify } from "node:yaml";
11
- import { dirname, join } from "node:path";
12
- import { fileURLToPath } from "node:url";
13
-
14
- const __dirname = dirname(fileURLToPath(import.meta.url));
15
- const ROOT = join(__dirname, "..");
16
- const RECORDS_DIR = join(ROOT, "data", "records");
17
- const DRY_RUN = process.env.DRY_RUN === "1";
18
-
19
- const LICENSE_MAP = {
20
- "mit": "MIT",
21
- "apache-2.0": "Apache-2.0",
22
- "apache 2.0": "Apache-2.0",
23
- "gpl-3.0": "GPL-3.0",
24
- "gpl-2.0": "GPL-2.0",
25
- "lgpl-2.1": "LGPL-2.1",
26
- "lgpl-3.0": "LGPL-3.0",
27
- "agpl-3.0": "AGPL-3.0",
28
- "bsd-3-clause": "BSD-3-Clause",
29
- "bsd-2-clause": "BSD-2-Clause",
30
- "isc": "ISC",
31
- "mpl-2.0": "MPL-2.0",
32
- "unlicense": "Unlicense",
33
- "wtfpl": "WTFPL",
34
- };
35
-
36
- function normalize(license) {
37
- if (!license) return license;
38
- const k = String(license).trim().toLowerCase();
39
- return LICENSE_MAP[k] ?? license;
40
- }
41
-
42
- async function main() {
43
- let files = [];
44
- try {
45
- files = (await readdir(RECORDS_DIR)).filter((f) => f.endsWith(".yml")).sort();
46
- } catch {
47
- console.error("No data/records directory. Run `grove init` first.");
48
- process.exit(1);
49
- }
50
-
51
- let repaired = 0;
52
- for (const file of files) {
53
- const path = join(RECORDS_DIR, file);
54
- const rec = parse(await readFile(path, "utf8")) ?? {};
55
- const normalized = normalize(rec.license);
56
- if (normalized === rec.license) continue;
57
- rec.license = normalized;
58
- if (DRY_RUN) {
59
- console.log(` ~ ${file} → ${normalized}`);
60
- } else {
61
- await writeFile(path, stringify(rec), "utf8");
62
- console.log(` ✓ ${file} → ${normalized}`);
63
- }
64
- repaired++;
65
- }
66
- console.log(`Repaired ${repaired} record${repaired === 1 ? "" : "s"}.`);
67
- }
68
-
69
- main().catch((err) => {
70
- console.error("repair-license-format failed:", err);
71
- process.exit(1);
72
- });