Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

Point version API at Tangled

+135 -39
+135 -39
system/netlify/functions/version.mjs
··· 1 - // Returns deployed commit hash vs latest GitHub commit 1 + // Returns deployed commit hash vs latest Tangled origin commit. 2 2 // Used by prompt.mjs to show version status on curtain UI 3 3 4 4 import fs from "fs"; 5 5 import path from "path"; 6 + import { execFile } from "child_process"; 7 + import { promisify } from "util"; 8 + 9 + const execFileAsync = promisify(execFile); 10 + const GIT_REMOTE_OVERRIDE = process.env.VERSION_GIT_REMOTE || ""; 11 + const GIT_BRANCH = process.env.VERSION_GIT_BRANCH || "main"; 12 + const RECENT_COMMIT_COUNT = 10; 13 + const HISTORY_SCAN_LIMIT = 50; 6 14 7 15 // Get deployed commit from file written during build/deploy 8 16 function getDeployedCommit() { ··· 24 32 return "unknown"; 25 33 } 26 34 35 + function getRepoRoot() { 36 + const candidates = [ 37 + path.resolve(process.cwd(), ".."), 38 + process.cwd(), 39 + ]; 40 + 41 + return candidates.find((candidate) => 42 + fs.existsSync(path.join(candidate, ".git")) 43 + ) || null; 44 + } 45 + 46 + async function git(args, repoRoot) { 47 + const { stdout } = await execFileAsync("git", ["-C", repoRoot, ...args], { 48 + timeout: 10000, 49 + maxBuffer: 1024 * 1024, 50 + }); 51 + return stdout.trim(); 52 + } 53 + 54 + function parseRecentCommits(rawLog) { 55 + if (!rawLog) return []; 56 + 57 + return rawLog 58 + .split("\n") 59 + .filter(Boolean) 60 + .map((line) => { 61 + const [hash = "", message = "no message", author = "unknown", date = null] = line.split("\t"); 62 + return { 63 + hash: hash.slice(0, 7), 64 + message: message.slice(0, 60), 65 + author, 66 + date, 67 + }; 68 + }); 69 + } 70 + 71 + async function getPreferredRemote(repoRoot) { 72 + if (GIT_REMOTE_OVERRIDE) return GIT_REMOTE_OVERRIDE; 73 + 74 + const remotes = (await git(["remote"], repoRoot)) 75 + .split("\n") 76 + .map((remote) => remote.trim()) 77 + .filter(Boolean); 78 + if (remotes.includes("tangled")) return "tangled"; 79 + if (remotes.includes("origin")) return "origin"; 80 + return remotes[0] || "origin"; 81 + } 82 + 83 + async function getLatestFromTangled(repoRoot, deployedCommit) { 84 + const gitRemote = await getPreferredRemote(repoRoot); 85 + await git(["fetch", "--quiet", gitRemote, GIT_BRANCH], repoRoot); 86 + 87 + const remoteRef = `${gitRemote}/${GIT_BRANCH}`; 88 + const latestCommit = await git(["rev-parse", remoteRef], repoRoot); 89 + let behindBy = 0; 90 + 91 + if (deployedCommit !== "unknown" && latestCommit) { 92 + try { 93 + behindBy = Number( 94 + await git(["rev-list", "--count", `${deployedCommit}..${remoteRef}`], repoRoot) 95 + ) || 0; 96 + } catch { 97 + behindBy = HISTORY_SCAN_LIMIT; 98 + } 99 + } 100 + 101 + const recentRaw = await git([ 102 + "log", 103 + remoteRef, 104 + `--max-count=${RECENT_COMMIT_COUNT}`, 105 + "--pretty=format:%H\t%s\t%an\t%cI", 106 + ], repoRoot); 107 + 108 + return { 109 + latestCommit, 110 + behindBy, 111 + recentCommits: parseRecentCommits(recentRaw), 112 + }; 113 + } 114 + 115 + async function getLatestFromGitHub(deployedCommit) { 116 + const res = await fetch( 117 + "https://api.github.com/repos/whistlegraph/aesthetic-computer/commits?per_page=50", 118 + { 119 + headers: { 120 + "User-Agent": "aesthetic-computer", 121 + Accept: "application/vnd.github.v3+json", 122 + }, 123 + } 124 + ); 125 + 126 + if (!res.ok) { 127 + throw new Error(`GitHub API returned ${res.status}`); 128 + } 129 + 130 + const commits = await res.json(); 131 + const latestCommit = commits[0]?.sha; 132 + 133 + let behindBy = 0; 134 + if (deployedCommit !== "unknown" && latestCommit) { 135 + const idx = commits.findIndex((c) => 136 + c.sha.startsWith(deployedCommit.slice(0, 7)) 137 + ); 138 + behindBy = idx === -1 ? HISTORY_SCAN_LIMIT : idx; 139 + } 140 + 141 + return { 142 + latestCommit, 143 + behindBy, 144 + recentCommits: commits.slice(0, RECENT_COMMIT_COUNT).map((c) => ({ 145 + hash: c.sha.slice(0, 7), 146 + message: c.commit?.message?.split("\n")[0]?.slice(0, 60) || "no message", 147 + author: c.commit?.author?.name || c.author?.login || "unknown", 148 + date: c.commit?.author?.date, 149 + })), 150 + }; 151 + } 152 + 27 153 export default async (request) => { 28 154 const deployedCommit = getDeployedCommit(); 29 155 const url = new URL(request.url); ··· 47 173 } 48 174 49 175 try { 50 - // Fetch latest commits from GitHub (public, no auth needed) 51 - const res = await fetch( 52 - "https://api.github.com/repos/whistlegraph/aesthetic-computer/commits?per_page=50", 53 - { 54 - headers: { 55 - "User-Agent": "aesthetic-computer", 56 - Accept: "application/vnd.github.v3+json", 57 - }, 58 - } 59 - ); 60 - 61 - if (!res.ok) { 62 - throw new Error(`GitHub API returned ${res.status}`); 63 - } 64 - 65 - const commits = await res.json(); 66 - const latestCommit = commits[0]?.sha; 67 - 68 - // Find how many commits behind 69 - let behindBy = 0; 70 - if (deployedCommit !== "unknown" && latestCommit) { 71 - const idx = commits.findIndex((c) => 72 - c.sha.startsWith(deployedCommit.slice(0, 7)) 73 - ); 74 - if (idx === -1) { 75 - behindBy = 50; // More than 50 commits behind or commit not found 76 - } else { 77 - behindBy = idx; 78 - } 79 - } 176 + const repoRoot = getRepoRoot(); 177 + const { 178 + latestCommit, 179 + behindBy, 180 + recentCommits, 181 + } = repoRoot 182 + ? await getLatestFromTangled(repoRoot, deployedCommit) 183 + : await getLatestFromGitHub(deployedCommit); 80 184 81 185 const status = behindBy === 0 ? "current" : "behind"; 82 - 83 - // Extract recent commits for ticker display (last 10) 84 - const recentCommits = commits.slice(0, 10).map((c) => ({ 85 - hash: c.sha.slice(0, 7), 86 - message: c.commit?.message?.split("\n")[0]?.slice(0, 60) || "no message", // First line, truncated 87 - author: c.commit?.author?.name || c.author?.login || "unknown", 88 - date: c.commit?.author?.date, 89 - })); 90 186 91 187 return new Response( 92 188 JSON.stringify({