-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgenerate.ts
More file actions
99 lines (91 loc) · 5.62 KB
/
Copy pathgenerate.ts
File metadata and controls
99 lines (91 loc) · 5.62 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
// github-pet generator: builds dist/*.svg from live GitHub data.
// Zero-dependency TypeScript - runs on Bun (CI) or Node 24+ (type stripping).
import { mkdirSync, writeFileSync, readFileSync, existsSync } from "node:fs";
import { join } from "node:path";
import { fetchEvents, fetchRepos, fetchLanguages, fetchCiStatus, fetchProfile, fetchOpenIssues } from "./src/githubApi.ts";
import { fetchCalendar, fetchActivity } from "./src/graphApi.ts";
import { decide, applyForceState, greetingFor, ownerHour, currentStreak, isBirthday, lastPushedRepo, milestone, isWeekend, season, weeklyDigest } from "./src/state.ts";
import { buildSvg, PALETTES } from "./src/render.ts";
import { buildGraphSvg } from "./src/graphRender.ts";
import { buildIsoSvg } from "./src/isoRender.ts";
import { buildBadgeSvg } from "./src/badgeRender.ts";
import { langsChart } from "./src/charts.ts";
const USER = process.env.PET_USER || "prsdx";
const OUT_DIR = process.env.PET_OUTPUT_DIR || "dist";
const CONTACT = process.env.PET_CONTACT || "";
const ATTRIBUTION = !["0", "false", "no"].includes((process.env.PET_ATTRIBUTION ?? "").toLowerCase());
async function main(): Promise<void> {
const token = process.env.GITHUB_TOKEN || process.env.PET_GITHUB_TOKEN;
const [events, repos, profile] = await Promise.all([fetchEvents(USER), fetchRepos(USER), fetchProfile(USER)]);
const langs = await fetchLanguages(USER, repos);
const [calendar, activity] = await Promise.all([fetchCalendar(token, USER), fetchActivity(token, USER)]);
const ci = await fetchCiStatus(USER);
const openIssues = await fetchOpenIssues(USER);
const catName = process.env.PET_CAT_NAME || "";
const now = new Date();
let status = decide(events, {
lastPush: activity?.lastPush ?? null,
merged24h: activity?.merged24h ?? 0,
}, ci, now, catName, {
openIssues,
hibernateUntil: process.env.PET_HIBERNATE_UNTIL || "",
});
// preview-only override: PET_FORCE_STATE forces any state for
// screenshots/testing (validation list shared with the preview worker)
status = applyForceState(status, process.env.PET_FORCE_STATE || "");
// state.json delta memory: enables milestone reactions (stars/followers).
// read the previous run's snapshot (committed by the workflow), compare, rewrite.
const statePath = join(OUT_DIR, "state.json");
let prev: { stars?: number; followers?: number } = {};
try {
if (existsSync(statePath)) prev = JSON.parse(readFileSync(statePath, "utf-8"));
} catch { /* corrupt state file is not worth dying over */ }
const stars = repos.reduce((s, r) => s + (r.stargazers_count ?? 0), 0);
const followers = profile?.followers ?? 0;
const starMilestone = milestone(prev.stars, stars);
const followerMilestone = milestone(prev.followers, followers);
const streak = currentStreak(calendar?.days ?? null);
const week = weeklyDigest(events, now);
const labelExtra: string[] = [];
if (week.pushes || week.merged) labelExtra.push(`${week.pushes} pushes · ${week.merged} prs this week`);
if (calendar) labelExtra.push(`${calendar.total} contributions this year`);
if (starMilestone) labelExtra.push(`just hit ${starMilestone} stars!!`);
if (followerMilestone) labelExtra.push(`just hit ${followerMilestone} followers!!`);
// scene signals - all derived from real data
const scene = {
hackingOn: lastPushedRepo(events) ?? "",
streakDays: streak,
birthday: isBirthday(profile?.createdAt, now),
hour: ownerHour(now),
accent: process.env.PET_ACCENT || "",
weekend: isWeekend(now),
topLang: Object.entries(langs).sort((a, b) => b[1] - a[1])[0]?.[0] ?? "",
seasonal: season(now),
touchGrass: streak >= 21,
confetti: starMilestone !== null || followerMilestone !== null,
labelExtra,
};
mkdirSync(OUT_DIR, { recursive: true });
const name = process.env.PET_NAME || profile?.name?.split(" ")[0]?.toLowerCase() || "";
const greeting = greetingFor(now, name);
const outputs: Record<string, string> = {
"pet.svg": buildSvg(status.state, status.caption, "dark", greeting, CONTACT, ATTRIBUTION, scene),
"pet-light.svg": buildSvg(status.state, status.caption, "light", greeting, CONTACT, ATTRIBUTION, scene),
"graph.svg": buildGraphSvg(status.state, status.caption, calendar, "dark", ATTRIBUTION),
"graph-light.svg": buildGraphSvg(status.state, status.caption, calendar, "light", ATTRIBUTION),
"isocat.svg": buildIsoSvg(status.state, status.caption, calendar, "dark", ATTRIBUTION),
"isocat-light.svg": buildIsoSvg(status.state, status.caption, calendar, "light", ATTRIBUTION),
"langs.svg": langsChart(langs, repos.length, PALETTES.dark),
"langs-light.svg": langsChart(langs, repos.length, PALETTES.light),
"pet-badge.svg": buildBadgeSvg(status.state, status.caption),
};
for (const [name, svg] of Object.entries(outputs)) {
writeFileSync(join(OUT_DIR, name), svg, "utf-8");
console.log(`wrote ${OUT_DIR}/${name} (${svg.length} bytes)`);
}
writeFileSync(statePath, JSON.stringify({ stars, followers, updatedAt: now.toISOString() }, null, 2) + "\n", "utf-8");
console.log(`wrote ${statePath} (stars=${stars} followers=${followers})`);
console.log(`state=${status.state} | caption='${status.caption}' | apiOk=${status.apiOk} | ci_failed=${ci.failed} | openIssues=${openIssues} | streak=${streak}`);
console.log(`calendar=${calendar ? calendar.total + " contributions" : "unavailable"} | lastPush=${activity?.lastPush ?? "?"} | events=${events.length} repos=${repos.length} langs=${Object.keys(langs).length}`);
}
main();