ghost-github-portfolio 0.3.5 → 0.4.0
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 +116 -118
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +91 -63
- package/dist/config.js.map +1 -1
- package/dist/generator.d.ts +36 -2
- package/dist/generator.d.ts.map +1 -1
- package/dist/generator.js +181 -180
- package/dist/generator.js.map +1 -1
- package/dist/ghost.d.ts +8 -0
- package/dist/ghost.d.ts.map +1 -1
- package/dist/ghost.js +35 -19
- package/dist/ghost.js.map +1 -1
- package/dist/github.d.ts +26 -7
- package/dist/github.d.ts.map +1 -1
- package/dist/github.js +91 -63
- package/dist/github.js.map +1 -1
- package/dist/http.d.ts +9 -0
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +47 -1
- package/dist/http.js.map +1 -1
- package/dist/index.js +55 -56
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +15 -25
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -5
package/dist/github.js
CHANGED
|
@@ -9,9 +9,9 @@ function headers(token) {
|
|
|
9
9
|
h.Authorization = `Bearer ${token}`;
|
|
10
10
|
return h;
|
|
11
11
|
}
|
|
12
|
-
|
|
12
|
+
/** Fetch every public repo the user owns, unfiltered. */
|
|
13
|
+
export async function fetchAllRepos(config, verbose = false) {
|
|
13
14
|
const { username, token } = config.github;
|
|
14
|
-
const { minStars, maxRepos, excludeRepos, includeForked } = config.portfolio;
|
|
15
15
|
const allRepos = [];
|
|
16
16
|
let page = 1;
|
|
17
17
|
const perPage = 100;
|
|
@@ -41,97 +41,125 @@ export async function fetchRepos(config, verbose = false) {
|
|
|
41
41
|
break;
|
|
42
42
|
page++;
|
|
43
43
|
}
|
|
44
|
+
return allRepos;
|
|
45
|
+
}
|
|
46
|
+
export function isAwesomeList(repo) {
|
|
47
|
+
return (repo.name.toLowerCase().startsWith("awesome") ||
|
|
48
|
+
repo.topics.includes("awesome-list"));
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Apply every filter, sort by stars and only then cut to maxRepos, so an
|
|
52
|
+
* excluded repo never takes a slot.
|
|
53
|
+
*/
|
|
54
|
+
export function selectRepos(repos, config) {
|
|
55
|
+
const { minStars, maxRepos, excludeRepos, includeForked, includeArchived, excludeAwesomeLists, } = config.portfolio;
|
|
44
56
|
const excludeSet = new Set(excludeRepos.map((r) => r.toLowerCase()));
|
|
45
|
-
|
|
57
|
+
const patterns = config.github.excludePatterns.map((p) => new RegExp(p, "i"));
|
|
58
|
+
return repos
|
|
46
59
|
.filter((r) => r.stargazers_count >= minStars)
|
|
47
60
|
.filter((r) => includeForked || !r.fork)
|
|
61
|
+
.filter((r) => includeArchived || !r.archived)
|
|
48
62
|
.filter((r) => !excludeSet.has(r.name.toLowerCase()))
|
|
49
63
|
.filter((r) => !config.portfolio.repos[r.name]?.exclude)
|
|
64
|
+
.filter((r) => !patterns.some((p) => p.test(r.name)))
|
|
65
|
+
.filter((r) => !excludeAwesomeLists || !isAwesomeList(r))
|
|
50
66
|
.sort((a, b) => b.stargazers_count - a.stargazers_count)
|
|
51
67
|
.slice(0, maxRepos);
|
|
52
68
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
];
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const { username } = config.github;
|
|
68
|
-
const branch = repo.default_branch;
|
|
69
|
-
// Config override for this specific repo
|
|
70
|
-
const overridePath = config.portfolio.repos[repo.name]?.bannerPath ??
|
|
71
|
-
config.portfolio.bannerPaths[repo.name];
|
|
72
|
-
if (overridePath) {
|
|
73
|
-
const url = `https://raw.githubusercontent.com/${username}/${repo.name}/${branch}/${overridePath}`;
|
|
74
|
-
if (await urlExists(url))
|
|
75
|
-
return url;
|
|
76
|
-
}
|
|
77
|
-
// Try default path
|
|
78
|
-
const defaultUrl = `https://raw.githubusercontent.com/${username}/${repo.name}/${branch}/${config.portfolio.defaultBannerPath}`;
|
|
79
|
-
if (await urlExists(defaultUrl))
|
|
80
|
-
return defaultUrl;
|
|
81
|
-
// Try candidates
|
|
82
|
-
for (const candidate of BANNER_CANDIDATES) {
|
|
83
|
-
if (candidate === config.portfolio.defaultBannerPath)
|
|
69
|
+
/**
|
|
70
|
+
* List every reference in an SVG that a browser would have to load from
|
|
71
|
+
* somewhere else. Inside an `<img>` those are never loaded, so the banner
|
|
72
|
+
* renders blank or broken. `#id` and `data:` references are fine.
|
|
73
|
+
*/
|
|
74
|
+
export function findExternalRefs(svg) {
|
|
75
|
+
const refs = [];
|
|
76
|
+
const attr = /\b(?:xlink:)?href\s*=\s*(?:"([^"]*)"|'([^']*)')/gi;
|
|
77
|
+
const cssUrl = /url\(\s*(?:"([^"]*)"|'([^']*)'|([^)"'\s]*))\s*\)/gi;
|
|
78
|
+
const cssImport = /@import\s+(?:"([^"]*)"|'([^']*)')/gi;
|
|
79
|
+
for (const re of [attr, cssUrl, cssImport]) {
|
|
80
|
+
for (const m of svg.matchAll(re)) {
|
|
81
|
+
const value = (m[1] ?? m[2] ?? m[3] ?? "").trim();
|
|
82
|
+
if (value === "" || value.startsWith("#") || /^data:/i.test(value))
|
|
84
83
|
continue;
|
|
85
|
-
|
|
86
|
-
if (await urlExists(url))
|
|
87
|
-
return url;
|
|
84
|
+
refs.push(value);
|
|
88
85
|
}
|
|
89
|
-
return null;
|
|
90
86
|
}
|
|
91
|
-
|
|
92
|
-
|
|
87
|
+
return [...new Set(refs)];
|
|
88
|
+
}
|
|
89
|
+
function isSvg(path, contentType) {
|
|
90
|
+
return (path.toLowerCase().split("?")[0].endsWith(".svg") ||
|
|
91
|
+
(contentType ?? "").toLowerCase().includes("svg"));
|
|
92
|
+
}
|
|
93
|
+
async function checkBanner(url, path) {
|
|
94
|
+
let res;
|
|
95
|
+
try {
|
|
96
|
+
res = await fetchWithRetry(url, { redirect: "follow" });
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
100
|
+
return `${path}: request failed (${message})`;
|
|
101
|
+
}
|
|
102
|
+
if (res.status === 404)
|
|
103
|
+
return `${path}: missing (HTTP 404)`;
|
|
104
|
+
if (res.status !== 200)
|
|
105
|
+
return `${path}: HTTP ${res.status}`;
|
|
106
|
+
if (!isSvg(path, res.headers.get("content-type")))
|
|
93
107
|
return null;
|
|
108
|
+
const refs = findExternalRefs(await res.text());
|
|
109
|
+
if (refs.length > 0) {
|
|
110
|
+
const shown = refs.slice(0, 3).join(", ");
|
|
111
|
+
const more = refs.length > 3 ? ` and ${refs.length - 3} more` : "";
|
|
112
|
+
return `${path}: not self-contained, it loads ${shown}${more}`;
|
|
94
113
|
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Pick the banner for a repo: the per-repo path first (repos.<name>.bannerPath
|
|
118
|
+
* or bannerPaths.<name>), then defaultBannerPath. A banner is used only when
|
|
119
|
+
* it answers 200 and, for SVG, loads nothing from outside itself.
|
|
120
|
+
*/
|
|
121
|
+
export async function resolveBanner(repo, config) {
|
|
122
|
+
if (!config.portfolio.showBanner)
|
|
123
|
+
return { url: null, problems: [] };
|
|
124
|
+
const override = config.portfolio.repos[repo.name]?.bannerPath ??
|
|
125
|
+
config.portfolio.bannerPaths[repo.name];
|
|
126
|
+
const paths = [
|
|
127
|
+
...new Set([override, config.portfolio.defaultBannerPath].filter((p) => Boolean(p))),
|
|
128
|
+
];
|
|
129
|
+
const problems = [];
|
|
130
|
+
for (const path of paths) {
|
|
131
|
+
const clean = path.replace(/^\/+/, "");
|
|
132
|
+
const url = `https://raw.githubusercontent.com/${repo.full_name}/${repo.default_branch}/${clean}`;
|
|
133
|
+
const problem = await checkBanner(url, clean);
|
|
134
|
+
if (!problem)
|
|
135
|
+
return { url, problems: [] };
|
|
136
|
+
problems.push(problem);
|
|
137
|
+
}
|
|
138
|
+
return { url: null, problems };
|
|
95
139
|
}
|
|
96
140
|
const PORTFOLIO_CONFIG_FILE = ".ghost-portfolio.yml";
|
|
97
|
-
export async function fetchPortfolioConfig(repo
|
|
98
|
-
const { username } = config.github;
|
|
141
|
+
export async function fetchPortfolioConfig(repo) {
|
|
99
142
|
const branch = repo.default_branch;
|
|
100
|
-
const url = `https://raw.githubusercontent.com/${
|
|
143
|
+
const url = `https://raw.githubusercontent.com/${repo.full_name}/${branch}/${PORTFOLIO_CONFIG_FILE}`;
|
|
101
144
|
try {
|
|
102
145
|
const res = await fetchWithRetry(url, { redirect: "follow" });
|
|
103
146
|
if (!res.ok)
|
|
104
147
|
return null;
|
|
105
148
|
const text = await res.text();
|
|
106
|
-
// Dynamic import to avoid adding yaml as a runtime dep for github.ts
|
|
107
|
-
// yaml is already a project dependency
|
|
108
149
|
const { parse } = await import("yaml");
|
|
109
150
|
const data = parse(text);
|
|
110
151
|
if (!data || typeof data !== "object")
|
|
111
152
|
return null;
|
|
153
|
+
const pick = (v) => (typeof v === "string" ? v : undefined);
|
|
112
154
|
return {
|
|
113
|
-
description: data.description
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
techStack: data.techStack ?? undefined,
|
|
118
|
-
badges: Array.isArray(data.badges) ? data.badges : undefined,
|
|
119
|
-
bannerPath: data.bannerPath ?? undefined,
|
|
155
|
+
description: pick(data.description),
|
|
156
|
+
homepage: pick(data.homepage),
|
|
157
|
+
siteLabel: pick(data.siteLabel),
|
|
158
|
+
bannerPath: pick(data.bannerPath),
|
|
120
159
|
};
|
|
121
160
|
}
|
|
122
161
|
catch {
|
|
123
162
|
return null;
|
|
124
163
|
}
|
|
125
164
|
}
|
|
126
|
-
async function urlExists(url) {
|
|
127
|
-
const res = await fetchWithRetry(url, {
|
|
128
|
-
method: "HEAD",
|
|
129
|
-
redirect: "follow",
|
|
130
|
-
});
|
|
131
|
-
if (res.ok)
|
|
132
|
-
return true;
|
|
133
|
-
if (res.status === 404)
|
|
134
|
-
return false;
|
|
135
|
-
throw new Error(`Banner check failed for ${url}: HTTP ${res.status}`);
|
|
136
|
-
}
|
|
137
165
|
//# sourceMappingURL=github.js.map
|
package/dist/github.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"github.js","sourceRoot":"","sources":["../src/github.ts"],"names":[],"mappings":"AACA,OAAO,EACL,cAAc,EACd,qBAAqB,GAEtB,MAAM,WAAW,CAAC;AAEnB,MAAM,UAAU,GAAG,wBAAwB,CAAC;AAE5C,SAAS,OAAO,CAAC,KAAc;IAC7B,MAAM,CAAC,GAA2B;QAChC,MAAM,EAAE,6BAA6B;QACrC,YAAY,EAAE,wBAAwB;KACvC,CAAC;IACF,IAAI,KAAK;QAAE,CAAC,CAAC,aAAa,GAAG,UAAU,KAAK,EAAE,CAAC;IAC/C,OAAO,CAAC,CAAC;AACX,CAAC;
|
|
1
|
+
{"version":3,"file":"github.js","sourceRoot":"","sources":["../src/github.ts"],"names":[],"mappings":"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"}
|
package/dist/http.d.ts
CHANGED
|
@@ -2,8 +2,17 @@ interface RetryOptions {
|
|
|
2
2
|
maxRetries?: number;
|
|
3
3
|
baseDelay?: number;
|
|
4
4
|
onRetry?: (attempt: number, delay: number, reason: string) => void;
|
|
5
|
+
/** Request function to use instead of the global fetch. */
|
|
6
|
+
fetchImpl?: (url: string, init?: RequestInit) => Promise<Response>;
|
|
5
7
|
}
|
|
6
8
|
export declare function fetchWithRetry(url: string, init?: RequestInit, opts?: RetryOptions): Promise<Response>;
|
|
9
|
+
/**
|
|
10
|
+
* A minimal fetch over node:http and node:https that sends the headers it is
|
|
11
|
+
* given as they are, Host included. The global fetch drops a custom Host
|
|
12
|
+
* header, which the Ghost origin option depends on. It never follows
|
|
13
|
+
* redirects: a redirect from the origin would lead back to the public URL.
|
|
14
|
+
*/
|
|
15
|
+
export declare function rawRequest(url: string, init?: RequestInit): Promise<Response>;
|
|
7
16
|
export declare function parseRateLimitHeaders(res: Response): RateLimitInfo | null;
|
|
8
17
|
export interface RateLimitInfo {
|
|
9
18
|
limit: number;
|
package/dist/http.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAMA,UAAU,YAAY;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACnE,2DAA2D;IAC3D,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;CACpE;AAED,wBAAsB,cAAc,CAClC,GAAG,EAAE,MAAM,EACX,IAAI,CAAC,EAAE,WAAW,EAClB,IAAI,CAAC,EAAE,YAAY,GAClB,OAAO,CAAC,QAAQ,CAAC,CAoEnB;AAID;;;;;GAKG;AACH,wBAAgB,UAAU,CACxB,GAAG,EAAE,MAAM,EACX,IAAI,GAAE,WAAgB,GACrB,OAAO,CAAC,QAAQ,CAAC,CAuCnB;AAED,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,QAAQ,GAAG,aAAa,GAAG,IAAI,CAYzE;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,IAAI,CAAC;CACf"}
|
package/dist/http.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
|
+
import { request as httpRequest } from "node:http";
|
|
2
|
+
import { request as httpsRequest } from "node:https";
|
|
1
3
|
const MAX_RETRIES = 3;
|
|
2
4
|
const BASE_DELAY_MS = 1000;
|
|
3
5
|
export async function fetchWithRetry(url, init, opts) {
|
|
4
6
|
const maxRetries = opts?.maxRetries ?? MAX_RETRIES;
|
|
5
7
|
const baseDelay = opts?.baseDelay ?? BASE_DELAY_MS;
|
|
8
|
+
const doFetch = opts?.fetchImpl ?? ((u, i) => fetch(u, i));
|
|
6
9
|
let lastError;
|
|
7
10
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
8
11
|
let res;
|
|
9
12
|
try {
|
|
10
|
-
res = await
|
|
13
|
+
res = await doFetch(url, init);
|
|
11
14
|
}
|
|
12
15
|
catch (error) {
|
|
13
16
|
lastError = error;
|
|
@@ -46,6 +49,49 @@ export async function fetchWithRetry(url, init, opts) {
|
|
|
46
49
|
const message = lastError instanceof Error ? lastError.message : String(lastError);
|
|
47
50
|
throw new Error(`Request to ${url} failed after ${maxRetries + 1} attempts: ${message}`);
|
|
48
51
|
}
|
|
52
|
+
const NULL_BODY_STATUS = new Set([101, 204, 205, 304]);
|
|
53
|
+
/**
|
|
54
|
+
* A minimal fetch over node:http and node:https that sends the headers it is
|
|
55
|
+
* given as they are, Host included. The global fetch drops a custom Host
|
|
56
|
+
* header, which the Ghost origin option depends on. It never follows
|
|
57
|
+
* redirects: a redirect from the origin would lead back to the public URL.
|
|
58
|
+
*/
|
|
59
|
+
export function rawRequest(url, init = {}) {
|
|
60
|
+
return new Promise((resolve, reject) => {
|
|
61
|
+
const target = new URL(url);
|
|
62
|
+
const headers = { ...init.headers };
|
|
63
|
+
const hostHeader = Object.entries(headers).find(([k]) => k.toLowerCase() === "host")?.[1];
|
|
64
|
+
const isHttps = target.protocol === "https:";
|
|
65
|
+
const send = isHttps ? httpsRequest : httpRequest;
|
|
66
|
+
const req = send(target, {
|
|
67
|
+
method: init.method ?? "GET",
|
|
68
|
+
headers,
|
|
69
|
+
// Present the public name for TLS too, so the origin can pick its certificate.
|
|
70
|
+
...(isHttps && hostHeader ? { servername: hostHeader.replace(/:\d+$/, "") } : {}),
|
|
71
|
+
}, (res) => {
|
|
72
|
+
const chunks = [];
|
|
73
|
+
res.on("data", (c) => chunks.push(c));
|
|
74
|
+
res.on("error", reject);
|
|
75
|
+
res.on("end", () => {
|
|
76
|
+
const out = new Headers();
|
|
77
|
+
for (const [k, v] of Object.entries(res.headers)) {
|
|
78
|
+
if (v === undefined)
|
|
79
|
+
continue;
|
|
80
|
+
for (const item of Array.isArray(v) ? v : [v])
|
|
81
|
+
out.append(k, item);
|
|
82
|
+
}
|
|
83
|
+
const status = res.statusCode ?? 502;
|
|
84
|
+
const body = NULL_BODY_STATUS.has(status) ? null : Buffer.concat(chunks);
|
|
85
|
+
resolve(new Response(body, { status, headers: out }));
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
req.setTimeout(30_000, () => req.destroy(new Error("timed out after 30s")));
|
|
89
|
+
req.on("error", reject);
|
|
90
|
+
if (typeof init.body === "string")
|
|
91
|
+
req.write(init.body);
|
|
92
|
+
req.end();
|
|
93
|
+
});
|
|
94
|
+
}
|
|
49
95
|
export function parseRateLimitHeaders(res) {
|
|
50
96
|
const limit = res.headers.get("x-ratelimit-limit");
|
|
51
97
|
const remaining = res.headers.get("x-ratelimit-remaining");
|
package/dist/http.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http.js","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,GAAG,CAAC,CAAC;AACtB,MAAM,aAAa,GAAG,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"http.js","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -1,102 +1,101 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import { writeFileSync } from "node:fs";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
4
5
|
import { loadConfig, generateExampleConfig } from "./config.js";
|
|
5
|
-
import {
|
|
6
|
+
import { fetchAllRepos, selectRepos, resolveBanner, fetchPortfolioConfig, } from "./github.js";
|
|
6
7
|
import { fetchPage, updatePage } from "./ghost.js";
|
|
7
|
-
import { generateCard,
|
|
8
|
+
import { generateCard, generatePortfolioHtml, buildLexical, buildPreviewPage, wrapHtmlCard, } from "./generator.js";
|
|
9
|
+
const { version } = createRequire(import.meta.url)("../package.json");
|
|
8
10
|
const program = new Command();
|
|
9
11
|
program
|
|
10
12
|
.name("ghost-github-portfolio")
|
|
11
|
-
.description("
|
|
12
|
-
.version(
|
|
13
|
+
.description("Write your most starred GitHub repositories to a Ghost page as a grid of cards, through the Ghost Admin API.")
|
|
14
|
+
.version(version);
|
|
13
15
|
program
|
|
14
16
|
.command("sync")
|
|
15
17
|
.description("Sync GitHub repos to your Ghost portfolio page")
|
|
16
18
|
.requiredOption("-c, --config <path>", "Path to config YAML file")
|
|
17
|
-
.option("--dry-run", "
|
|
18
|
-
.option("--json", "
|
|
19
|
+
.option("--dry-run", "Build the page without updating Ghost")
|
|
20
|
+
.option("--json", "Print the lexical JSON (implies --dry-run)")
|
|
21
|
+
.option("--html-out <file>", "Write a standalone preview page of the card (implies --dry-run)")
|
|
19
22
|
.option("-v, --verbose", "Show detailed progress")
|
|
20
23
|
.action(async (opts) => {
|
|
21
24
|
try {
|
|
22
25
|
const config = loadConfig(opts.config);
|
|
23
26
|
const verbose = opts.verbose || false;
|
|
24
|
-
const dryRun = opts.dryRun || opts.json ||
|
|
25
|
-
// Fetch repos
|
|
27
|
+
const dryRun = opts.dryRun || opts.json || Boolean(opts.htmlOut);
|
|
26
28
|
if (verbose)
|
|
27
29
|
console.log(`Fetching repos for ${config.github.username} (min ${config.portfolio.minStars} stars)...`);
|
|
28
|
-
const allRepos = await
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
: allRepos;
|
|
33
|
-
console.log(`Found ${allRepos.length} repos matching criteria (${config.portfolio.minStars}+ stars)${displayRepos.length < allRepos.length ? `, displaying ${displayRepos.length}` : ""}`);
|
|
34
|
-
if (displayRepos.length === 0) {
|
|
30
|
+
const allRepos = await fetchAllRepos(config, verbose);
|
|
31
|
+
const repos = selectRepos(allRepos, config);
|
|
32
|
+
console.log(`Found ${allRepos.length} public repos, showing ${repos.length}`);
|
|
33
|
+
if (repos.length === 0) {
|
|
35
34
|
console.log("No repos found. Check your config.");
|
|
36
35
|
return;
|
|
37
36
|
}
|
|
38
|
-
//
|
|
37
|
+
// Per-repo .ghost-portfolio.yml: the file gives defaults, config.yml wins.
|
|
38
|
+
// Its description stands in for the GitHub one and is cleaned the same
|
|
39
|
+
// way; only a description in config.yml is used verbatim.
|
|
39
40
|
if (verbose)
|
|
40
|
-
console.log("
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
if (
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
|
|
41
|
+
console.log("Fetching portfolio configs...");
|
|
42
|
+
await Promise.all(repos.map(async (repo) => {
|
|
43
|
+
const portfolioConfig = await fetchPortfolioConfig(repo);
|
|
44
|
+
if (!portfolioConfig)
|
|
45
|
+
return;
|
|
46
|
+
const { description, ...fileOverrides } = portfolioConfig;
|
|
47
|
+
if (description)
|
|
48
|
+
repo.description = description;
|
|
49
|
+
const existing = config.portfolio.repos[repo.name] ?? {};
|
|
50
|
+
config.portfolio.repos[repo.name] = {
|
|
51
|
+
...Object.fromEntries(Object.entries(fileOverrides).filter(([, v]) => v !== undefined)),
|
|
52
|
+
...Object.fromEntries(Object.entries(existing).filter(([, v]) => v !== undefined)),
|
|
53
|
+
};
|
|
54
|
+
if (verbose)
|
|
55
|
+
console.log(` ${repo.name}: loaded .ghost-portfolio.yml`);
|
|
47
56
|
}));
|
|
48
|
-
// Fetch per-repo portfolio configs (.ghost-portfolio.yml)
|
|
49
57
|
if (verbose)
|
|
50
|
-
console.log("
|
|
51
|
-
await Promise.all(
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
config.portfolio.repos[repo.name] = {
|
|
57
|
-
...portfolioConfig,
|
|
58
|
-
...Object.fromEntries(Object.entries(existing).filter(([, v]) => v !== undefined)),
|
|
59
|
-
};
|
|
60
|
-
if (verbose)
|
|
61
|
-
console.log(` ${repo.name}: loaded .ghost-portfolio.yml`);
|
|
58
|
+
console.log("Checking banners...");
|
|
59
|
+
const banners = await Promise.all(repos.map((repo) => resolveBanner(repo, config)));
|
|
60
|
+
repos.forEach((repo, i) => {
|
|
61
|
+
const { problems } = banners[i];
|
|
62
|
+
if (problems.length > 0) {
|
|
63
|
+
console.warn(`Warning: ${repo.name} gets a name tile: ${problems.join("; ")}`);
|
|
62
64
|
}
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
const lexical = buildLexical(cards, footer);
|
|
65
|
+
});
|
|
66
|
+
const cards = repos.map((repo, i) => generateCard(repo, banners[i].url, config));
|
|
67
|
+
const html = generatePortfolioHtml({ cards, totalRepos: allRepos.length }, config);
|
|
68
|
+
const lexical = buildLexical(html);
|
|
68
69
|
if (opts.json) {
|
|
69
70
|
console.log(JSON.stringify(lexical, null, 2));
|
|
70
71
|
return;
|
|
71
72
|
}
|
|
72
73
|
if (dryRun) {
|
|
73
|
-
console.log("\n--- DRY RUN
|
|
74
|
-
|
|
75
|
-
console.log(`${repo.name} (${repo.stargazers_count} stars) ${
|
|
76
|
-
}
|
|
74
|
+
console.log("\n--- DRY RUN ---\n");
|
|
75
|
+
repos.forEach((repo, i) => {
|
|
76
|
+
console.log(`${repo.name} (${repo.stargazers_count} stars) ${banners[i].url ? "[banner]" : "[tile]"}`);
|
|
77
|
+
});
|
|
77
78
|
console.log(`\nTotal cards: ${cards.length}`);
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
79
|
+
const cardPath = "/tmp/ghost-portfolio-preview.html";
|
|
80
|
+
writeFileSync(cardPath, wrapHtmlCard(html));
|
|
81
|
+
console.log(`\nHtml card written to ${cardPath}`);
|
|
82
|
+
if (opts.htmlOut) {
|
|
83
|
+
writeFileSync(opts.htmlOut, buildPreviewPage(html));
|
|
84
|
+
console.log(`Preview page written to ${opts.htmlOut}`);
|
|
85
|
+
}
|
|
84
86
|
return;
|
|
85
87
|
}
|
|
86
|
-
// Fetch current page
|
|
87
88
|
if (verbose)
|
|
88
89
|
console.log("Fetching Ghost page...");
|
|
89
90
|
const page = await fetchPage(config);
|
|
90
91
|
if (verbose)
|
|
91
92
|
console.log(` Page: ${page.title} (${page.id})`);
|
|
92
|
-
// Update page
|
|
93
93
|
if (verbose)
|
|
94
94
|
console.log("Updating Ghost page...");
|
|
95
95
|
const updated = await updatePage(config, page.id, page.updated_at, lexical);
|
|
96
96
|
console.log(`Portfolio updated: ${updated.title}`);
|
|
97
|
-
console.log(` ${
|
|
98
|
-
console.log(` ${
|
|
99
|
-
console.log(` ${bannerResults.filter((b) => b.banner).length} banners loaded`);
|
|
97
|
+
console.log(` ${repos.length} projects displayed`);
|
|
98
|
+
console.log(` ${banners.filter((b) => b.url).length} banners, ${banners.filter((b) => !b.url).length} tiles`);
|
|
100
99
|
}
|
|
101
100
|
catch (err) {
|
|
102
101
|
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACxC,OAAO,EAAE,UAAU,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAChE,OAAO,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|