@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,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
|
-
});
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// SPDX-License-Identifier: MIT
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* report-cleanup-candidates.mjs — flag records that look like
|
|
6
|
-
* they should be archived. Wrapper around `grove cleanup stale`,
|
|
7
|
-
* which writes a markdown report under `data/generated/`.
|
|
8
|
-
*/
|
|
9
|
-
import { spawnSync } from "node:child_process";
|
|
10
|
-
|
|
11
|
-
const result = spawnSync("grove", ["cleanup", "stale"], {
|
|
12
|
-
stdio: "inherit",
|
|
13
|
-
env: process.env,
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
if (result.error) {
|
|
17
|
-
console.error("Failed to spawn `grove cleanup stale`:", result.error.message);
|
|
18
|
-
process.exit(1);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
process.exit(result.status ?? 0);
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// SPDX-License-Identifier: MIT
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* seed-from-github.mjs — interactive seeder for the first
|
|
6
|
-
* record. Given a GitHub URL, fetch the metadata and write a
|
|
7
|
-
* starter yml to `data/records/<slug>.yml`. Useful for the
|
|
8
|
-
* "add my repo" onboarding path.
|
|
9
|
-
*/
|
|
10
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
11
|
-
import { parse, stringify } from "yaml";
|
|
12
|
-
import { dirname, join } from "node:path";
|
|
13
|
-
import { fileURLToPath } from "node:url";
|
|
14
|
-
|
|
15
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
|
-
const ROOT = join(__dirname, "..");
|
|
17
|
-
const RECORDS_DIR = join(ROOT, "data", "records");
|
|
18
|
-
|
|
19
|
-
const url = process.argv[2];
|
|
20
|
-
if (!url) {
|
|
21
|
-
console.error("Usage: node seed-from-github.mjs <github-url>");
|
|
22
|
-
process.exit(1);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const match = String(url).match(/github\.com\/([^/]+)\/([^/?#]+)/);
|
|
26
|
-
if (!match) {
|
|
27
|
-
console.error("Not a GitHub URL:", url);
|
|
28
|
-
process.exit(1);
|
|
29
|
-
}
|
|
30
|
-
const owner = match[1];
|
|
31
|
-
const repo = match[2].replace(/\.git$/, "");
|
|
32
|
-
|
|
33
|
-
const res = await fetch(`https://api.github.com/repos/${owner}/${repo}`, {
|
|
34
|
-
headers: { Accept: "application/vnd.github+json", "User-Agent": "grove-seeder" },
|
|
35
|
-
});
|
|
36
|
-
if (!res.ok) {
|
|
37
|
-
console.error("GitHub returned", res.status);
|
|
38
|
-
process.exit(1);
|
|
39
|
-
}
|
|
40
|
-
const r = await res.json();
|
|
41
|
-
const slug = repo.toLowerCase();
|
|
42
|
-
const rec = {
|
|
43
|
-
kind: "project",
|
|
44
|
-
slug,
|
|
45
|
-
name: r.name,
|
|
46
|
-
description: r.description ?? "",
|
|
47
|
-
repoUrl: r.html_url,
|
|
48
|
-
homepageUrl: r.homepage ?? "",
|
|
49
|
-
category: "tools",
|
|
50
|
-
stack: r.language ?? "",
|
|
51
|
-
stacks: [],
|
|
52
|
-
platforms: [],
|
|
53
|
-
tags: r.topics ?? [],
|
|
54
|
-
stars: r.stargazers_count,
|
|
55
|
-
license: r.license?.spdx_id ?? "",
|
|
56
|
-
status: r.archived ? "archived" : "active",
|
|
57
|
-
curation: { reviewed: false, labels: [] },
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
await mkdir(RECORDS_DIR, { recursive: true });
|
|
61
|
-
await writeFile(join(RECORDS_DIR, `${slug}.yml`), stringify(rec), "utf8");
|
|
62
|
-
console.log(`Wrote ${slug}.yml`);
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* sync-contributors — fetch contributors for every record's repoUrl
|
|
4
|
-
* and write `data/generated/contributors.json` (consumed by the home
|
|
5
|
-
* page and the /contributors page).
|
|
6
|
-
*
|
|
7
|
-
* Generic: walks every record in data/generated/records.index.json,
|
|
8
|
-
* looks at record.github.fullName or links.github, and calls the
|
|
9
|
-
* GitHub /repos/{owner}/{repo}/contributors endpoint. Aggregates by
|
|
10
|
-
* owner (across all of an owner's projects in the directory).
|
|
11
|
-
*
|
|
12
|
-
* Anonymous contributors are excluded — the home grid renders <a>
|
|
13
|
-
* avatars, and a name-less contributor is more confusing than
|
|
14
|
-
* missing.
|
|
15
|
-
*
|
|
16
|
-
* Usage: node scripts/sync-contributors.mjs
|
|
17
|
-
* Env: GH_TOKEN — required for >60 req/h. Falls back to unauth.
|
|
18
|
-
*
|
|
19
|
-
* V1 intentionally small: no retries, no rate-limit backoff beyond
|
|
20
|
-
* the basic 403-detection. The weekly schedule in
|
|
21
|
-
* .github/workflows/sync-contributors.yml is loose enough that
|
|
22
|
-
* re-running the workflow is a fine recovery path.
|
|
23
|
-
*/
|
|
24
|
-
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
25
|
-
import { dirname, resolve } from "node:path";
|
|
26
|
-
|
|
27
|
-
const HERE = new URL(".", import.meta.url).pathname;
|
|
28
|
-
const ROOT = resolve(HERE, "..");
|
|
29
|
-
const GENERATED = resolve(ROOT, "data", "generated");
|
|
30
|
-
const INDEX_PATH = resolve(GENERATED, "records.index.json");
|
|
31
|
-
const OUT_PATH = resolve(GENERATED, "contributors.json");
|
|
32
|
-
|
|
33
|
-
const GH_TOKEN = process.env.GH_TOKEN || process.env.GITHUB_TOKEN || "";
|
|
34
|
-
const HEADERS = {
|
|
35
|
-
Accept: "application/vnd.github+json",
|
|
36
|
-
"User-Agent": "grove-sync-contributors",
|
|
37
|
-
"X-GitHub-Api-Version": "2022-11-28",
|
|
38
|
-
...(GH_TOKEN ? { Authorization: `Bearer ${GH_TOKEN}` } : {}),
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
function ownerRepoFromFullName(fullName) {
|
|
42
|
-
if (!fullName || !fullName.includes("/")) return null;
|
|
43
|
-
const [owner, repo] = fullName.split("/", 2);
|
|
44
|
-
return { owner, repo };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async function ghJson(url) {
|
|
48
|
-
const res = await fetch(url, { headers: HEADERS });
|
|
49
|
-
if (res.status === 204) return null;
|
|
50
|
-
if (res.status === 403) {
|
|
51
|
-
const remaining = res.headers.get("x-ratelimit-remaining");
|
|
52
|
-
if (remaining === "0") {
|
|
53
|
-
throw new Error(`rate-limited (403) on ${url}`);
|
|
54
|
-
}
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
if (res.status === 404) return null;
|
|
58
|
-
if (!res.ok) throw new Error(`HTTP ${res.status} on ${url}`);
|
|
59
|
-
return res.json();
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
async function fetchContributorsForRepo(owner, repo) {
|
|
63
|
-
// GitHub caps /contributors at 500; for our purposes (fanning into
|
|
64
|
-
// a home grid) we only need the first page. Anonymous entries
|
|
65
|
-
// (no `login`) are dropped at parse time.
|
|
66
|
-
const url = `https://api.github.com/repos/${owner}/${repo}/contributors?per_page=100&anon=false`;
|
|
67
|
-
const data = await ghJson(url);
|
|
68
|
-
if (!Array.isArray(data)) return [];
|
|
69
|
-
return data
|
|
70
|
-
.filter((c) => c && c.login)
|
|
71
|
-
.map((c) => ({
|
|
72
|
-
username: c.login,
|
|
73
|
-
avatarUrl: c.avatar_url,
|
|
74
|
-
profileUrl: c.html_url,
|
|
75
|
-
contributions: c.contributions ?? 0,
|
|
76
|
-
}));
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
async function main() {
|
|
80
|
-
let index;
|
|
81
|
-
try {
|
|
82
|
-
index = JSON.parse(await readFile(INDEX_PATH, "utf8"));
|
|
83
|
-
} catch (err) {
|
|
84
|
-
console.error(`Could not read ${INDEX_PATH}: ${err.message}`);
|
|
85
|
-
console.error("Run `pnpm run build:data` first.");
|
|
86
|
-
process.exit(1);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const records = Array.isArray(index.records) ? index.records : [];
|
|
90
|
-
// Group by owner — a single person with three projects in the
|
|
91
|
-
// directory shows up once with the summed contribution count.
|
|
92
|
-
const byOwner = new Map();
|
|
93
|
-
const seenRepos = new Set();
|
|
94
|
-
|
|
95
|
-
for (const r of records) {
|
|
96
|
-
const gh = r.github || {};
|
|
97
|
-
const ref = ownerRepoFromFullName(gh.fullName) || (() => {
|
|
98
|
-
const url = r.repoUrl || (r.links && r.links.github) || "";
|
|
99
|
-
const m = /github\.com\/([^/]+)\/([^/?#]+)/.exec(url);
|
|
100
|
-
return m ? { owner: m[1], repo: m[2].replace(/\.git$/, "") } : null;
|
|
101
|
-
})();
|
|
102
|
-
if (!ref) continue;
|
|
103
|
-
if (seenRepos.has(`${ref.owner}/${ref.repo}`)) continue;
|
|
104
|
-
seenRepos.add(`${ref.owner}/${ref.repo}`);
|
|
105
|
-
|
|
106
|
-
try {
|
|
107
|
-
const list = await fetchContributorsForRepo(ref.owner, ref.repo);
|
|
108
|
-
for (const c of list) {
|
|
109
|
-
const existing = byOwner.get(c.username);
|
|
110
|
-
if (existing) {
|
|
111
|
-
existing.contributions = (existing.contributions ?? 0) + c.contributions;
|
|
112
|
-
} else {
|
|
113
|
-
byOwner.set(c.username, c);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
console.log(` ${ref.owner}/${ref.repo}: ${list.length} contributors`);
|
|
117
|
-
} catch (err) {
|
|
118
|
-
console.warn(` ${ref.owner}/${ref.repo}: ${err.message}`);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const contributors = [...byOwner.values()].sort(
|
|
123
|
-
(a, b) => (b.contributions ?? 0) - (a.contributions ?? 0),
|
|
124
|
-
);
|
|
125
|
-
|
|
126
|
-
await mkdir(dirname(OUT_PATH), { recursive: true });
|
|
127
|
-
await writeFile(
|
|
128
|
-
OUT_PATH,
|
|
129
|
-
JSON.stringify(
|
|
130
|
-
{
|
|
131
|
-
generatedAt: new Date().toISOString(),
|
|
132
|
-
contributors,
|
|
133
|
-
},
|
|
134
|
-
null,
|
|
135
|
-
2,
|
|
136
|
-
),
|
|
137
|
-
"utf8",
|
|
138
|
-
);
|
|
139
|
-
console.log(`\nWrote ${contributors.length} contributors → ${OUT_PATH}`);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
main().catch((err) => {
|
|
143
|
-
console.error(err.stack || err.message);
|
|
144
|
-
process.exit(1);
|
|
145
|
-
});
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// SPDX-License-Identifier: MIT
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* sync-github-metadata.mjs — full metadata sync from GitHub's
|
|
6
|
-
* REST API. Wrapper around `@grove-dev/cli sync github`.
|
|
7
|
-
*
|
|
8
|
-
* Refreshes stargazers_count, forks_count, license, default branch,
|
|
9
|
-
* pushed_at, language, topics, and the contributor list for every
|
|
10
|
-
* record whose `repoUrl` is a GitHub repository.
|
|
11
|
-
*
|
|
12
|
-
* Writes back into the corresponding `data/records/<slug>.yml` (the
|
|
13
|
-
* CLI is conservative — only fills in fields that are currently
|
|
14
|
-
* empty).
|
|
15
|
-
*/
|
|
16
|
-
import { spawnSync } from "node:child_process";
|
|
17
|
-
|
|
18
|
-
const result = spawnSync("grove", ["sync", "github"], {
|
|
19
|
-
stdio: "inherit",
|
|
20
|
-
env: process.env,
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
if (result.error) {
|
|
24
|
-
console.error("Failed to spawn `grove sync github`:", result.error.message);
|
|
25
|
-
process.exit(1);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
process.exit(result.status ?? 0);
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// SPDX-License-Identifier: MIT
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* validate-records.mjs — Zod-schema validation for every record yml.
|
|
6
|
-
*
|
|
7
|
-
* Thin wrapper around `@grove-dev/cli`'s `validate` command so the
|
|
8
|
-
* `pnpm run validate:data` script works without V0 command names.
|
|
9
|
-
*
|
|
10
|
-
* For schema authoring see `packages/core/src/schema.ts`. The CLI
|
|
11
|
-
* handles every blueprint (project-directory, resource-hub,
|
|
12
|
-
* ecosystem-map) and every record kind (project, resource, entity).
|
|
13
|
-
*/
|
|
14
|
-
import { spawnSync } from "node:child_process";
|
|
15
|
-
|
|
16
|
-
const result = spawnSync("grove", ["validate"], {
|
|
17
|
-
stdio: "inherit",
|
|
18
|
-
env: process.env,
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
if (result.error) {
|
|
22
|
-
console.error("Failed to spawn `grove validate`:", result.error.message);
|
|
23
|
-
console.error("Make sure `@grove-dev/cli` is installed and on PATH.");
|
|
24
|
-
process.exit(1);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
process.exit(result.status ?? 0);
|