app
Hero Studio
Public Made by Adomby adom
The hero-image lab with a human gate: `hero-studio pose <owner/slug>` opens the studio in front of the human, posed for their package, and blocks until they click Generate, producing the provenance-stamped 2000x1250 PNG the registry's hero gate requires. The image carries the title and artwork; the exported image also bakes the type pill (top right) and full wiki slug (bottom right); the page shows the hero bare, and only the homepage marquee overlays its own dynamic info. Every hero wears its P
master
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
// Where the local renderer's pieces live, shared by doctor.mjs (the full
// preflight), server.js (the cheap status the gate shows) and install.sh.
// One list, so a path added here is seen by every reader (hero-studio#58).
//
// puppeteer-core is installed OUTSIDE the package directory, under
// ~/.cache/hero-studio, because a package update replaces the package
// directory including its node_modules, which is how a running studio lost
// its renderer between a passing doctor and the human's Generate click.
const fs = require('fs');
const path = require('path');
const os = require('os');
const DIR = __dirname;
const RENDERER_HOME = process.env.HERO_STUDIO_RENDERER_HOME || path.join(os.homedir(), '.cache', 'hero-studio');
// Test-only: consult nothing but the env override and RENDERER_HOME, so a
// box that has puppeteer-core in a shared node_modules can still exercise
// the "no renderer" path.
const ISOLATED = process.env.HERO_STUDIO_RENDERER_ISOLATED === '1';
// Package roots, in the order they win. The entry file inside differs by
// line (25.x: lib/puppeteer/puppeteer-core.js; 24.x: lib/esm/puppeteer/...),
// so it is read from each package.json rather than assumed.
function puppeteerRoots() {
const list = [path.join(RENDERER_HOME, 'node_modules', 'puppeteer-core')];
if (!ISOLATED) {
list.push(path.join(DIR, 'node_modules', 'puppeteer-core'), '/home/adom/project/node_modules/puppeteer-core', path.join(os.homedir(), 'node_modules', 'puppeteer-core'));
}
return list;
}
function entryOf(root) {
try {
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
const ex = pkg.exports && pkg.exports['.'];
const rel = (ex && (typeof ex === 'string' ? ex : (ex.import || ex.default || ex.require))) || pkg.main;
const f = rel ? path.join(root, rel) : null;
return f && fs.existsSync(f) ? f : null;
} catch { return null; }
}
// Entry files worth trying, best first: the env override (a file), then each
// root's own entry.
function puppeteerCandidates() {
const list = [];
if (process.env.PUPPETEER_CORE) list.push(process.env.PUPPETEER_CORE);
for (const r of puppeteerRoots()) { const e = entryOf(r); if (e) list.push(e); }
return list;
}
// package.json paths for the same roots (the engines check reads them).
function puppeteerPackages() {
return puppeteerRoots().map((r) => path.join(r, 'package.json')).filter((f) => fs.existsSync(f));
}
// The first puppeteer-core entry file that exists, else the package as Node
// resolves it from here, else null.
function puppeteerPath() {
for (const c of puppeteerCandidates()) if (fs.existsSync(c)) return c;
if (ISOLATED) return null;
try { return require.resolve('puppeteer-core'); } catch { return null; }
}
// Every place a usable Chrome can live, best first: CHROME, the puppeteer
// cache (whose folder name depends on the OS: chrome-linux64, chrome-win64,
// chrome-mac-arm64 / chrome-mac-x64), then the standard system installs.
// Only the Linux cache layout used to be known, so a desktop with Chrome in
// Program Files was told Chrome was missing.
function chromeCandidates(platform = process.platform, env = process.env, home = os.homedir()) {
const list = [];
const cache = path.join(home, '.cache', 'puppeteer', 'chrome');
let versions = [];
try { versions = fs.readdirSync(cache).sort().reverse(); } catch { versions = []; }
const inCache = (rel) => versions.map((d) => path.join(cache, d, ...rel));
if (platform === 'win32') {
list.push(...inCache(['chrome-win64', 'chrome.exe']), ...inCache(['chrome-win32', 'chrome.exe']));
for (const base of [env.ProgramFiles, env['ProgramFiles(x86)'], env.LOCALAPPDATA]) {
if (base) list.push(path.win32.join(base, 'Google', 'Chrome', 'Application', 'chrome.exe'));
}
} else if (platform === 'darwin') {
for (const arch of ['chrome-mac-arm64', 'chrome-mac-x64', 'chrome-mac']) {
list.push(...inCache([arch, 'Google Chrome for Testing.app', 'Contents', 'MacOS', 'Google Chrome for Testing']));
}
list.push('/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
path.join(home, 'Applications', 'Google Chrome.app', 'Contents', 'MacOS', 'Google Chrome'));
} else {
list.push(...inCache(['chrome-linux64', 'chrome']));
list.push('/usr/bin/google-chrome', '/usr/bin/google-chrome-stable', '/usr/bin/chromium', '/usr/bin/chromium-browser');
}
return list;
}
function chromePath() {
if (process.env.CHROME && fs.existsSync(process.env.CHROME)) return process.env.CHROME;
if (ISOLATED) return null;
for (const c of chromeCandidates()) if (fs.existsSync(c)) return c;
return null;
}
// puppeteer-core 25.x wants Node >= 22.12 (it imports on 18 and then fails at
// launch); 24.x is the last line that runs on older Node.
function puppeteerPin(nodeVersion) {
const [a, b] = String(nodeVersion || process.versions.node).split('.').map(Number);
return a > 22 || (a === 22 && b >= 12) ? 'latest' : '24';
}
function puppeteerInstallCommand() {
return 'npm install --prefix "' + RENDERER_HOME + '" --no-audit --no-fund puppeteer-core@' + puppeteerPin() +
' # outside the package dir, so a package update cannot remove it; no restart needed';
}
function chromeInstallCommand() {
if (process.platform === 'win32' || process.platform === 'darwin') return require('./platform.cjs').fixFor('chrome');
return 'npx @puppeteer/browsers install chrome@stable --path ' + path.join(os.homedir(), '.cache', 'puppeteer');
}
// The cheap, synchronous status the gate shows: files exist, nothing is
// launched. doctor.mjs is the ground truth (it launches Chrome); this is what
// can run before every Generate and once a minute without cost.
function quickStatus() {
const pptr = puppeteerPath();
const chrome = chromePath();
const proxied = !!process.env.VSCODE_PROXY_URI;
return {
local: {
ok: !!(pptr && chrome),
puppeteer: pptr,
chrome,
why: !pptr ? 'puppeteer-core not found' : (!chrome ? 'Chrome not found' : null),
fix: !pptr ? puppeteerInstallCommand() : (!chrome ? chromeInstallCommand() : null),
},
pup: { ok: proxied, why: proxied ? null : 'VSCODE_PROXY_URI is not set, so the host browser cannot reach the studio' },
checkedAt: Date.now(),
};
}
module.exports = { DIR, RENDERER_HOME, ISOLATED, chromeCandidates, puppeteerRoots, puppeteerCandidates, puppeteerPackages, puppeteerPath, chromePath, puppeteerPin, puppeteerInstallCommand, chromeInstallCommand, quickStatus };