@grove-dev/core 0.1.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/README.md +89 -0
- package/dist/build-data.d.ts +55 -0
- package/dist/build-data.d.ts.map +1 -0
- package/dist/build-data.js +115 -0
- package/dist/build-data.js.map +1 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +14 -0
- package/dist/config.js.map +1 -0
- package/dist/decisions.d.ts +28 -0
- package/dist/decisions.d.ts.map +1 -0
- package/dist/decisions.js +57 -0
- package/dist/decisions.js.map +1 -0
- package/dist/enrich.d.ts +26 -0
- package/dist/enrich.d.ts.map +1 -0
- package/dist/enrich.js +121 -0
- package/dist/enrich.js.map +1 -0
- package/dist/github-client.d.ts +28 -0
- package/dist/github-client.d.ts.map +1 -0
- package/dist/github-client.js +91 -0
- package/dist/github-client.js.map +1 -0
- package/dist/github.d.ts +8 -0
- package/dist/github.d.ts.map +1 -0
- package/dist/github.js +62 -0
- package/dist/github.js.map +1 -0
- package/dist/health.d.ts +3 -0
- package/dist/health.d.ts.map +1 -0
- package/dist/health.js +103 -0
- package/dist/health.js.map +1 -0
- package/dist/importer.d.ts +3 -0
- package/dist/importer.d.ts.map +1 -0
- package/dist/importer.js +24 -0
- package/dist/importer.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/io.d.ts +4 -0
- package/dist/io.d.ts.map +1 -0
- package/dist/io.js +21 -0
- package/dist/io.js.map +1 -0
- package/dist/llms.d.ts +34 -0
- package/dist/llms.d.ts.map +1 -0
- package/dist/llms.js +99 -0
- package/dist/llms.js.map +1 -0
- package/dist/markdown.d.ts +18 -0
- package/dist/markdown.d.ts.map +1 -0
- package/dist/markdown.js +182 -0
- package/dist/markdown.js.map +1 -0
- package/dist/parseReadme.d.ts +25 -0
- package/dist/parseReadme.d.ts.map +1 -0
- package/dist/parseReadme.js +100 -0
- package/dist/parseReadme.js.map +1 -0
- package/dist/schema.d.ts +988 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +729 -0
- package/dist/schema.js.map +1 -0
- package/dist/sitemap.d.ts +38 -0
- package/dist/sitemap.d.ts.map +1 -0
- package/dist/sitemap.js +73 -0
- package/dist/sitemap.js.map +1 -0
- package/dist/slug.d.ts +3 -0
- package/dist/slug.d.ts.map +1 -0
- package/dist/slug.js +15 -0
- package/dist/slug.js.map +1 -0
- package/dist/validate.d.ts +11 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +58 -0
- package/dist/validate.js.map +1 -0
- package/package.json +50 -0
package/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# `@grove-dev/core`
|
|
2
|
+
|
|
3
|
+
> Generic, framework-free engine for Grove.
|
|
4
|
+
|
|
5
|
+
Headless TypeScript. Owns the resource schema, config loader, importers, validators, taxonomy types, optional GitHub signal sync, sitemap generation, llms.txt generation, and the data build pipeline. Zero framework dependencies — no Astro, no React, no Svelte.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @grove-dev/core
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## What it does
|
|
12
|
+
|
|
13
|
+
- **Resource schema** — Zod-validated generic `Resource` model (name, description, type, url, repository, topics, tags, status, maintainers, organizations, metadata).
|
|
14
|
+
- **Config** — `defineGroveConfig()` and `loadConfig()` for `curated.config.ts` projects.
|
|
15
|
+
- **Importers** — parse Markdown awesome lists into structured records.
|
|
16
|
+
- **Validators** — schema + reference checks (duplicate ids, missing descriptions, broken categories, unknown decisions).
|
|
17
|
+
- **Taxonomy** — typed registry for stacks, platforms, categories, distribution channels.
|
|
18
|
+
- **Build pipeline** — `buildData()` produces `data/generated/apps.{full,index}.json` and a typed `src/data/config.ts`.
|
|
19
|
+
- **Sitemap & llms.txt** — `buildSitemap()` and `buildLlmsFiles()` for SEO and LLM-friendly output.
|
|
20
|
+
- **Optional GitHub signal sync** — `fetchGithubMetadata()`, `classifyHealth()`, `enrichFromGithubHtml()` for spaces that want maintenance signals.
|
|
21
|
+
- **Rate-limit aware** — `ghFetch()` and `pLimit()` for safe concurrent HTTP.
|
|
22
|
+
|
|
23
|
+
## Public surface
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import {
|
|
27
|
+
// Config
|
|
28
|
+
defineConfig,
|
|
29
|
+
loadConfig,
|
|
30
|
+
|
|
31
|
+
// Schemas
|
|
32
|
+
curatedConfigSchema,
|
|
33
|
+
resourceSchema,
|
|
34
|
+
healthEntrySchema,
|
|
35
|
+
decisionSchema,
|
|
36
|
+
itemsFileSchema,
|
|
37
|
+
|
|
38
|
+
// Build pipeline
|
|
39
|
+
buildData,
|
|
40
|
+
buildSiteConfigTs,
|
|
41
|
+
|
|
42
|
+
// Sitemap / llms
|
|
43
|
+
buildSitemap,
|
|
44
|
+
buildSitemapXml,
|
|
45
|
+
buildLlmsFiles,
|
|
46
|
+
buildLlmsTxt,
|
|
47
|
+
buildLlmsFullTxt,
|
|
48
|
+
|
|
49
|
+
// Decision / review
|
|
50
|
+
buildReviewReport,
|
|
51
|
+
pickReviewCandidates,
|
|
52
|
+
|
|
53
|
+
// Importers / parsers
|
|
54
|
+
importAwesomeList,
|
|
55
|
+
parseAppYaml,
|
|
56
|
+
normalizeAppRecord,
|
|
57
|
+
toIndexApp,
|
|
58
|
+
parseGithubRepoUrl,
|
|
59
|
+
|
|
60
|
+
// GitHub
|
|
61
|
+
fetchGithubMetadata,
|
|
62
|
+
classifyHealth,
|
|
63
|
+
healthFromSignals,
|
|
64
|
+
enrichFromGithubHtml,
|
|
65
|
+
ghFetch,
|
|
66
|
+
pLimit,
|
|
67
|
+
|
|
68
|
+
// IO
|
|
69
|
+
readYamlFile,
|
|
70
|
+
writeYamlFile,
|
|
71
|
+
writeTextFile,
|
|
72
|
+
stringifyAppYaml,
|
|
73
|
+
|
|
74
|
+
// Validate
|
|
75
|
+
validateProject,
|
|
76
|
+
validateAppRecord,
|
|
77
|
+
} from "@grove-dev/core";
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Development
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
pnpm --filter @grove-dev/core build
|
|
84
|
+
pnpm --filter @grove-dev/core check
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## License
|
|
88
|
+
|
|
89
|
+
MIT
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { type CuratedConfig } from "./schema.js";
|
|
2
|
+
/**
|
|
3
|
+
* The full payload written to data/generated/apps.full.json.
|
|
4
|
+
* Carries every normalized record, regardless of visibility.
|
|
5
|
+
*/
|
|
6
|
+
export interface AppsFullPayload {
|
|
7
|
+
schemaVersion: number;
|
|
8
|
+
generatedAt: string;
|
|
9
|
+
totalApps: number;
|
|
10
|
+
visibleApps: number;
|
|
11
|
+
apps: Array<Record<string, unknown>>;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The slim payload written to data/generated/apps.index.json.
|
|
15
|
+
* Carries only what list and search pages need.
|
|
16
|
+
*/
|
|
17
|
+
export interface AppsIndexPayload {
|
|
18
|
+
schemaVersion: number;
|
|
19
|
+
generatedAt: string;
|
|
20
|
+
totalApps: number;
|
|
21
|
+
apps: Array<Record<string, unknown>>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The TS file written to src/data/config.ts. Sites import this for
|
|
25
|
+
* a typed view of curated.config.ts. Auto-generated; do not edit.
|
|
26
|
+
*/
|
|
27
|
+
export interface SiteConfigTsInput {
|
|
28
|
+
name: string;
|
|
29
|
+
tagline: string;
|
|
30
|
+
description: string;
|
|
31
|
+
siteUrl: string;
|
|
32
|
+
repoUrl: string;
|
|
33
|
+
itemLabel: string;
|
|
34
|
+
}
|
|
35
|
+
export declare function buildSiteConfigTs(input: SiteConfigTsInput): string;
|
|
36
|
+
/**
|
|
37
|
+
* Read every apps/*.yml in the project, normalize, and build:
|
|
38
|
+
* - data/generated/apps.full.json (full records, all visibility)
|
|
39
|
+
* - data/generated/apps.index.json (slim, visible-only)
|
|
40
|
+
* - data/generated/apps.json (compatibility alias of apps.full.json)
|
|
41
|
+
* - src/data/config.ts (typed view of curated.config.ts)
|
|
42
|
+
*
|
|
43
|
+
* Returns a small summary; throws on schema errors.
|
|
44
|
+
*/
|
|
45
|
+
export interface BuildDataResult {
|
|
46
|
+
totalApps: number;
|
|
47
|
+
visibleApps: number;
|
|
48
|
+
fullPath: string;
|
|
49
|
+
indexPath: string;
|
|
50
|
+
aliasPath: string;
|
|
51
|
+
configTsPath: string;
|
|
52
|
+
errors: string[];
|
|
53
|
+
}
|
|
54
|
+
export declare function buildData(cwd?: string, config?: CuratedConfig): Promise<BuildDataResult>;
|
|
55
|
+
//# sourceMappingURL=build-data.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-data.d.ts","sourceRoot":"","sources":["../src/build-data.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,aAAa,EAInB,MAAM,aAAa,CAAC;AAGrB;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACtC;AAED;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACtC;AAED;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,iBAAiB,GAAG,MAAM,CAwBlE;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,wBAAsB,SAAS,CAC7B,GAAG,SAAgB,EACnB,MAAM,CAAC,EAAE,aAAa,GACrB,OAAO,CAAC,eAAe,CAAC,CAyF1B"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { basename, join, resolve } from "node:path";
|
|
3
|
+
import { normalizeAppRecord, parseAppYaml, toIndexApp, } from "./schema.js";
|
|
4
|
+
import { loadConfig } from "./config.js";
|
|
5
|
+
export function buildSiteConfigTs(input) {
|
|
6
|
+
return `/**
|
|
7
|
+
* AUTO-GENERATED by grove build-data — do not edit by hand.
|
|
8
|
+
* Re-run \`grove build-data\` after changing curated.config.ts.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export type SiteConfig = {
|
|
12
|
+
name: string;
|
|
13
|
+
tagline: string;
|
|
14
|
+
description: string;
|
|
15
|
+
siteUrl: string;
|
|
16
|
+
repoUrl: string;
|
|
17
|
+
itemLabel: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const siteConfig: SiteConfig = {
|
|
21
|
+
name: ${JSON.stringify(input.name)},
|
|
22
|
+
tagline: ${JSON.stringify(input.tagline)},
|
|
23
|
+
description: ${JSON.stringify(input.description)},
|
|
24
|
+
siteUrl: ${JSON.stringify(input.siteUrl)},
|
|
25
|
+
repoUrl: ${JSON.stringify(input.repoUrl)},
|
|
26
|
+
itemLabel: ${JSON.stringify(input.itemLabel)},
|
|
27
|
+
};
|
|
28
|
+
`;
|
|
29
|
+
}
|
|
30
|
+
export async function buildData(cwd = process.cwd(), config) {
|
|
31
|
+
const cfg = config ?? (await loadConfig(cwd));
|
|
32
|
+
const appsDir = resolve(cwd, cfg.paths.appsDir);
|
|
33
|
+
const outDir = resolve(cwd, cfg.paths.generatedDir);
|
|
34
|
+
await mkdir(outDir, { recursive: true });
|
|
35
|
+
let entries = [];
|
|
36
|
+
try {
|
|
37
|
+
entries = await readdir(appsDir);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
entries = [];
|
|
41
|
+
}
|
|
42
|
+
const files = entries.filter((f) => f.endsWith(".yml")).sort();
|
|
43
|
+
const apps = [];
|
|
44
|
+
const errors = [];
|
|
45
|
+
for (const file of files) {
|
|
46
|
+
const fileSlug = basename(file, ".yml");
|
|
47
|
+
try {
|
|
48
|
+
const text = await readFile(join(appsDir, file), "utf8");
|
|
49
|
+
const raw = parseAppYaml(text, fileSlug);
|
|
50
|
+
const app = normalizeAppRecord(raw, fileSlug);
|
|
51
|
+
if (app.slug !== fileSlug) {
|
|
52
|
+
app.slug = fileSlug;
|
|
53
|
+
}
|
|
54
|
+
apps.push(app);
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
errors.push(`${file}: ${err.message}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (errors.length > 0) {
|
|
61
|
+
const e = new Error(`build-data failed: ${errors.length} schema error(s)`);
|
|
62
|
+
e.details = errors;
|
|
63
|
+
throw e;
|
|
64
|
+
}
|
|
65
|
+
apps.sort((a, b) => {
|
|
66
|
+
const sa = a.stars ?? 0;
|
|
67
|
+
const sb = b.stars ?? 0;
|
|
68
|
+
if (sb !== sa)
|
|
69
|
+
return sb - sa;
|
|
70
|
+
return (a.name ?? "").localeCompare(b.name ?? "");
|
|
71
|
+
});
|
|
72
|
+
const indexApps = apps
|
|
73
|
+
.map((app) => toIndexApp(app))
|
|
74
|
+
.filter((a) => a.visibility !== "hide");
|
|
75
|
+
const generatedAt = new Date().toISOString();
|
|
76
|
+
const fullPayload = {
|
|
77
|
+
schemaVersion: 1,
|
|
78
|
+
generatedAt,
|
|
79
|
+
totalApps: apps.length,
|
|
80
|
+
visibleApps: indexApps.length,
|
|
81
|
+
apps: apps,
|
|
82
|
+
};
|
|
83
|
+
const indexPayload = {
|
|
84
|
+
schemaVersion: 1,
|
|
85
|
+
generatedAt,
|
|
86
|
+
totalApps: indexApps.length,
|
|
87
|
+
apps: indexApps,
|
|
88
|
+
};
|
|
89
|
+
const fullPath = join(outDir, "apps.full.json");
|
|
90
|
+
const indexPath = join(outDir, "apps.index.json");
|
|
91
|
+
const aliasPath = join(outDir, "apps.json");
|
|
92
|
+
await writeFile(fullPath, JSON.stringify(fullPayload, null, 2), "utf8");
|
|
93
|
+
await writeFile(indexPath, JSON.stringify(indexPayload, null, 2), "utf8");
|
|
94
|
+
await writeFile(aliasPath, JSON.stringify(fullPayload, null, 2), "utf8");
|
|
95
|
+
const configTsPath = resolve(cwd, "src/data/config.ts");
|
|
96
|
+
const configTs = buildSiteConfigTs({
|
|
97
|
+
name: cfg.name,
|
|
98
|
+
tagline: cfg.tagline,
|
|
99
|
+
description: cfg.description ?? cfg.tagline,
|
|
100
|
+
siteUrl: cfg.siteUrl ?? "https://example.com",
|
|
101
|
+
repoUrl: cfg.repoUrl ?? "",
|
|
102
|
+
itemLabel: cfg.itemLabel,
|
|
103
|
+
});
|
|
104
|
+
await writeFile(configTsPath, configTs, "utf8");
|
|
105
|
+
return {
|
|
106
|
+
totalApps: apps.length,
|
|
107
|
+
visibleApps: indexApps.length,
|
|
108
|
+
fullPath,
|
|
109
|
+
indexPath,
|
|
110
|
+
aliasPath,
|
|
111
|
+
configTsPath,
|
|
112
|
+
errors,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=build-data.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-data.js","sourceRoot":"","sources":["../src/build-data.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACvE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpD,OAAO,EAGL,kBAAkB,EAClB,YAAY,EACZ,UAAU,GACX,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAsCzC,MAAM,UAAU,iBAAiB,CAAC,KAAwB;IACxD,OAAO;;;;;;;;;;;;;;;UAeC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC;aACvB,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,OAAO,CAAC;iBACzB,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,WAAW,CAAC;aACrC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,OAAO,CAAC;aAC7B,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,OAAO,CAAC;eAC3B,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,SAAS,CAAC;;CAE7C,CAAC;AACF,CAAC;AAqBD,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE,EACnB,MAAsB;IAEtB,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,MAAM,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;IAC9C,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAChD,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;IACpD,MAAM,KAAK,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAEzC,IAAI,OAAO,GAAa,EAAE,CAAC;IAC3B,IAAI,CAAC;QACH,OAAO,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IACnC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,GAAG,EAAE,CAAC;IACf,CAAC;IACD,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAE/D,MAAM,IAAI,GAAgB,EAAE,CAAC;IAC7B,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,QAAQ,GAAG,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACxC,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,CAAC;YACzD,MAAM,GAAG,GAAG,YAAY,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;YACzC,MAAM,GAAG,GAAG,kBAAkB,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;YAC9C,IAAI,GAAG,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;gBAC1B,GAAG,CAAC,IAAI,GAAG,QAAQ,CAAC;YACtB,CAAC;YACD,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACjB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,CAAC,IAAI,CAAC,GAAG,IAAI,KAAM,GAAa,CAAC,OAAO,EAAE,CAAC,CAAC;QACpD,CAAC;IACH,CAAC;IACD,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACtB,MAAM,CAAC,GAAG,IAAI,KAAK,CAAC,sBAAsB,MAAM,CAAC,MAAM,kBAAkB,CAAC,CAAC;QAC1E,CAAoC,CAAC,OAAO,GAAG,MAAM,CAAC;QACvD,MAAM,CAAC,CAAC;IACV,CAAC;IAED,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QACjB,MAAM,EAAE,GAAG,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC;QACxB,MAAM,EAAE,GAAG,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC;QACxB,IAAI,EAAE,KAAK,EAAE;YAAE,OAAO,EAAE,GAAG,EAAE,CAAC;QAC9B,OAAO,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;IACpD,CAAC,CAAC,CAAC;IAEH,MAAM,SAAS,GAAG,IAAI;SACnB,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;SAC7B,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,KAAK,MAAM,CAAC,CAAC;IAE1C,MAAM,WAAW,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC7C,MAAM,WAAW,GAAoB;QACnC,aAAa,EAAE,CAAC;QAChB,WAAW;QACX,SAAS,EAAE,IAAI,CAAC,MAAM;QACtB,WAAW,EAAE,SAAS,CAAC,MAAM;QAC7B,IAAI,EAAE,IAAiD;KACxD,CAAC;IACF,MAAM,YAAY,GAAqB;QACrC,aAAa,EAAE,CAAC;QAChB,WAAW;QACX,SAAS,EAAE,SAAS,CAAC,MAAM;QAC3B,IAAI,EAAE,SAAsD;KAC7D,CAAC;IAEF,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAChD,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;IAClD,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IAC5C,MAAM,SAAS,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;IACxE,MAAM,SAAS,CAAC,SAAS,EAAE,IAAI,CAAC,SAAS,CAAC,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;IAC1E,MAAM,SAAS,CAAC,SAAS,EAAE,IAAI,CAAC,SAAS,CAAC,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;IAEzE,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,EAAE,oBAAoB,CAAC,CAAC;IACxD,MAAM,QAAQ,GAAG,iBAAiB,CAAC;QACjC,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,OAAO,EAAE,GAAG,CAAC,OAAO;QACpB,WAAW,EAAE,GAAG,CAAC,WAAW,IAAI,GAAG,CAAC,OAAO;QAC3C,OAAO,EAAE,GAAG,CAAC,OAAO,IAAI,qBAAqB;QAC7C,OAAO,EAAE,GAAG,CAAC,OAAO,IAAI,EAAE;QAC1B,SAAS,EAAE,GAAG,CAAC,SAAS;KACzB,CAAC,CAAC;IACH,MAAM,SAAS,CAAC,YAAY,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;IAEhD,OAAO;QACL,SAAS,EAAE,IAAI,CAAC,MAAM;QACtB,WAAW,EAAE,SAAS,CAAC,MAAM;QAC7B,QAAQ;QACR,SAAS;QACT,SAAS;QACT,YAAY;QACZ,MAAM;KACP,CAAC;AACJ,CAAC"}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type CuratedConfig } from "./schema.js";
|
|
2
|
+
export type { CuratedConfig };
|
|
3
|
+
export declare function loadConfig(cwd?: string, configPath?: string): Promise<CuratedConfig>;
|
|
4
|
+
export declare function defineConfig(config: CuratedConfig): CuratedConfig;
|
|
5
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,OAAO,EAAuB,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAEtE,YAAY,EAAE,aAAa,EAAE,CAAC;AAE9B,wBAAsB,UAAU,CAAC,GAAG,SAAgB,EAAE,UAAU,SAAsB,GAAG,OAAO,CAAC,aAAa,CAAC,CAM9G;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,aAAa,GAAG,aAAa,CAEjE"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { createJiti } from "jiti";
|
|
3
|
+
import { curatedConfigSchema } from "./schema.js";
|
|
4
|
+
export async function loadConfig(cwd = process.cwd(), configPath = "curated.config.ts") {
|
|
5
|
+
const resolved = resolve(cwd, configPath);
|
|
6
|
+
const jiti = createJiti(import.meta.url);
|
|
7
|
+
const mod = await jiti.import(resolved, { default: true });
|
|
8
|
+
const raw = mod.default ?? mod.config ?? mod;
|
|
9
|
+
return curatedConfigSchema.parse(raw);
|
|
10
|
+
}
|
|
11
|
+
export function defineConfig(config) {
|
|
12
|
+
return curatedConfigSchema.parse(config);
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;AAClC,OAAO,EAAE,mBAAmB,EAAsB,MAAM,aAAa,CAAC;AAItE,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE,EAAE,UAAU,GAAG,mBAAmB;IACpF,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC;IAC1C,MAAM,IAAI,GAAG,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACzC,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAA4B,CAAC;IACtF,MAAM,GAAG,GAAG,GAAG,CAAC,OAAO,IAAI,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC;IAC7C,OAAO,mBAAmB,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;AACxC,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,MAAqB;IAChD,OAAO,mBAAmB,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;AAC3C,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type AppRecord } from "./schema.js";
|
|
2
|
+
import { type CuratedConfig } from "./config.js";
|
|
3
|
+
export interface ReviewCandidate {
|
|
4
|
+
slug: string;
|
|
5
|
+
name: string;
|
|
6
|
+
repoUrl: string;
|
|
7
|
+
status: string;
|
|
8
|
+
tier: string;
|
|
9
|
+
staleReason: string | null;
|
|
10
|
+
lastCommitAt: string | null;
|
|
11
|
+
stars: number;
|
|
12
|
+
}
|
|
13
|
+
export interface ReviewReport {
|
|
14
|
+
generatedAt: string;
|
|
15
|
+
totalCandidates: number;
|
|
16
|
+
candidates: ReviewCandidate[];
|
|
17
|
+
}
|
|
18
|
+
/** Compute the set of review candidates from normalized app records. */
|
|
19
|
+
export declare function pickReviewCandidates(apps: AppRecord[]): AppRecord[];
|
|
20
|
+
/**
|
|
21
|
+
* Read every apps/*.yml, normalize, then write data/generated/review-report.json
|
|
22
|
+
* with cleanup candidates (unknown / needs_review / cleanupCandidate).
|
|
23
|
+
*/
|
|
24
|
+
export declare function buildReviewReport(cwd?: string, config?: CuratedConfig): Promise<{
|
|
25
|
+
report: ReviewReport;
|
|
26
|
+
path: string;
|
|
27
|
+
}>;
|
|
28
|
+
//# sourceMappingURL=decisions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decisions.d.ts","sourceRoot":"","sources":["../src/decisions.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,SAAS,EAAoC,MAAM,aAAa,CAAC;AAC/E,OAAO,EAAE,KAAK,aAAa,EAAc,MAAM,aAAa,CAAC;AAE7D,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,YAAY;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,eAAe,EAAE,CAAC;CAC/B;AAeD,wEAAwE;AACxE,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,SAAS,EAAE,GAAG,SAAS,EAAE,CAOnE;AAED;;;GAGG;AACH,wBAAsB,iBAAiB,CACrC,GAAG,SAAgB,EACnB,MAAM,CAAC,EAAE,aAAa,GACrB,OAAO,CAAC;IAAE,MAAM,EAAE,YAAY,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CA6BjD"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { basename, join, resolve } from "node:path";
|
|
3
|
+
import { normalizeAppRecord, parseAppYaml } from "./schema.js";
|
|
4
|
+
import { loadConfig } from "./config.js";
|
|
5
|
+
function toCandidate(app) {
|
|
6
|
+
return {
|
|
7
|
+
slug: app.slug,
|
|
8
|
+
name: app.name,
|
|
9
|
+
repoUrl: app.repoUrl,
|
|
10
|
+
status: app.status,
|
|
11
|
+
tier: app.tier,
|
|
12
|
+
staleReason: app.staleReason,
|
|
13
|
+
lastCommitAt: app.lastCommitAt ?? null,
|
|
14
|
+
stars: app.stars ?? 0,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/** Compute the set of review candidates from normalized app records. */
|
|
18
|
+
export function pickReviewCandidates(apps) {
|
|
19
|
+
return apps.filter((app) => app.cleanupCandidate ||
|
|
20
|
+
app.status === "unknown" ||
|
|
21
|
+
app.status === "needs_review");
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Read every apps/*.yml, normalize, then write data/generated/review-report.json
|
|
25
|
+
* with cleanup candidates (unknown / needs_review / cleanupCandidate).
|
|
26
|
+
*/
|
|
27
|
+
export async function buildReviewReport(cwd = process.cwd(), config) {
|
|
28
|
+
const cfg = config ?? (await loadConfig(cwd));
|
|
29
|
+
const appsDir = resolve(cwd, cfg.paths.appsDir);
|
|
30
|
+
const outDir = resolve(cwd, cfg.paths.generatedDir);
|
|
31
|
+
await mkdir(outDir, { recursive: true });
|
|
32
|
+
let entries = [];
|
|
33
|
+
try {
|
|
34
|
+
entries = await readdir(appsDir);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
entries = [];
|
|
38
|
+
}
|
|
39
|
+
const files = entries.filter((f) => f.endsWith(".yml")).sort();
|
|
40
|
+
const apps = [];
|
|
41
|
+
for (const file of files) {
|
|
42
|
+
const fileSlug = basename(file, ".yml");
|
|
43
|
+
const text = await readFile(join(appsDir, file), "utf8");
|
|
44
|
+
const raw = parseAppYaml(text, fileSlug);
|
|
45
|
+
apps.push(normalizeAppRecord(raw, fileSlug));
|
|
46
|
+
}
|
|
47
|
+
const candidates = pickReviewCandidates(apps).map(toCandidate);
|
|
48
|
+
const report = {
|
|
49
|
+
generatedAt: new Date().toISOString(),
|
|
50
|
+
totalCandidates: candidates.length,
|
|
51
|
+
candidates,
|
|
52
|
+
};
|
|
53
|
+
const path = join(outDir, "review-report.json");
|
|
54
|
+
await writeFile(path, JSON.stringify(report, null, 2), "utf8");
|
|
55
|
+
return { report, path };
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=decisions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decisions.js","sourceRoot":"","sources":["../src/decisions.ts"],"names":[],"mappings":"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"}
|
package/dist/enrich.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enrich GitHub repository metadata from public HTML + shields.io.
|
|
3
|
+
*
|
|
4
|
+
* Used as a token-free fallback when GitHub REST quota is exhausted.
|
|
5
|
+
* Scrapes the public github.com HTML page for license, language,
|
|
6
|
+
* topics, and homepage. Falls back to shields.io for license when
|
|
7
|
+
* the page render doesn't include the standard "X license" link text.
|
|
8
|
+
*
|
|
9
|
+
* The returned shape is compatible with the GitHub REST `repository`
|
|
10
|
+
* block used in `data/apps/<slug>.yml`, so a subsequent token-backed
|
|
11
|
+
* sync can fill in the rest of the fields.
|
|
12
|
+
*/
|
|
13
|
+
export interface EnrichedFields {
|
|
14
|
+
license: string | null;
|
|
15
|
+
language: string | null;
|
|
16
|
+
topics: string[];
|
|
17
|
+
homepage: string | null;
|
|
18
|
+
}
|
|
19
|
+
export interface EnrichResult {
|
|
20
|
+
fields: EnrichedFields;
|
|
21
|
+
notFound?: boolean;
|
|
22
|
+
rateLimited?: boolean;
|
|
23
|
+
error?: string;
|
|
24
|
+
}
|
|
25
|
+
export declare function enrichFromGithubHtml(repoUrl: string): Promise<EnrichResult>;
|
|
26
|
+
//# sourceMappingURL=enrich.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"enrich.d.ts","sourceRoot":"","sources":["../src/enrich.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AA+ED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,cAAc,CAAC;IACvB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAqCjF"}
|
package/dist/enrich.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enrich GitHub repository metadata from public HTML + shields.io.
|
|
3
|
+
*
|
|
4
|
+
* Used as a token-free fallback when GitHub REST quota is exhausted.
|
|
5
|
+
* Scrapes the public github.com HTML page for license, language,
|
|
6
|
+
* topics, and homepage. Falls back to shields.io for license when
|
|
7
|
+
* the page render doesn't include the standard "X license" link text.
|
|
8
|
+
*
|
|
9
|
+
* The returned shape is compatible with the GitHub REST `repository`
|
|
10
|
+
* block used in `data/apps/<slug>.yml`, so a subsequent token-backed
|
|
11
|
+
* sync can fill in the rest of the fields.
|
|
12
|
+
*/
|
|
13
|
+
import { getOwnerRepoFromUrl } from "./schema.js";
|
|
14
|
+
const HEADERS = {
|
|
15
|
+
Accept: "text/html,application/xhtml+xml",
|
|
16
|
+
"User-Agent": "grove-enricher",
|
|
17
|
+
};
|
|
18
|
+
async function fetchHtml(owner, repo) {
|
|
19
|
+
try {
|
|
20
|
+
const res = await fetch(`https://github.com/${owner}/${repo}`, { headers: HEADERS });
|
|
21
|
+
if (res.status === 404)
|
|
22
|
+
return { notFound: true };
|
|
23
|
+
if (res.status === 429)
|
|
24
|
+
return { rateLimited: true };
|
|
25
|
+
if (!res.ok)
|
|
26
|
+
return { error: `${res.status} ${res.statusText}` };
|
|
27
|
+
return { html: await res.text() };
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
return { error: err.message };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function extractLicense(html) {
|
|
34
|
+
const m = html.match(/>([A-Z][A-Z0-9.+-]*-\d(?:\.\d)?)\s+license/i) ||
|
|
35
|
+
html.match(/>(MIT|Apache-2\.0|BSD-2-Clause|BSD-3-Clause|GPL-2\.0|GPL-3\.0|AGPL-3\.0|MPL-2\.0|ISC|Unlicense|0BSD)\s+license/i);
|
|
36
|
+
return m && m[1] ? m[1] : null;
|
|
37
|
+
}
|
|
38
|
+
async function fetchLicenseViaShields(owner, repo) {
|
|
39
|
+
try {
|
|
40
|
+
const res = await fetch(`https://img.shields.io/github/license/${owner}/${repo}`, {
|
|
41
|
+
headers: HEADERS,
|
|
42
|
+
});
|
|
43
|
+
if (!res.ok)
|
|
44
|
+
return null;
|
|
45
|
+
const svg = await res.text();
|
|
46
|
+
const m = svg.match(/<title>([^<]+)<\/title>/);
|
|
47
|
+
if (!m || !m[1])
|
|
48
|
+
return null;
|
|
49
|
+
const id = m[1].trim().replace(/^license:\s*/i, "").trim();
|
|
50
|
+
if (!id ||
|
|
51
|
+
id === "not specified" ||
|
|
52
|
+
id === "not identifiable by github" ||
|
|
53
|
+
id === "unknown" ||
|
|
54
|
+
id.toLowerCase().includes("business source")) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
return id;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function extractLanguage(html) {
|
|
64
|
+
const m = html.match(/class="color-fg-default text-bold mr-1">([^<]+)<\/span>/);
|
|
65
|
+
return m && m[1] ? m[1].trim() : null;
|
|
66
|
+
}
|
|
67
|
+
function extractTopics(html) {
|
|
68
|
+
const matches = html.matchAll(/<a href="\/topics\/[^"]+"[^>]*>([^<]+)<\/a>/g);
|
|
69
|
+
const out = [];
|
|
70
|
+
for (const m of matches) {
|
|
71
|
+
if (m[1]) {
|
|
72
|
+
const t = m[1].trim();
|
|
73
|
+
if (t)
|
|
74
|
+
out.push(t);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return [...new Set(out)];
|
|
78
|
+
}
|
|
79
|
+
function extractHomepage(html) {
|
|
80
|
+
const m = html.match(/<a [^>]*href="(https?:\/\/[^"]+)"[^>]*rel="nofollow"[^>]*>\s*Homepage\s*<\/a>/);
|
|
81
|
+
return m && m[1] ? m[1] : null;
|
|
82
|
+
}
|
|
83
|
+
export async function enrichFromGithubHtml(repoUrl) {
|
|
84
|
+
const ref = getOwnerRepoFromUrl(repoUrl);
|
|
85
|
+
if (!ref) {
|
|
86
|
+
return {
|
|
87
|
+
fields: { license: null, language: null, topics: [], homepage: null },
|
|
88
|
+
error: "invalid_repo_url",
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const res = await fetchHtml(ref.owner, ref.repo);
|
|
92
|
+
if ("notFound" in res) {
|
|
93
|
+
return {
|
|
94
|
+
fields: { license: null, language: null, topics: [], homepage: null },
|
|
95
|
+
notFound: true,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
if ("rateLimited" in res) {
|
|
99
|
+
return {
|
|
100
|
+
fields: { license: null, language: null, topics: [], homepage: null },
|
|
101
|
+
rateLimited: true,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if ("error" in res) {
|
|
105
|
+
return {
|
|
106
|
+
fields: { license: null, language: null, topics: [], homepage: null },
|
|
107
|
+
error: res.error,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
const htmlLicense = extractLicense(res.html);
|
|
111
|
+
const license = htmlLicense ?? (await fetchLicenseViaShields(ref.owner, ref.repo));
|
|
112
|
+
return {
|
|
113
|
+
fields: {
|
|
114
|
+
license,
|
|
115
|
+
language: extractLanguage(res.html),
|
|
116
|
+
topics: extractTopics(res.html),
|
|
117
|
+
homepage: extractHomepage(res.html),
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=enrich.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"enrich.js","sourceRoot":"","sources":["../src/enrich.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared GitHub API helpers used by the grove CLI.
|
|
3
|
+
*
|
|
4
|
+
* - ghFetch: fetch + JSON parse + 2-attempt exponential backoff
|
|
5
|
+
* - rateLimitWaitMs: read X-RateLimit-Reset header and return ms to wait
|
|
6
|
+
* - sleep: small helper
|
|
7
|
+
* - pLimit: tiny concurrency cap (no external deps)
|
|
8
|
+
*
|
|
9
|
+
* These exist so a single transient 5xx doesn't fail an entire daily
|
|
10
|
+
* refresh, and so we can run the per-app loop with bounded concurrency
|
|
11
|
+
* instead of one round-trip at a time.
|
|
12
|
+
*/
|
|
13
|
+
export interface GhFetchOptions {
|
|
14
|
+
token?: string;
|
|
15
|
+
userAgent?: string;
|
|
16
|
+
attempts?: number;
|
|
17
|
+
onRateLimited?: () => number;
|
|
18
|
+
}
|
|
19
|
+
export declare function rateLimitWaitMs(res: Response, capMs?: number): number;
|
|
20
|
+
export declare function sleep(ms: number): Promise<void>;
|
|
21
|
+
export declare function ghFetch(path: string, options?: GhFetchOptions): Promise<Response>;
|
|
22
|
+
/**
|
|
23
|
+
* Tiny p-limit implementation: a promise-pool that caps how many
|
|
24
|
+
* `fn(item)` calls run at once. Await the returned promise to get the
|
|
25
|
+
* full results array in input order.
|
|
26
|
+
*/
|
|
27
|
+
export declare function pLimit<T, U>(concurrency: number, items: T[], fn: (item: T, index: number) => Promise<U>): Promise<U[]>;
|
|
28
|
+
//# sourceMappingURL=github-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"github-client.d.ts","sourceRoot":"","sources":["../src/github-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAQH,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,MAAM,CAAC;CAC9B;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,QAAQ,EAAE,KAAK,SAAa,GAAG,MAAM,CAMzE;AAED,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAG/C;AAED,wBAAsB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,QAAQ,CAAC,CAuC3F;AAED;;;;GAIG;AACH,wBAAsB,MAAM,CAAC,CAAC,EAAE,CAAC,EAC/B,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,CAAC,EAAE,EACV,EAAE,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,GACzC,OAAO,CAAC,CAAC,EAAE,CAAC,CAad"}
|