Monorepo for Aesthetic.Computer
aesthetic.computer
1#!/usr/bin/env node
2// ants/refresh-score.mjs
3// Refreshes the Front Door stats in SCORE.md with live data from /api/metrics
4// and local file counts. Runs once per day (skips if already refreshed today).
5//
6// Usage:
7// node ants/refresh-score.mjs # uses production API
8// node ants/refresh-score.mjs http://localhost:8888 # uses local dev server
9// node ants/refresh-score.mjs --force # bypass daily check
10//
11// Called from: .git/hooks/pre-commit (once per day on first commit)
12
13import { readFileSync, writeFileSync, existsSync, readdirSync } from "fs";
14import { join, dirname } from "path";
15import { fileURLToPath } from "url";
16
17const __dirname = dirname(fileURLToPath(import.meta.url));
18const ROOT = join(__dirname, "..");
19const SCORE_PATH = join(ROOT, "SCORE.md");
20const DISKS_DIR = join(ROOT, "system/public/aesthetic.computer/disks");
21const FUNCTIONS_DIR = join(ROOT, "system/netlify/functions");
22const MARKER = "/tmp/ac-score-refreshed";
23
24const args = process.argv.slice(2);
25const force = args.includes("--force");
26const apiArg = args.find((a) => a.startsWith("http"));
27const API_BASE = apiArg || "https://aesthetic.computer";
28
29// Once-per-day check
30const today = new Date().toISOString().slice(0, 10);
31if (!force && existsSync(MARKER)) {
32 const markerDate = readFileSync(MARKER, "utf8").trim();
33 if (markerDate === today) {
34 process.exit(0); // Already refreshed today
35 }
36}
37
38async function fetchMetrics() {
39 try {
40 const res = await fetch(`${API_BASE}/api/metrics?fresh=true`);
41 if (!res.ok) throw new Error(`HTTP ${res.status}`);
42 return await res.json();
43 } catch (err) {
44 process.stderr.write(`refresh-score: could not fetch metrics: ${err.message}\n`);
45 return null;
46 }
47}
48
49function countFiles(dir, ext) {
50 try {
51 return readdirSync(dir).filter((f) => f.endsWith(ext)).length;
52 } catch {
53 return 0;
54 }
55}
56
57function formatDate() {
58 return new Date().toLocaleDateString("en-US", {
59 year: "numeric",
60 month: "short",
61 day: "numeric",
62 });
63}
64
65async function main() {
66 const metrics = await fetchMetrics();
67 const mjsCount = countFiles(DISKS_DIR, ".mjs");
68 const lispCount = countFiles(DISKS_DIR, ".lisp");
69 const builtInPieces = mjsCount + lispCount;
70 const apiEndpoints = countFiles(FUNCTIONS_DIR, ".mjs");
71
72 const lines = [];
73 lines.push(
74 `${builtInPieces} built-in pieces (${mjsCount} JS + ${lispCount} KidLisp), ~${apiEndpoints} API endpoints.`
75 );
76
77 if (metrics) {
78 const parts = [];
79 if (metrics.handles) parts.push(`${metrics.handles} registered handles`);
80 if (metrics.pieces) parts.push(`${metrics.pieces} user-published pieces`);
81 if (metrics.paintings) parts.push(`${metrics.paintings} paintings`);
82 if (metrics.kidlisp) parts.push(`${metrics.kidlisp} KidLisp programs`);
83 if (metrics.chatMessages) parts.push(`${metrics.chatMessages} chat messages`);
84 if (metrics.printsOrdered) parts.push(`${metrics.printsOrdered} prints ordered`);
85 if (parts.length) lines.push(parts.join(", ") + ".");
86 }
87
88 lines.push(`*Last refreshed: ${formatDate()}*`);
89
90 const statsBlock = lines.join("<br>\n");
91
92 const score = readFileSync(SCORE_PATH, "utf8");
93
94 const startMarker = "<!-- stats:start -->";
95 const endMarker = "<!-- stats:end -->";
96
97 if (!score.includes(startMarker) || !score.includes(endMarker)) {
98 process.stderr.write(
99 `refresh-score: missing stats markers in SCORE.md\n`
100 );
101 process.exit(1);
102 }
103
104 const before = score.slice(
105 0,
106 score.indexOf(startMarker) + startMarker.length
107 );
108 const after = score.slice(score.indexOf(endMarker));
109 const updated = before + "\n" + statsBlock + "\n" + after;
110
111 if (updated !== score) {
112 writeFileSync(SCORE_PATH, updated);
113 process.stderr.write(`refresh-score: ✓ updated SCORE.md\n`);
114 } else {
115 process.stderr.write(`refresh-score: no changes needed\n`);
116 }
117
118 // Mark today as done
119 writeFileSync(MARKER, today);
120}
121
122main();