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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
// Where Hero Studio is running, answered once for every module (server.js,
// doctor.mjs, pose.mjs, render-pup.mjs, install.sh). The studio was born in
// Adom containers and assumed them: a token file at /var/run/adom/api-key,
// HOME set, python3 on PATH, a VS Code proxy URL. On a Windows or macOS
// desktop none of that holds, and each assumption failed silently in its own
// place. Everything that depends on the host asks here instead.
const fs = require('fs');
const os = require('os');
const path = require('path');
const { execFileSync } = require('child_process');
const IS_WIN = process.platform === 'win32';
const IS_MAC = process.platform === 'darwin';
// Overridable for tests only: this box HAS a container key, and the identity
// order below must be testable without it.
const CONTAINER_TOKEN_FILE = process.env.HERO_STUDIO_CONTAINER_KEY || '/var/run/adom/api-key';
/// The home directory. os.homedir() reads USERPROFILE on Windows, so it is
/// right whichever shell started the process (PowerShell sets no HOME).
function homeDir() {
return os.homedir();
}
/// Inside an Adom container: the container key exists, or the VS Code proxy
/// is set. Wording ("this container" vs "this machine") and the fix lines
/// branch on it.
function inContainer() {
return !!process.env.VSCODE_PROXY_URI || fs.existsSync(CONTAINER_TOKEN_FILE);
}
function where() {
return inContainer() ? 'this container' : 'this machine';
}
/// The same, with its preposition: "in this container" / "on this machine".
function whereIn() {
return inContainer() ? 'in this container' : 'on this machine';
}
/// The per-user config directory, the same one the adom-wiki and adom-cli
/// CLIs use (Rust dirs::config_dir): %APPDATA% on Windows,
/// ~/Library/Application Support on macOS, $XDG_CONFIG_HOME or ~/.config.
function configDir() {
if (IS_WIN) return process.env.APPDATA || path.join(homeDir(), 'AppData', 'Roaming');
if (IS_MAC) return path.join(homeDir(), 'Library', 'Application Support');
return process.env.XDG_CONFIG_HOME || path.join(homeDir(), '.config');
}
function readJsonField(file, field) {
try {
const v = JSON.parse(fs.readFileSync(file, 'utf8'));
const t = v && typeof v[field] === 'string' ? v[field].trim() : '';
return t || null;
} catch { return null; }
}
function readTrimmed(file) {
try { const t = fs.readFileSync(file, 'utf8').trim(); return t || null; } catch { return null; }
}
/// The wiki identity to read as, first match wins, in the adom-wiki CLI's
/// order: env ADOM_WIKI_TOKEN / ADOMPKG_TOKEN, the wiki CLI's stored token,
/// the container key, the adom-cli login, then the desktop session that Adom
/// Bridge writes to ~/.adom/session.json. Re-read on every call (cheap), so
/// a sign-in after the server started is picked up without a restart.
function wikiIdentity() {
const env = (k) => (process.env[k] || '').trim() || null;
const tries = [
['env:ADOM_WIKI_TOKEN', () => env('ADOM_WIKI_TOKEN')],
['env:ADOMPKG_TOKEN', () => env('ADOMPKG_TOKEN')],
['adom-wiki config', () => readJsonField(path.join(configDir(), 'adom-wiki', 'config.json'), 'api_token')],
['container key', () => readTrimmed(CONTAINER_TOKEN_FILE)],
['adom-cli login', () => readJsonField(path.join(configDir(), 'adom-cli', 'config.json'), 'api_token')],
['desktop session (~/.adom/session.json)', () => readJsonField(path.join(homeDir(), '.adom', 'session.json'), 'session_token')],
];
for (const [source, get] of tries) {
const token = get();
if (token) return { token, source, anonymous: false };
}
return { token: null, source: 'none', anonymous: true };
}
/// { Authorization } for a wiki call, or {} when there is no identity.
function authHeader() {
const id = wikiIdentity();
return id.token ? { Authorization: 'Bearer ' + id.token } : {};
}
/// The interpreter that can run Pillow, probed once and cached: python3,
/// then python, then the Windows launcher `py -3`. On Windows `python3` is
/// usually the Microsoft Store alias stub, so it is tried last there.
/// Returns { cmd, args } (args to put before -c), or null.
let pythonCache;
function pythonWithPillow({ fresh = false } = {}) {
// Only a FOUND interpreter is cached: after `pip install pillow` the next
// Generate must find it without a server restart.
if (pythonCache && !fresh) return pythonCache;
const order = IS_WIN
? [['python', []], ['py', ['-3']], ['python3', []]]
: [['python3', []], ['python', []]];
pythonCache = null;
for (const [cmd, pre] of order) {
try {
execFileSync(cmd, [...pre, '-c', 'import PIL'], { stdio: 'ignore', timeout: 15000, windowsHide: true });
pythonCache = { cmd, args: pre, label: [cmd, ...pre].join(' ') };
break;
} catch { /* next */ }
}
return pythonCache;
}
/// ImageMagick for the no-Pillow fallback. Never `convert` on Windows: that
/// name is the FAT-to-NTFS volume converter in System32. ImageMagick 7 is
/// `magick` everywhere.
function imageMagick() {
const tries = IS_WIN ? ['magick'] : ['magick', 'convert'];
for (const cmd of tries) {
try {
execFileSync(cmd, ['-version'], { stdio: 'ignore', timeout: 10000, windowsHide: true });
return cmd;
} catch { /* next */ }
}
return null;
}
/// Platform-aware fix lines for doctor and the gate.
function fixFor(what) {
if (what === 'pillow') {
if (IS_WIN) return 'py -m pip install pillow # or: python -m pip install pillow (python.org Python, not the Microsoft Store alias)';
if (IS_MAC) return 'python3 -m pip install pillow';
return 'sudo apt-get install -y python3-pil # or: pip3 install pillow';
}
if (what === 'chrome') {
if (IS_WIN) return 'install Google Chrome, or set CHROME to a chrome.exe (e.g. C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe)';
if (IS_MAC) return 'install Google Chrome, or set CHROME to "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"';
return 'npx @puppeteer/browsers install chrome@stable --path ~/.cache/puppeteer';
}
return '';
}
/// The command that opens a URL in the default browser.
function openUrlCommand(url) {
if (IS_WIN) return { cmd: 'cmd', args: ['/c', 'start', '""', url] };
if (IS_MAC) return { cmd: 'open', args: [url] };
return { cmd: 'xdg-open', args: [url] };
}
module.exports = {
IS_WIN, IS_MAC, CONTAINER_TOKEN_FILE,
homeDir, inContainer, where, whereIn, configDir,
wikiIdentity, authHeader,
pythonWithPillow, imageMagick, fixFor, openUrlCommand,
};