← Commit history

Publish 0.19.1

Colby Knox ·af711f8b3e ·2mo ago ·parent 787164c
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",