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
← Commit history
Publish 0.19.1
4 files changed
+147−4
SKILL.md+7−2@@ -47,8 +47,13 @@ scales by gating the STYLE, not every part: headline?, subtitle?}. Generation is local (no wiki calls, no rate limits). The publish loop is-where pacing matters: publish serially or 2-3 at a time and let the 429-hints set the pace if they appear.+where pacing matters, and you do not have to write it: a complete runnable+reference ships with this package at docs/bulk-publish-reference.mjs+(installed under adom_modules/adom/hero-studio/docs/). It walks a parts+directory (part.json + model.step + render.png per part), batches the heroes+through your gated style, scaffolds and lints each page, publishes SERIALLY+with pacing, backs off exponentially on any 429, and RESUMES from+bulk-state.json so a stop never repeats work. Copy it, do not reinvent it. ## Automated mode: same-session repeats only
docs/bulk-publish-reference.mjsadded+136@@ -0,0 +1,136 @@+#!/usr/bin/env node+/* Reference implementation: bulk hardware pages with style-gated heroes.+ Copy it, point it at a parts directory, run it. It is DELIBERATELY plain+ node with no dependencies, and it resumes: every completed part lands in+ bulk-state.json, so a crash or a rate-limit stop never repeats work.++ Layout it expects (one dir per part):+ parts/+ lm317-dpak/+ part.json { "title": "LM317 (DPAK)", "mpn": "LM317MDT", "kind": "component", "tags": [...] }+ model.step (REQUIRED by the registry)+ render.png (from chip-thumbnailer; becomes the hero showcase)+ symbol.kicad_sym footprint.kicad_mod (optional, warnings without)++ Usage:+ node bulk-publish-reference.mjs --parts ./parts --org adom \+ --style <saved-style> --human colby --quote "match the family style" \+ [--pace 4] [--dry-run] [--studio-port 8846]++ Before running: pose ONE representative part, let a human Generate, then+ `hero-studio style save <name>`. That approval gates every hero this+ script produces (it rides each PNG's provenance).++ Rate limits: publishes run SERIALLY with --pace seconds between; any+ publish whose output mentions a 429/rate answer triggers exponential+ backoff (30s, 60s, 120s) and a retry, then the part is marked failed and+ the loop continues. Nothing here hammers the wiki. */+import { execFileSync, execFile } from 'child_process';+import fs from 'fs';+import path from 'path';++const arg = (f, d) => { const i = process.argv.indexOf(f); return i >= 0 ? process.argv[i + 1] : d; };+const has = (f) => process.argv.includes(f);+const PARTS = arg('--parts');+const ORG = arg('--org', 'adom');+const STYLE = arg('--style');+const HUMAN = arg('--human');+const QUOTE = arg('--quote');+const PACE = Number(arg('--pace', 4));+const PORT = arg('--studio-port', process.env.HERO_STUDIO_PORT || '8846');+const DRY = has('--dry-run');+if (!PARTS || !STYLE || !HUMAN || !QUOTE) {+ console.error('usage: bulk-publish-reference.mjs --parts <dir> --style <name> --human <who> --quote "<their words>" [--org adom] [--pace 4] [--dry-run]');+ process.exit(2);+}+const AI_THREAD = process.env.ADOM_AI_THREAD || 'hardware bulk publish';+const sleep = (s) => new Promise((r) => setTimeout(r, s * 1000));+const STATE_FILE = path.join(PARTS, 'bulk-state.json');+const state = fs.existsSync(STATE_FILE) ? JSON.parse(fs.readFileSync(STATE_FILE, 'utf8')) : { done: {}, failed: {} };+const save = () => fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 1));++const dirs = fs.readdirSync(PARTS).filter((d) => fs.existsSync(path.join(PARTS, d, 'part.json')));+console.error(`bulk: ${dirs.length} part(s); ${Object.keys(state.done).length} already done (resume)`);++// 1. heroes first, one paced batch through the human-gated style+const partsManifest = [];+for (const d of dirs) {+ if (state.done[d]) continue;+ const meta = JSON.parse(fs.readFileSync(path.join(PARTS, d, 'part.json'), 'utf8'));+ const render = path.join(PARTS, d, 'render.png');+ partsManifest.push({+ ref: `${ORG}/${d}`, title: meta.title, kind: meta.kind || 'component',+ vitrine: fs.existsSync(render) ? render : undefined, _dir: d,+ });+}+if (partsManifest.length && !DRY) {+ const manifestPath = path.join(PARTS, 'parts-batch.json');+ fs.writeFileSync(manifestPath, JSON.stringify(partsManifest.map(({ _dir, ...p }) => p)));+ console.error('generating heroes through the gated style (local, paced)...');+ const out = execFileSync('hero-studio', ['--ai-thread', AI_THREAD, 'batch',+ '--style', STYLE, '--parts', manifestPath, '--pace', '2',+ '--human', HUMAN, '--quote', QUOTE, '--port', PORT], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'inherit'] });+ const results = JSON.parse(out).results;+ // copy each generated hero into its part dir as docs/hero.png; generated/+ // lives next to the hero-studio install (same resolution bin/hero-studio uses)+ const pkgDir = path.join(path.dirname(fs.realpathSync(execFileSync('which', ['hero-studio'], { encoding: 'utf8' }).trim())), '..');+ for (const r of results) {+ if (!r.ok) { state.failed[r.ref] = r.error; continue; }+ const p = partsManifest.find((x) => x.ref === r.ref);+ const dst = path.join(PARTS, p._dir, 'docs');+ fs.mkdirSync(dst, { recursive: true });+ fs.copyFileSync(path.join(pkgDir, 'generated', r.file), path.join(dst, 'hero.png'));+ }+ save();+}++// 2. init + publish, serial, paced, 429-aware+const run = (cmd, args, cwd) => new Promise((res) => {+ execFile(cmd, args, { cwd, encoding: 'utf8' }, (err, so, se) => res({ ok: !err, out: (so || '') + (se || '') }));+});+for (const p of partsManifest) {+ if (state.done[p._dir] || state.failed[p.ref]) continue;+ const dir = path.join(PARTS, p._dir);+ const meta = JSON.parse(fs.readFileSync(path.join(dir, 'part.json'), 'utf8'));+ if (DRY) { console.error(`[dry] would publish ${p.ref}`); continue; }+ // scaffold in place if package.json is absent+ if (!fs.existsSync(path.join(dir, 'package.json'))) {+ const pkg = {+ slug: p._dir, title: meta.title, type: 'hardware', kind: meta.kind || 'component',+ version: meta.version || '0.1.0', brief: meta.brief || meta.title,+ description: meta.description || meta.title, tags: meta.tags || ['hardware'],+ dependencies: {}, hero: { path: 'docs/hero.png' }, org: ORG, platform: 'any',+ };+ if (meta.mpn) pkg.mpn = meta.mpn;+ fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify(pkg, null, 2));+ }+ const lint = await run('adom-wiki', ['pkg', 'lint'], dir);+ if (/\berror\b/i.test(lint.out) && !/0 error/.test(lint.out)) {+ console.error(` ${p.ref}: lint errors, skipping (fix and re-run; resume skips done parts)`);+ state.failed[p.ref] = 'lint';+ save();+ continue;+ }+ let published = false;+ for (let attempt = 0, backoff = 30; attempt < 4 && !published; attempt++) {+ const pub = await run('adom-wiki', ['pkg', 'publish', '--org', ORG], dir);+ if (pub.ok && /"published"/.test(pub.out)) {+ published = true;+ if (meta.source) await run('adom-wiki', ['page', 'edit', p.ref, '--source', meta.source], dir);+ state.done[p._dir] = { at: new Date().toISOString() };+ delete state.failed[p.ref];+ console.error(` ${p.ref}: published`);+ } else if (/429|rate/i.test(pub.out)) {+ console.error(` ${p.ref}: rate-limited; backing off ${backoff}s (attempt ${attempt + 1})`);+ await sleep(backoff);+ backoff *= 2;+ } else {+ console.error(` ${p.ref}: FAILED: ${pub.out.split('\n').find((l) => /error/i.test(l)) || 'unknown'}`);+ state.failed[p.ref] = 'publish';+ break;+ }+ }+ save();+ await sleep(PACE);+}+console.error(`done: ${Object.keys(state.done).length} published, ${Object.keys(state.failed).length} failed (bulk-state.json has the ledger; re-running resumes)`);
package.json+2−1@@ -2,7 +2,7 @@ "slug": "hero-studio", "title": "Hero Studio", "type": "app",- "version": "0.19.0",+ "version": "0.19.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": [@@ -85,6 +85,7 @@ "bin/hero-studio", "public/**", "data/**",+ "docs/bulk-publish-reference.mjs", "docs/contexts.png", "docs/themes.png", "docs/index-cards.png",
page.json+2−1@@ -2,7 +2,7 @@ "slug": "hero-studio", "title": "Hero Studio", "type": "app",- "version": "0.19.0",+ "version": "0.19.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": [@@ -86,6 +86,7 @@ "bin/hero-studio", "public/**", "data/**",+ "docs/bulk-publish-reference.mjs", "docs/contexts.png", "docs/themes.png", "docs/index-cards.png",