← Commit history

Publish 0.20.5

Colby Knox ·9e0952edbb ·1mo ago ·parent 3e6a1bc
5 files changed +110−10
data/pages.json+14
@@ -1,4 +1,18 @@ [+ {+  "type": "component",+  "title": "Nametag Blank Large LED",+  "author": "",+  "score": 0,+  "stars": 0,+  "installs30d": 0,+  "brief": "A large LED nametag blank board for badge builds.",+  "tags": [],+  "updated_at": "2026-08-28T19:08:26.378Z",+  "owner": "kcknox",+  "slug": "ztest-hero52",+  "aiThread": "cli thread hero52 verify"+ },  {   "type": "hardware",   "title": "Ztest Batch B",
package.json+1−1
@@ -2,7 +2,7 @@   "slug": "hero-studio",   "title": "Hero Studio",   "type": "app",-  "version": "0.20.4",+  "version": "0.20.5",   "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 showcase: 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, showcase, type match) and a Generate that unlocks only fully confirmed. Includes showcase 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.20.4",+  "version": "0.20.5",   "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 showcase: 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, showcase, type match) and a Generate that unlocks only fully confirmed. Includes showcase 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": [
pose.mjs+79−7
@@ -25,7 +25,7 @@ const flags = {}; const pos = []; // Valueless flags MUST be declared: the old parser consumed the next token as // a value, so `--steal --headline "X"` silently dropped the headline (#42).-const BOOL_FLAGS = new Set(['steal', 'fresh-showcase', 'no-fallback', 'keep-alive']);+const BOOL_FLAGS = new Set(['steal', 'fresh-showcase', 'no-fallback', 'keep-alive', 'seed-only', 'no-local']); for (let i = 0; i < args.length; i++) {   if (!args[i].startsWith('--')) { pos.push(args[i]); continue; }   const name = args[i].slice(2);@@ -167,16 +167,56 @@ if (!state) {   if (!state) { console.error('hero-studio server did not come up on :' + port); process.exit(1); } } +// 2-local. The LOCAL PACKAGE subject source (hero-studio#52): a first hero is+// a PUBLISH PREREQUISITE, so at hero time the page often does not exist on the+// wiki yet -- but everything the studio needs is sitting in the invoking+// directory. When ./package.json (or page.json) matches the posed slug, its+// fields seed the subject and its images become showcase candidates.+// Precedence: local package -> wiki page -> generic mock ("prefer what is+// about to be published over what is already published"); explicit --title/+// --brief flags outrank everything. --no-local opts out.+let localPkg = null;+let localImages = [];+if (!('no-local' in flags)) {+  for (const mf of ['package.json', 'page.json']) {+    try {+      const m = JSON.parse(fs.readFileSync(path.join(process.cwd(), mf), 'utf8'));+      if ((m.slug || m.name || '').replace(/^.*\//, '') === slug) { localPkg = m; break; }+    } catch {}+  }+  if (localPkg) {+    const seen = new Set();+    const addImgs = (dir, excl) => {+      try {+        for (const f of fs.readdirSync(path.join(process.cwd(), dir))) {+          if (!/\.(png|jpe?g|webp)$/i.test(f)) continue;+          if (excl && excl.test(f)) continue;+          const rel = path.join(dir, f);+          if (!seen.has(rel)) { seen.add(rel); localImages.push(path.join(process.cwd(), rel)); }+        }+      } catch {}+    };+    // UI/part screenshots first (the old hero.png is the thing being REPLACED,+    // so it goes last, never first)+    addImgs('screenshots', /^hero\./i);+    addImgs('docs');+    try { if (fs.existsSync(path.join(process.cwd(), slug + '.png'))) localImages.push(path.join(process.cwd(), slug + '.png')); } catch {}+    console.error('local package subject: ' + (localPkg.title || slug) + ' (' + (localImages.length || 'no') + ' local image candidate(s))');+  }+}+ // 2. make sure the studio knows this package, then pose it. // --type accepts a comma list for multi-type pages (hero-studio#19, // e.g. --type app,service): the first entry drives the mode, the full set // renders as a pill group.-const title = flags.title || slug.split('-').map((w) => w[0].toUpperCase() + w.slice(1)).join(' ');-const typeList = String(flags.type || 'app').split(',').map((t) => t.trim()).filter(Boolean);+const localType = localPkg && (localPkg.type === 'hardware' ? 'component' : localPkg.type);+const title = flags.title || (localPkg && localPkg.title) || slug.split('-').map((w) => w[0].toUpperCase() + w.slice(1)).join(' ');+const localBrief = localPkg && (localPkg.brief || localPkg.description);+const typeList = String(flags.type || localType || 'app').split(',').map((t) => t.trim()).filter(Boolean); await jf('api/pages', {   method: 'POST', headers: { 'Content-Type': 'application/json' },   body: JSON.stringify(Object.fromEntries(Object.entries(-    { owner, slug, title, type: typeList[0], types: typeList.length > 1 ? typeList : null, brief: flags.brief },+    { owner, slug, title, type: typeList[0], types: typeList.length > 1 ? typeList : null, brief: flags.brief || localBrief },   ).filter(([, v]) => v))), }); // Every type poses into the app composition (title + subtitle + showcase@@ -196,6 +236,9 @@ await jf('state', {   method: 'POST', headers: { 'Content-Type': 'application/json' },   body: JSON.stringify({     mode: poseMode, page: ref,+    // #53: the legacy component lab's built-in sample part (a real JST MPN)+    // must never ride into a posed session; a posed page's comp slot is inert+    comp: { id: null },     // the gate presentation: verification chrome + a fresh checklist     verify: true, tour: true, marqueePreview: true, guides: false,     confirm: { title: false, subtitle: false, showcase: false, type: false },@@ -215,12 +258,41 @@ await jf('state', {   }), }); -// 2a-live. re-pull title/brief/type from the wiki (#30/#33): the seeded row-// above is only a fallback; the LIVE page wins when it exists.+// 2a-live. re-pull title/brief/type from the wiki (#30/#33). For a page with+// NO local package, the live page wins as before. With a local package the+// PRECEDENCE INVERTS (#52): local fields are what is about to be published,+// so they are re-asserted over the live row after the pull (which still runs+// for the metadata the local manifest lacks: score, stars, author). try {   const lv = await jf('refresh-page?page=' + ref, { method: 'POST' });-  if (lv && lv.ok) console.error('live page: "' + lv.title + '"');+  if (lv && lv.ok) console.error('live page: "' + lv.title + '"' + (localPkg ? ' (local package fields win for this pose)' : '')); } catch {}+if (localPkg) {+  await jf('api/pages', {+    method: 'POST', headers: { 'Content-Type': 'application/json' },+    body: JSON.stringify(Object.fromEntries(Object.entries(+      { owner, slug, title, type: typeList[0], brief: flags.brief || localBrief },+    ).filter(([, v]) => v))),+  });+}+// 2a-showcase. Stage the best local image as the showcase (unless staged work+// is being preserved on a same-page re-pose, or the caller staged their own).+if (localImages.length && !(samePageRepose && !('fresh-showcase' in flags))) {+  const pick = localImages[0];+  const sc = await jf('showcase-file', {+    method: 'POST', headers: { 'Content-Type': 'application/json' },+    body: JSON.stringify({ path: pick }),+  });+  if (sc && sc.ok) {+    console.error('local showcase staged: ' + path.relative(process.cwd(), pick) + (localImages.length > 1 ? ' (alternatives: ' + localImages.slice(1, 4).map((f) => path.relative(process.cwd(), f)).join(', ') + '; swap with `hero-studio showcase --file <path>`)' : ''));+  }+}+// Test hook (#52 verification): everything above ran; skip the human gate.+if ('seed-only' in flags) {+  const st = await jf('state');+  console.log(JSON.stringify({ ok: true, seedOnly: true, page: st.page, resolved: st.resolved, showcaseBytes: (st.showcase && st.showcase.src || '').length, localImages: localImages.map((f) => path.relative(process.cwd(), f)) }));+  process.exit(0);+}  // 2b. fresh change ledger for THIS review session (#20): everything the human // edits at the gate (page fields + pose deltas) accumulates from here and
public/js/app.js+15−1
@@ -463,7 +463,14 @@ function toast(text, kind) { }  /* wire controls: server owns the truth, SSE echoes back */-document.querySelectorAll('#grp-mode .opt').forEach(b => b.addEventListener('click', () => send({ mode: b.dataset.v })));+document.querySelectorAll('#grp-mode .opt').forEach(b => b.addEventListener('click', () => {+  // #53: entering the component design lab with no part re-seeds its built-in+  // sample EXPLICITLY (the lab's photos are of this part); a posed page never+  // carries it implicitly (pose nulls comp.id).+  const payload = { mode: b.dataset.v };+  if (b.dataset.v === 'component' && !(state.comp && state.comp.id)) payload.comp = { id: 'sm02b-srss-tb' };+  send(payload);+})); $('sel-comp').addEventListener('change', e => send({ comp: { id: e.target.value } })); document.querySelectorAll('#grp-imgsrc .opt').forEach(b => b.addEventListener('click', () => send({ comp: { imgSrc: b.dataset.v } }))); document.querySelectorAll('#grp-clayout .opt').forEach(b => b.addEventListener('click', () => send({ comp: { layout: b.dataset.v } })));@@ -827,6 +834,13 @@ function computeLint() {     }   } +  // 4d. the built-in sample part composing a POSED page (#53): a real,+  // unrelated MPN in the slot where the page's own part belongs reads as a+  // bug to the person whose board it is, and shipped unlinted once.+  if (state.mode === 'component' && state.comp && state.comp.id === 'sm02b-srss-tb' && state.page) {+    items.push({ id: 'comp-demo-part', level: 'warn', el: null, msg: 'the composition shows the built-in SAMPLE part (SM02B-SRSS-TB, a JST connector), not ' + state.page + "'s part: set the real part (set {\"comp\":{\"id\":\"<slug>\"}}) or pose the app composition with a showcase image" });+  }+   // 4. title wrap count: a 4-line title is almost never intended.   const tEl = $('el-title');   if (vis(tEl)) {