← Commit history

Publish 0.11.1

Colby Knox ·522deb3e48 ·2mo ago ·parent bc8e67c
7 files changed +120−29
data/pages.json+26
@@ -1,4 +1,30 @@ [+ {+  "type": "app",+  "title": "COSMIIC Implantables",+  "author": "",+  "score": 0,+  "stars": 0,+  "installs30d": 0,+  "brief": "The COSMIIC implantables project as a ready-to-open Adom 3D editor layout",+  "tags": [],+  "updated_at": "2026-07-30T20:05:23.149Z",+  "owner": "cosmiic",+  "slug": "implantables"+ },+ {+  "type": "app",+  "title": "COSMIIC WireBender",+  "author": "",+  "score": 0,+  "stars": 0,+  "installs30d": 0,+  "brief": "The COSMIIC firmware development bench as a ready-to-open Adom 3D editor layout",+  "tags": [],+  "updated_at": "2026-07-30T19:59:32.597Z",+  "owner": "cosmiic",+  "slug": "wirebender"+ },  {   "type": "app",   "title": "US History Jeopardy",
doctor.mjs+1−1
@@ -203,6 +203,6 @@ export function printReport(report, out = console.error) { if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {   const report = await runChecks();   printReport(report);-  console.log(JSON.stringify({ ok: report.ok, checks: report.checks }));+  console.log(JSON.stringify({ ok: report.ok, renderer: report.renderer, checks: report.checks }));   process.exit(report.ok ? 0 : 1); }
install.sh+22−13
@@ -8,16 +8,25 @@ echo "run 'hero-studio' (serves on port 8846), then open /proxy/8846/ in a webvi echo "refresh the sample data with ./refresh-data.sh (needs the adom-wiki CLI)"  # Renderer preflight, warn mode: install stays exit 0 (driving the studio and-# fixing deps later are legitimate on light containers), but a broken renderer-# must be impossible to read as success. Generate spawns headless Chrome INSIDE-# this container; light images (Hydrogen Desktop) do not ship that runtime.-if node "$PWD/doctor.mjs" >/dev/null 2>&1; then-  echo "renderer: OK (headless Chrome + stamping verified; Generate will work)"-else-  echo ""-  echo "!! RENDERER_NOT_READY -- installed, BUT GENERATE WILL FAIL IN THIS CONTAINER !!"-  echo "!! Do NOT open the pose gate for a human yet: run 'hero-studio doctor',    !!"-  echo "!! run the fix commands it prints (you have the shell; no SSH needed),     !!"-  echo "!! and re-run it until it passes. pose refuses to open until then.         !!"-  node "$PWD/doctor.mjs" 2>&1 | sed 's/^/   /' || true-fi+# fixing deps later are legitimate on light containers), but the message must+# say which renderer actually verified. "OK" while local Chrome was missing is+# how hero-studio#8 happened: the pup fallback carried a fresh install further+# than its fidelity deserved, silently.+DOCTOR_JSON="$(node "$PWD/doctor.mjs" 2>/dev/null || true)"+case "$DOCTOR_JSON" in+  *'"renderer":"local"'*)+    echo "renderer: OK (local headless Chrome verified; Generate renders in-container)" ;;+  *'"renderer":"pup"'*)+    echo ""+    echo "!! renderer: PUP FALLBACK ONLY. The local headless-Chrome stack is missing,  !!"+    echo "!! so Generate will render through the HOST browser: it works, at reduced    !!"+    echo "!! fidelity, and only while Adom Desktop is running. For the full local      !!"+    echo "!! renderer run 'hero-studio doctor' and apply the fixes it prints.          !!" ;;+  *)+    echo ""+    echo "!! RENDERER_NOT_READY -- installed, BUT GENERATE WILL FAIL IN THIS CONTAINER !!"+    echo "!! Do NOT open the pose gate for a human yet: run 'hero-studio doctor',    !!"+    echo "!! run the fix commands it prints (you have the shell; no SSH needed),     !!"+    echo "!! and re-run it until it passes. pose refuses to open until then.         !!"+    node "$PWD/doctor.mjs" 2>&1 | sed 's/^/   /' || true ;;+esac
package.json+1−1
@@ -2,7 +2,7 @@   "slug": "hero-studio",   "title": "Hero Studio",   "type": "app",-  "version": "0.11.0",+  "version": "0.11.1",   "description": "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. Adom-org pages get the house wordmark and palette automatically; every other owner brings their own accent and background colors, with placement fixed for all. The floating app window is the vitrine: it auto-loads a real screenshot from the package's wiki page (screenshots/, docs/), takes a manual import, or a full-hero import (the human-ratified re-stamp path for existing heroes). The pose gate opens in verification mode: per-item checklist (title, subtitle, vitrine, type match) and a Generate that unlocks only fully confirmed. Includes vitrine bleeds, brand tile themes, gradient angles, pixel-faithful wiki marquee and index-card previews, and a component-hero mode with a photo/raytrace/phong fallback ladder.",   "license": "MIT",   "tags": [
page.json+1−1
@@ -2,7 +2,7 @@   "slug": "hero-studio",   "title": "Hero Studio",   "type": "app",-  "version": "0.11.0",+  "version": "0.11.1",   "description": "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. Adom-org pages get the house wordmark and palette automatically; every other owner brings their own accent and background colors, with placement fixed for all. The floating app window is the vitrine: it auto-loads a real screenshot from the package's wiki page (screenshots/, docs/), takes a manual import, or a full-hero import (the human-ratified re-stamp path for existing heroes). The pose gate opens in verification mode: per-item checklist (title, subtitle, vitrine, type match) and a Generate that unlocks only fully confirmed. Includes vitrine bleeds, brand tile themes, gradient angles, pixel-faithful wiki marquee and index-card previews, and a component-hero mode with a photo/raytrace/phong fallback ladder.",   "license": "MIT",   "tags": [
render-pup.mjs+45−13
@@ -56,8 +56,15 @@ await sleep(2000);  // 5. measure #hero in viewport pixels + devicePixelRatio. Numbers only, // joined with commas: no quotes means no escaping ambiguity in the transport.+// Also paint a sentinel square in the viewport's top-left corner: the crop+// step verifies it is present in the screenshot, proving the relay shot THIS+// window and not whatever was focused (hero-studio#8: a relay that ignores+// sessionId captures the human's interactive studio, tooltips and all). const boxRaw = await evalIn(-  "(function(){var b=document.querySelector('#hero').getBoundingClientRect();" ++  "(function(){var s=document.getElementById('hs-sentinel');if(!s){s=document.createElement('div');" ++  "s.id='hs-sentinel';s.style.cssText='position:fixed;top:0;left:0;width:16px;height:16px;" ++  "background:#ff00fe;z-index:2147483647';document.body.appendChild(s);}" ++  "var b=document.querySelector('#hero').getBoundingClientRect();" +   "return 'HSBOX@'+[b.x,b.y,b.width,b.height,window.devicePixelRatio].join(',')+'@XOBSH';})()"); const m = boxRaw && boxRaw.match(/HSBOX@([\d.,\-]+)@XOBSH/); if (!m) die('could not measure #hero (unexpected browser_eval output).');@@ -80,32 +87,57 @@ if (!srcPng) { } if (!srcPng) die('browser_screenshot returned neither a readable path nor base64 (raw head: ' + shot.out.slice(0, 200) + ')'); -// 7. crop #hero out (viewport rect * dpr, clamped) and sanity-check aspect.-// The screenshot is assumed to be the page viewport; if the host returns a-// native window shot including chrome, the crop will land off and the aspect-// warning below is the tell.+// 7. verify the sentinel, then crop #hero out (viewport rect * dpr, clamped).+// Both checks are HARD failures: a hero that would fail the registry gate+// must never be handed to the human as a success (hero-studio#8). const py = [   'import sys',   'from PIL import Image',   'src, out = sys.argv[1], sys.argv[2]',   'x, y, w, h, dpr = [float(v) for v in sys.argv[3:8]]',-  'im = Image.open(src)',-  'box = (max(0, round(x*dpr)), max(0, round(y*dpr)), min(im.size[0], round((x+w)*dpr)), min(im.size[1], round((y+h)*dpr)))',+  'im = Image.open(src).convert("RGB")',+  'W, H = im.size',+  '# locate the 16px #ff00fe sentinel painted at viewport (0,0). Its found',+  '# position IS the viewport origin inside the screenshot: a native window',+  '# shot includes chrome above the viewport, which would silently shift the',+  '# crop while keeping its aspect, so the origin must come from the pixels.',+  'ox = oy = -1',+  'strip = im.crop((0, 0, min(W, 240), min(H, 480)))',+  'px = strip.load()',+  'for yy in range(strip.size[1]):',+  '    for xx in range(strip.size[0]):',+  '        r, g, b = px[xx, yy]',+  '        if r > 200 and g < 80 and b > 200:',+  '            ox, oy = xx, yy',+  '            break',+  '    if ox >= 0:',+  '        break',+  'if ox < 0:',+  '    print("NO-SENTINEL"); sys.exit(3)',+  'box = (max(0, ox + round(x*dpr)), max(0, oy + round(y*dpr)), min(W, ox + round((x+w)*dpr)), min(H, oy + round((y+h)*dpr)))',   'im2 = im.crop(box)',+  'aspect = im2.size[0] / im2.size[1]',+  'if abs(aspect - 1.6) > 0.03:',+  '    print("BAD-ASPECT %dx%d" % im2.size); sys.exit(4)',   'im2.save(out)',   'print("%dx%d" % im2.size)', ].join('\n'); const crop = await new Promise((res) => {   execFile('python3', ['-c', py, srcPng, out, String(bx), String(by), String(bw), String(bh), String(dpr)],-    { timeout: 30000 }, (err, stdout, stderr) => res({ ok: !err, out: String(stdout || ''), err: String(stderr || '') }));+    { timeout: 30000 }, (err, stdout, stderr) => res({ code: err ? (err.code || 1) : 0, out: String(stdout || ''), err: String(stderr || '') })); }); if (srcPng === out + '.full.png') { try { fs.unlinkSync(srcPng); } catch {} }-if (!crop.ok || !fs.existsSync(out)) die('PIL crop failed: ' + crop.err.trim().split('\n').pop());-const [cw, ch] = crop.out.trim().split('x').map(Number);-const aspect = cw / ch;-if (Math.abs(aspect - 1.6) > 0.03) {-  console.error('warning: cropped ' + cw + 'x' + ch + ' (aspect ' + aspect.toFixed(3) + ', want 1.600 +-0.03): the hero may have been clipped by the host viewport; enlarge the host window and regenerate.');+if (crop.code === 3) {+  die('the screenshot does NOT contain the render window (sentinel missing: ' + crop.out.trim() + '). ' ++      'The relay captured a different window than the hero-render session, so the shot was discarded; ' ++      'this is the wrong-window class from hero-studio#8. Report which adom-desktop build this is.');+}+if (crop.code === 4) {+  die('cropped ' + crop.out.trim().replace('BAD-ASPECT ', '') + ', not 16:10 within 0.03: the hero was clipped by the host viewport. ' ++      'Enlarge the host render window and regenerate; refusing to ship a hero the registry would reject.'); }+if (crop.code !== 0 || !fs.existsSync(out)) die('PIL crop failed: ' + crop.err.trim().split('\n').pop());+const [cw, ch] = crop.out.trim().split('x').map(Number); if (cw < 1600) {   console.error('warning: cropped width ' + cw + ' is under the gate\'s 1600px floor; the finalize upscale will cost sharpness. A larger host window or higher devicePixelRatio fixes it.'); }
server.js+24
@@ -288,6 +288,22 @@ const server = http.createServer((req, res) => {     return;      function finalize(renderedVia) {+      // refuse to hand the human a hero the registry will reject: the gate+      // wants 16:10 within 0.03, and a bad capture (wrong window, clipped+      // viewport) shows up here as a wrong aspect. PNG IHDR: width/height+      // big-endian at bytes 16/20.+      try {+        const head = fs.readFileSync(outFile);+        const w = head.readUInt32BE(16);+        const h = head.readUInt32BE(20);+        if (!(w > 0 && h > 0) || Math.abs(w / h - 1.6) > 0.03) {+          logLine('error', 'generate via ' + renderedVia + ' produced ' + w + 'x' + h + ' (not 16:10); refusing to ship it');+          try { fs.unlinkSync(outFile); } catch {}+          return finish({ ok: false, error: renderedVia + ' render produced ' + w + 'x' + h + ', not 16:10; the registry would reject it, so it was discarded', hint: renderedVia === 'pup' ? 'the host capture was clipped or hit the wrong window; enlarge the host render window and retry, and report what render-pup printed on stderr' : "run 'hero-studio doctor' and retry" });+        }+      } catch (e) {+        return finish({ ok: false, error: 'could not read the rendered PNG: ' + e.message, hint: "run 'hero-studio doctor'" });+      }       // downscale to the hero standard 2000x1250 and stamp provenance metadata       const meta = JSON.stringify({ generator: 'hero-studio', version: PKG_VERSION, ts, renderer: renderedVia, state: { mode: state.mode, page: state.page, comp: state.comp, theme: state.theme, layout: state.layout, gradAngle: state.gradAngle, brand: state.brand, fontSize: state.fontSize, verify: state.verify, confirm: state.confirm, vitrine_used: !!(state.vitrine && state.vitrine.src), vitrine_full: !!(state.vitrine && state.vitrine.full), circuitSeed: state.theme === 'circuitry' ? state.circuitSeed : undefined } });       const py = 'import sys\n' +@@ -345,4 +361,12 @@ const server = http.createServer((req, res) => {   }); }); +server.on('error', (err) => {+  if (err && err.code === 'EADDRINUSE') {+    console.error('hero-studio: a server is already running on :' + PORT + ' (that one keeps working).');+    console.error("talk to it with 'hero-studio state', or stop it first to start a fresh one.");+    process.exit(1);+  }+  throw err;+}); server.listen(PORT, () => console.log('hero-studio on ' + PORT));