pixelkiln 0.21.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // src/cli.ts
4
4
  import path26 from "path";
5
5
  import { existsSync as existsSync21 } from "fs";
6
- import { readFile as readFile21 } from "fs/promises";
6
+ import { readFile as readFile22 } from "fs/promises";
7
7
 
8
8
  // src/env.ts
9
9
  import { readFileSync, existsSync } from "fs";
@@ -7790,7 +7790,7 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
7790
7790
  // src/gallery/snapshot.ts
7791
7791
  import { createHash as createHash3 } from "crypto";
7792
7792
  import { existsSync as existsSync16 } from "fs";
7793
- import { stat as stat3 } from "fs/promises";
7793
+ import { readFile as readFile15, stat as stat3 } from "fs/promises";
7794
7794
  import path20 from "path";
7795
7795
 
7796
7796
  // src/workspace.ts
@@ -7980,8 +7980,17 @@ var ManifestEditSchema = z5.discriminatedUnion("action", [
7980
7980
  expectedSha256: HexSha,
7981
7981
  patch: z5.object({
7982
7982
  /** Candidates per generation, written to the provider's own option. */
7983
- candidates: z5.number().int().min(1).max(64)
7984
- }).strict()
7983
+ candidates: z5.number().int().min(1).max(64).optional(),
7984
+ /** Empty clears the style's own value (inherit, or the default). */
7985
+ promptPrefix: z5.string().optional(),
7986
+ promptSuffix: z5.string().optional(),
7987
+ /** `#rrggbb` values; null or empty clears the style's own palette. */
7988
+ palette: z5.array(z5.string().regex(/^#?[0-9a-f]{6}$/i, "expected a six-digit hex colour")).max(256).nullable().optional(),
7989
+ /** Provider view name (PixelLab: low top-down, high top-down, side); empty clears. */
7990
+ view: z5.string().max(64).nullable().optional(),
7991
+ /** pixflux only; null clears the style's own value. */
7992
+ noBackground: z5.boolean().nullable().optional()
7993
+ }).strict().refine((patch) => Object.keys(patch).length > 0, { message: "nothing to change" })
7985
7994
  }).strict()
7986
7995
  ]);
7987
7996
  var ManifestDriftError = class extends Error {
@@ -8019,16 +8028,27 @@ function applyEdit(raw, edit) {
8019
8028
  if (edit.action === "patch-style") {
8020
8029
  const style = raw.styles && Object.hasOwn(raw.styles, edit.styleId) ? raw.styles[edit.styleId] : void 0;
8021
8030
  if (!style) throw new ManifestEditError(`style "${edit.styleId}" is not declared by the manifest`);
8022
- const provider = styleProvider(raw, edit.styleId);
8023
- const option = CANDIDATE_OPTION[provider];
8024
- if (!option) {
8025
- throw new ManifestEditError(
8026
- provider === "pixellab" ? "PixelLab's candidate count follows the generator and size: map and pixflux return one image; a 1dir style returns 4\u201364 for its size" : `provider "${provider}" has no candidate-count option`
8027
- );
8031
+ const { patch: patch2 } = edit;
8032
+ if (patch2.candidates !== void 0) {
8033
+ const provider = styleProvider(raw, edit.styleId);
8034
+ const option = CANDIDATE_OPTION[provider];
8035
+ if (!option) {
8036
+ throw new ManifestEditError(
8037
+ provider === "pixellab" ? "PixelLab's candidate count follows the generator and size: map and pixflux return one image; a 1dir style returns 4\u201364 for its size" : `provider "${provider}" has no candidate-count option`
8038
+ );
8039
+ }
8040
+ const options = { ...style.providerOptions ?? {} };
8041
+ options[provider] = { ...options[provider] ?? {}, [option]: patch2.candidates };
8042
+ style.providerOptions = options;
8028
8043
  }
8029
- const options = { ...style.providerOptions ?? {} };
8030
- options[provider] = { ...options[provider] ?? {}, [option]: edit.patch.candidates };
8031
- style.providerOptions = options;
8044
+ if (patch2.promptPrefix !== void 0) setOrDelete(style, "promptPrefix", patch2.promptPrefix || null);
8045
+ if (patch2.promptSuffix !== void 0) setOrDelete(style, "promptSuffix", patch2.promptSuffix || null);
8046
+ if (patch2.palette !== void 0) {
8047
+ const colors = (patch2.palette ?? []).map((color) => "#" + color.replace(/^#/, "").toLowerCase());
8048
+ setOrDelete(style, "palette", colors.length ? colors : null);
8049
+ }
8050
+ if (patch2.view !== void 0) setOrDelete(style, "view", patch2.view?.trim() || null);
8051
+ if (patch2.noBackground !== void 0) setOrDelete(style, "noBackground", patch2.noBackground);
8032
8052
  return;
8033
8053
  }
8034
8054
  if (edit.action === "add-asset") {
@@ -8253,6 +8273,14 @@ async function buildGallerySnapshot(opts) {
8253
8273
  const media = /* @__PURE__ */ new Map();
8254
8274
  const plan = await buildPlan(specs, lock);
8255
8275
  const items = [];
8276
+ const manifestText = await readFile15(loaded.path, "utf8");
8277
+ const manifestSha256 = sha256(manifestText);
8278
+ let rawStyles = {};
8279
+ try {
8280
+ const raw = JSON.parse(manifestText);
8281
+ if (raw && typeof raw === "object" && raw.styles && typeof raw.styles === "object") rawStyles = raw.styles;
8282
+ } catch {
8283
+ }
8256
8284
  for (const planItem of plan.items) {
8257
8285
  const { spec, key } = planItem;
8258
8286
  const entry = lock.entries[key];
@@ -8387,6 +8415,8 @@ async function buildGallerySnapshot(opts) {
8387
8415
  const counts = new Set(styleItems.filter((item) => item.declared && item.candidates !== null).map((item) => item.candidates));
8388
8416
  const provider = style ? style.provider ?? loaded.manifest.provider : styleItems[0]?.provider ?? loaded.manifest.provider;
8389
8417
  const actionableItems = styleItems.filter((item) => item.declared && (item.state === "missing" || item.state === "stale" || item.state === "failed"));
8418
+ const declaredItems = styleItems.filter((item) => item.declared);
8419
+ const rawStyle = Object.hasOwn(rawStyles, id) ? rawStyles[id] : void 0;
8390
8420
  return {
8391
8421
  id,
8392
8422
  project: null,
@@ -8395,11 +8425,22 @@ async function buildGallerySnapshot(opts) {
8395
8425
  provider,
8396
8426
  generator: style?.generator ?? styleItems[0]?.generator ?? "map",
8397
8427
  outDir: style?.outDir ?? "",
8428
+ promptPrefix: style?.promptPrefix ?? "",
8429
+ promptSuffix: style?.promptSuffix ?? "",
8398
8430
  palette: style?.palette ?? [],
8431
+ view: style?.view ?? null,
8432
+ noBackground: style?.noBackground ?? true,
8399
8433
  quality: Boolean(style?.quality),
8400
8434
  tags: style?.tags ?? [],
8435
+ extends: typeof rawStyle?.extends === "string" ? rawStyle.extends : null,
8436
+ ownFields: rawStyle ? Object.keys(rawStyle).filter((key) => key !== "extends") : [],
8401
8437
  items: styleItems.length,
8402
8438
  spendByUnit: spend,
8439
+ regenerate: {
8440
+ assets: declaredItems.length,
8441
+ cost: declaredItems.reduce((sum, item) => sum + (item.estimatedCost ?? 0), 0),
8442
+ costUnit: declaredItems[0]?.costUnit ?? styleItems[0]?.costUnit ?? "generations"
8443
+ },
8403
8444
  candidates: counts.size === 1 ? [...counts][0] : null,
8404
8445
  candidatesEditable: Boolean(style) && Object.hasOwn(CANDIDATE_OPTION, provider),
8405
8446
  actionable: {
@@ -8420,7 +8461,7 @@ async function buildGallerySnapshot(opts) {
8420
8461
  root,
8421
8462
  provider: loaded.manifest.provider,
8422
8463
  account: null,
8423
- manifestSha256: await sha256File(loaded.path),
8464
+ manifestSha256,
8424
8465
  entries: Object.keys(lock.entries).length,
8425
8466
  items: items.length,
8426
8467
  spendByUnit: spendByUnit(lock),
@@ -8457,6 +8498,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
8457
8498
  const lock = await loadLock(lockPath);
8458
8499
  let projectItems = [];
8459
8500
  let projectStyles = [];
8501
+ let manifestSha256 = null;
8460
8502
  if (!excluded) {
8461
8503
  const specs = await resolveSpecs(loaded, { styles: styleFilter, assets: assetFilter });
8462
8504
  normalizeLockOutputPaths(lock, specs);
@@ -8469,6 +8511,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
8469
8511
  now: opts.now
8470
8512
  });
8471
8513
  for (const [id, file] of build.media) media.set(id, file);
8514
+ manifestSha256 = build.snapshot.project?.manifestSha256 ?? null;
8472
8515
  projectItems = build.snapshot.items.map((item) => ({
8473
8516
  ...item,
8474
8517
  id: `${project.id}:${item.key}`,
@@ -8486,7 +8529,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
8486
8529
  root: loaded.root,
8487
8530
  provider: loaded.manifest.provider,
8488
8531
  account: project.account ?? null,
8489
- manifestSha256: await sha256File(manifestPath),
8532
+ manifestSha256: manifestSha256 ?? sha256(await readFile15(manifestPath, "utf8")),
8490
8533
  entries: Object.keys(lock.entries).length,
8491
8534
  items: projectItems.length,
8492
8535
  spendByUnit: spendByUnit(lock),
@@ -8544,7 +8587,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
8544
8587
  import { spawn as spawn2 } from "child_process";
8545
8588
  import { randomBytes } from "crypto";
8546
8589
  import { createServer as createServer2 } from "http";
8547
- import { readFile as readFile15 } from "fs/promises";
8590
+ import { readFile as readFile16 } from "fs/promises";
8548
8591
 
8549
8592
  // src/gallery/page.ts
8550
8593
  function renderGallery(snapshot, opts = {}) {
@@ -8741,6 +8784,12 @@ function renderGallery(snapshot, opts = {}) {
8741
8784
  .shead .cand { display:inline-flex; align-items:center; gap:6px; font-size:12px; color:var(--dim); }
8742
8785
  .shead .cand input { width:56px; background:var(--panel-deep); border:1px solid var(--line); color:var(--text);
8743
8786
  padding:2px 6px; font:12px/1.4 inherit; border-radius:0; }
8787
+ form.edit.style-form { margin:0 0 14px; }
8788
+ form.edit .blast { padding:8px 10px; border:1px solid var(--line); font-size:12.5px; color:var(--dim); }
8789
+ form.edit .blast.hot { border-color:var(--warn); color:var(--text); }
8790
+ form.edit .blast b { color:var(--warn); font-weight:600; }
8791
+ form.edit .pal { margin-top:4px; }
8792
+ .style .notice { margin:0 0 14px; }
8744
8793
  #jobs { width:min(100%,var(--content)); margin:0 auto; padding:0 22px 12px; display:grid; gap:6px; }
8745
8794
  #jobs:empty { display:none; }
8746
8795
  .job { border:1px solid var(--line); background:var(--panel-deep); padding:8px 12px; display:grid;
@@ -8861,6 +8910,7 @@ function renderGallery(snapshot, opts = {}) {
8861
8910
  </footer>
8862
8911
  <div id="drawer-host"></div>
8863
8912
  <div id="dialog-host"></div>
8913
+ <datalist id="view-options"><option value="low top-down"><option value="high top-down"><option value="side"><option value="sidescroller"></datalist>
8864
8914
  <script>
8865
8915
  const INITIAL = ${data};
8866
8916
  const SESSION = ${session};
@@ -9192,7 +9242,8 @@ function renderHeader() {
9192
9242
  t.lastChild.append(el('b', null, n), document.createTextNode(' lock ' + (n === 1 ? 'entry' : 'entries')));
9193
9243
  const declared = snap.items.filter((i) => i.declared).length;
9194
9244
  t.append(el('span', null, declared + ' declared by ' + (snap.workspace ? 'a manifest' : 'the manifest')));
9195
- t.append(el('span', null, fmtSpend(snap.totals.spendByUnit) + ' recorded'));
9245
+ const recorded = fmtSpend(snap.totals.spendByUnit);
9246
+ t.append(el('span', null, recorded === 'no spend recorded' ? recorded : recorded + ' recorded'));
9196
9247
  if (snap.filter.styles.length || snap.filter.assets.length) {
9197
9248
  t.append(el('span', 'state-warn', 'filtered: ' +
9198
9249
  [snap.filter.styles.length ? '--style ' + snap.filter.styles.join(',') : '',
@@ -9386,7 +9437,12 @@ function renderMain(items) {
9386
9437
  g.onclick = () => generateDialog(snap.items.filter((i) => i.project === s.project && s.actionable.keys.includes(i.key)), { project: s.project });
9387
9438
  tools.append(g);
9388
9439
  }
9440
+ const styleKey = 'style:' + (s.project || '') + ':' + s.id;
9389
9441
  if (EDITABLE && s.outDir) {
9442
+ const es = el('button', null, ui.editing === styleKey ? 'Cancel' : 'Edit style');
9443
+ es.type = 'button';
9444
+ es.onclick = () => { ui.editing = ui.editing === styleKey ? null : styleKey; ui.notice = null; render(); };
9445
+ tools.append(es);
9390
9446
  const add = el('button', 'add', ui.editing === addKey ? 'Cancel' : '+ Add asset');
9391
9447
  add.type = 'button';
9392
9448
  add.onclick = () => { ui.editing = ui.editing === addKey ? null : addKey; render(); };
@@ -9394,6 +9450,8 @@ function renderMain(items) {
9394
9450
  }
9395
9451
  if (tools.childNodes.length) head.append(tools);
9396
9452
  wrap.append(head);
9453
+ if (ui.notice && ui.notice.id === styleKey) wrap.append(el('div', 'notice', ui.notice.text));
9454
+ if (ui.editing === styleKey) wrap.append(styleForm(s));
9397
9455
  if (ui.editing === addKey) wrap.append(addAssetForm(s));
9398
9456
  }
9399
9457
  const grid = el('div', 'grid');
@@ -9627,6 +9685,156 @@ function openCompare() {
9627
9685
 
9628
9686
  // ---- editing (only when the server minted a session) ---------------------
9629
9687
 
9688
+ // Styles a parent edit reaches: every descendant (via extends) that does not
9689
+ // declare the field itself. Child-wins means an omitted field is inherited,
9690
+ // so the count follows the manifest's own rule rather than guessing.
9691
+ function inheritorsOf(style, field) {
9692
+ const byId = new Map(snap.styles.filter((x) => x.project === style.project).map((x) => [x.id, x]));
9693
+ const out = [];
9694
+ for (const other of byId.values()) {
9695
+ if (other.id === style.id) continue;
9696
+ let cur = other, blocked = false, hops = 0;
9697
+ while (cur && cur.id !== style.id && hops++ < 32) {
9698
+ if (cur.ownFields.includes(field)) { blocked = true; break; }
9699
+ cur = cur.extends ? byId.get(cur.extends) : null;
9700
+ }
9701
+ if (cur && cur.id === style.id && !blocked) out.push(other.id);
9702
+ }
9703
+ return out;
9704
+ }
9705
+ // noBackground reaches the request only for pixflux and non-PixelLab
9706
+ // providers; elsewhere a change is recorded but alters nothing.
9707
+ const fieldReaches = (field, style) =>
9708
+ field !== 'noBackground' || style.generator === 'pixflux' || style.provider !== 'pixellab';
9709
+ function blastRadius(style, fields) {
9710
+ const byId = new Map(snap.styles.filter((x) => x.project === style.project).map((x) => [x.id, x]));
9711
+ const styleIds = new Set();
9712
+ for (const field of fields) {
9713
+ for (const id of [style.id, ...inheritorsOf(style, field)]) {
9714
+ if (fieldReaches(field, byId.get(id) || style)) styleIds.add(id);
9715
+ }
9716
+ }
9717
+ const affected = snap.items.filter((i) => i.project === style.project && i.declared && styleIds.has(i.styleId));
9718
+ const units = new Map();
9719
+ for (const i of affected) if (i.estimatedCost !== null) units.set(i.costUnit, (units.get(i.costUnit) || 0) + i.estimatedCost);
9720
+ return {
9721
+ styles: [...styleIds],
9722
+ assets: affected.length,
9723
+ cost: [...units].map(([unit, n]) => fmtCost(unit, Math.round(n * 100) / 100)).join(' + '),
9724
+ };
9725
+ }
9726
+ const parsePalette = (value) => value.split(/[s,]+/).map((c) => c.trim()).filter(Boolean)
9727
+ .map((c) => '#' + c.replace(/^#/, '').toLowerCase());
9728
+ const paletteValid = (colors) => colors.every((c) => /^#[0-9a-f]{6}$/.test(c));
9729
+
9730
+ function styleForm(style) {
9731
+ const pr = snap.workspace ? snap.workspace.projects.find((x) => x.id === style.project) : snap.project;
9732
+ const own = (field) => style.ownFields.includes(field);
9733
+ const provenance = (field) => own(field) ? 'set on this style'
9734
+ : style.extends ? 'inherited from ' + style.extends + ' \u2014 saving sets a value on this style itself; ' + style.extends + ' is unchanged'
9735
+ : 'default \u2014 saving sets a value on this style itself';
9736
+ const form = el('form', 'edit style-form');
9737
+ form.append(el('h3', null, 'Edit style ' + style.id));
9738
+ const prefix = el('input'); prefix.type = 'text'; prefix.value = style.promptPrefix; prefix.placeholder = 'none';
9739
+ const suffix = el('input'); suffix.type = 'text'; suffix.value = style.promptSuffix; suffix.placeholder = 'none';
9740
+ const palette = el('input'); palette.type = 'text'; palette.value = style.palette.join(', '); palette.placeholder = '#rrggbb, #rrggbb \u2014 leave empty for no forced palette';
9741
+ const view = el('input'); view.type = 'text'; view.value = style.view || ''; view.placeholder = 'provider default';
9742
+ view.setAttribute('list', 'view-options');
9743
+ const noBg = el('select');
9744
+ noBg.append(new Option(style.extends ? 'inherit from ' + style.extends : 'default (on)', 'default'), new Option('on \u2014 strip the generated background', 'on'), new Option('off \u2014 keep the background (scenes, banners)', 'off'));
9745
+ noBg.value = own('noBackground') ? (style.noBackground ? 'on' : 'off') : 'default';
9746
+ const swatches = el('div', 'pal');
9747
+ const drawSwatches = () => {
9748
+ swatches.textContent = '';
9749
+ for (const c of parsePalette(palette.value)) { const i = el('i'); i.style.background = c; i.title = c; swatches.append(i); }
9750
+ };
9751
+ drawSwatches();
9752
+ form.append(
9753
+ field('prompt prefix', prefix, 'Prepended to every asset prompt in this style. ' + provenance('promptPrefix')),
9754
+ field('prompt suffix', suffix, 'Appended to every asset prompt in this style. ' + provenance('promptSuffix')),
9755
+ field('palette', palette, 'Forced colours where the provider supports them (pixflux). ' + provenance('palette')),
9756
+ swatches,
9757
+ );
9758
+ const row = el('div', 'row');
9759
+ row.append(
9760
+ field('view', view, 'PixelLab accepts low top-down, high top-down, side; Retro Diffusion reads sidescroller. ' + provenance('view')),
9761
+ field('background', noBg, (fieldReaches('noBackground', style) ? 'Sent for this style. ' : 'Not sent for ' + style.provider + ' ' + style.generator + '; recorded only. ') + provenance('noBackground')),
9762
+ );
9763
+ form.append(row);
9764
+ const blast = el('div', 'blast');
9765
+ const changed = () => {
9766
+ const out = [];
9767
+ if (prefix.value !== style.promptPrefix) out.push('promptPrefix');
9768
+ if (suffix.value !== style.promptSuffix) out.push('promptSuffix');
9769
+ if (parsePalette(palette.value).join(',') !== style.palette.map((c) => c.toLowerCase()).join(',')) out.push('palette');
9770
+ if (view.value.trim() !== (style.view || '')) out.push('view');
9771
+ const bgNow = own('noBackground') ? (style.noBackground ? 'on' : 'off') : 'default';
9772
+ if (noBg.value !== bgNow) out.push('noBackground');
9773
+ return out;
9774
+ };
9775
+ const updateBlast = () => {
9776
+ const fields = changed();
9777
+ drawSwatches();
9778
+ if (!fields.length) { blast.className = 'blast'; blast.textContent = 'No changes yet. A style change alters the request for every asset that uses it.'; return; }
9779
+ const r = blastRadius(style, fields);
9780
+ if (!r.assets) {
9781
+ blast.className = 'blast';
9782
+ blast.textContent = 'Recorded in the manifest only: nothing in ' + style.id + ' sends this field, so no request changes.';
9783
+ return;
9784
+ }
9785
+ blast.className = 'blast hot';
9786
+ blast.textContent = '';
9787
+ const others = r.styles.filter((id) => id !== style.id);
9788
+ blast.append(el('b', null, 'Affects ' + r.assets + (r.assets === 1 ? ' asset' : ' assets')));
9789
+ blast.append(document.createTextNode(others.length
9790
+ ? ' in ' + style.id + ' and ' + others.length + (others.length === 1 ? ' style that inherits it (' : ' styles that inherit it (') + others.join(', ') + ').'
9791
+ : ' in ' + style.id + '.'));
9792
+ blast.append(document.createTextNode(' Generated ones become stale; regenerating all of them is about ' + (r.cost || 'nothing') + '. Nothing is spent until you generate.'));
9793
+ };
9794
+ for (const input of [prefix, suffix, palette, view]) input.addEventListener('input', updateBlast);
9795
+ noBg.addEventListener('change', updateBlast);
9796
+ updateBlast();
9797
+ form.append(blast);
9798
+ const actions = el('div', 'actions');
9799
+ const save = el('button', 'primary', 'Save to manifest'); save.type = 'submit';
9800
+ const cancel = el('button', null, 'Cancel'); cancel.type = 'button'; cancel.onclick = () => { ui.editing = null; render(); };
9801
+ const msg = el('span', 'msg');
9802
+ actions.append(save, cancel, msg);
9803
+ form.append(actions);
9804
+ form.onsubmit = async (e) => {
9805
+ e.preventDefault();
9806
+ const fields = changed();
9807
+ if (!fields.length) { ui.editing = null; render(); return; }
9808
+ const colors = parsePalette(palette.value);
9809
+ if (fields.includes('palette') && !paletteValid(colors)) { msg.className = 'msg bad'; msg.textContent = 'palette must be six-digit hex colours'; return; }
9810
+ const patch = {};
9811
+ if (fields.includes('promptPrefix')) patch.promptPrefix = prefix.value;
9812
+ if (fields.includes('promptSuffix')) patch.promptSuffix = suffix.value;
9813
+ if (fields.includes('palette')) patch.palette = colors;
9814
+ if (fields.includes('view')) patch.view = view.value.trim();
9815
+ if (fields.includes('noBackground')) patch.noBackground = noBg.value === 'default' ? null : noBg.value === 'on';
9816
+ save.disabled = true; msg.className = 'msg'; msg.textContent = 'saving\u2026';
9817
+ const r = blastRadius(style, fields);
9818
+ try {
9819
+ const body = { action: 'patch-style', styleId: style.id, expectedSha256: pr.manifestSha256, patch };
9820
+ if (style.project) body.project = style.project;
9821
+ snap = await postEdit(body);
9822
+ ui.editing = null;
9823
+ ui.notice = { id: 'style:' + (style.project || '') + ':' + style.id,
9824
+ text: r.assets
9825
+ ? 'Saved. The request for ' + r.assets + (r.assets === 1 ? ' asset' : ' assets') + ' changed' + (r.cost ? ' \u2014 about ' + r.cost + ' to regenerate.' : '.') + ' Nothing is spent until you generate.'
9826
+ : 'Saved. Recorded in the manifest; no request changed.' };
9827
+ render();
9828
+ } catch (err) {
9829
+ save.disabled = false;
9830
+ msg.className = 'msg bad';
9831
+ msg.textContent = err.message + (err.status === 409 ? ' \u2014 press Refresh.' : '');
9832
+ }
9833
+ };
9834
+ setTimeout(() => prefix.focus(), 0);
9835
+ return form;
9836
+ }
9837
+
9630
9838
  function addAssetForm(style) {
9631
9839
  const pr = snap.workspace ? snap.workspace.projects.find((x) => x.id === style.project) : snap.project;
9632
9840
  const siblings = snap.styles.filter((x) => x.project === style.project);
@@ -10352,7 +10560,7 @@ async function serveGallery(opts) {
10352
10560
  const asset = reviewAssets.get(jobId)?.get(url2.pathname);
10353
10561
  if (!asset) return fail(404, "no such review asset");
10354
10562
  try {
10355
- const bytes = await readFile15(asset.path);
10563
+ const bytes = await readFile16(asset.path);
10356
10564
  res.writeHead(200, {
10357
10565
  "Content-Type": asset.contentType,
10358
10566
  "Cache-Control": "no-store",
@@ -10401,7 +10609,7 @@ async function serveGallery(opts) {
10401
10609
  if (!asset) return fail(404, "no such gallery media");
10402
10610
  let bytes;
10403
10611
  try {
10404
- bytes = await readFile15(asset.path);
10612
+ bytes = await readFile16(asset.path);
10405
10613
  } catch {
10406
10614
  return fail(404, "gallery media is no longer on disk; refresh the page");
10407
10615
  }
@@ -10691,7 +10899,7 @@ function createGenerateHandlers(opts) {
10691
10899
  }
10692
10900
 
10693
10901
  // src/pipeline/salvage.ts
10694
- import { readFile as readFile16 } from "fs/promises";
10902
+ import { readFile as readFile17 } from "fs/promises";
10695
10903
  import { existsSync as existsSync17 } from "fs";
10696
10904
  import path21 from "path";
10697
10905
  function providerClaimId(provider, objectId) {
@@ -10703,7 +10911,7 @@ async function loadClaims(lockPaths, opts = {}) {
10703
10911
  if (!existsSync17(p)) throw new Error(`Claim lockfile not found: ${p}`);
10704
10912
  let parsed;
10705
10913
  try {
10706
- parsed = parseLock(JSON.parse(await readFile16(p, "utf8")));
10914
+ parsed = parseLock(JSON.parse(await readFile17(p, "utf8")));
10707
10915
  } catch {
10708
10916
  throw new Error(`Claim lockfile is malformed: ${p}`);
10709
10917
  }
@@ -10984,7 +11192,7 @@ async function workspaceStatus(ws, dir) {
10984
11192
 
10985
11193
  // src/pipeline/cache-health.ts
10986
11194
  import { existsSync as existsSync18 } from "fs";
10987
- import { readFile as readFile17, readdir as readdir2, rm as rm8 } from "fs/promises";
11195
+ import { readFile as readFile18, readdir as readdir2, rm as rm8 } from "fs/promises";
10988
11196
  import path22 from "path";
10989
11197
  async function inspectCaches(lock, lockPath, options = {}) {
10990
11198
  if (options.prune && !existsSync18(lockPath)) {
@@ -11014,7 +11222,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
11014
11222
  await saveCache(remotePath, { version: 1, hashes: {} });
11015
11223
  removed.resetRemoteHashCache = true;
11016
11224
  } else if (remoteHashes.invalidIds.length) {
11017
- const cache = parseCache(JSON.parse(await readFile17(remotePath, "utf8")));
11225
+ const cache = parseCache(JSON.parse(await readFile18(remotePath, "utf8")));
11018
11226
  for (const id of remoteHashes.invalidIds) delete cache.hashes[id];
11019
11227
  removed.remoteHashEntries = remoteHashes.invalidIds.length;
11020
11228
  await saveCache(remotePath, cache);
@@ -11058,7 +11266,7 @@ async function inspectContentCache(contentDir, referenced) {
11058
11266
  const expected = mediaType ? entry.name.slice(0, -4) : "";
11059
11267
  let bytes;
11060
11268
  try {
11061
- bytes = await readFile17(file);
11269
+ bytes = await readFile18(file);
11062
11270
  report.bytes += bytes.length;
11063
11271
  } catch (err) {
11064
11272
  report.invalid.push({
@@ -11105,7 +11313,7 @@ async function inspectRemoteHashCache(remotePath) {
11105
11313
  if (!report.exists) return report;
11106
11314
  let cache;
11107
11315
  try {
11108
- cache = parseCache(JSON.parse(await readFile17(remotePath, "utf8")));
11316
+ cache = parseCache(JSON.parse(await readFile18(remotePath, "utf8")));
11109
11317
  } catch (err) {
11110
11318
  report.error = err instanceof Error ? err.message : String(err);
11111
11319
  return report;
@@ -11376,7 +11584,7 @@ function isRecord2(value) {
11376
11584
  }
11377
11585
 
11378
11586
  // src/pick/salvage-server.ts
11379
- import { mkdir as mkdir6, writeFile as writeFile9, readFile as readFile18 } from "fs/promises";
11587
+ import { mkdir as mkdir6, writeFile as writeFile9, readFile as readFile19 } from "fs/promises";
11380
11588
  import path23 from "path";
11381
11589
 
11382
11590
  // src/pick/salvage-sheet.ts
@@ -11629,7 +11837,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
11629
11837
  }
11630
11838
  }
11631
11839
  await applyTags(provider, decisions, existingTags, { onProgress: log2 });
11632
- const raw = JSON.parse(await readFile18(ctx.manifestPath, "utf8"));
11840
+ const raw = JSON.parse(await readFile19(ctx.manifestPath, "utf8"));
11633
11841
  for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
11634
11842
  await writeFile9(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
11635
11843
  await saveLock(ctx.lockPath, ctx.lock);
@@ -11641,7 +11849,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
11641
11849
  // src/recipes.ts
11642
11850
  import path24 from "path";
11643
11851
  import { existsSync as existsSync19 } from "fs";
11644
- import { readdir as readdir3, readFile as readFile19 } from "fs/promises";
11852
+ import { readdir as readdir3, readFile as readFile20 } from "fs/promises";
11645
11853
  import { fileURLToPath } from "url";
11646
11854
  import { z as z7 } from "zod";
11647
11855
  var SHA256_RE = /^[0-9a-f]{64}$/;
@@ -11829,7 +12037,7 @@ async function readRecipeFile(recipePath, bundled) {
11829
12037
  const absolute = path24.resolve(recipePath);
11830
12038
  let raw;
11831
12039
  try {
11832
- raw = JSON.parse(await readFile19(absolute, "utf8"));
12040
+ raw = JSON.parse(await readFile20(absolute, "utf8"));
11833
12041
  } catch (error) {
11834
12042
  throw new Error(`Could not read recipe ${absolute}: ${message4(error)}`, { cause: error });
11835
12043
  }
@@ -11971,12 +12179,12 @@ async function installRecipe(target, options = {}) {
11971
12179
  ];
11972
12180
  const files = await Promise.all(sources.map(async (file) => ({
11973
12181
  path: path24.join(destination, ...file.path.split("/")),
11974
- data: await readFile19(file.source)
12182
+ data: await readFile20(file.source)
11975
12183
  })));
11976
12184
  if (!options.force) {
11977
12185
  for (const file of files) {
11978
12186
  if (!existsSync19(file.path)) continue;
11979
- const current = await readFile19(file.path);
12187
+ const current = await readFile20(file.path);
11980
12188
  if (!current.equals(file.data)) {
11981
12189
  throw new Error(
11982
12190
  `Recipe destination has local changes: ${file.path}. Choose another --out or pass --force to replace declared recipe files.`
@@ -12001,7 +12209,7 @@ async function installRecipe(target, options = {}) {
12001
12209
  // src/pipeline/quality-regression.ts
12002
12210
  import path25 from "path";
12003
12211
  import { existsSync as existsSync20 } from "fs";
12004
- import { readFile as readFile20 } from "fs/promises";
12212
+ import { readFile as readFile21 } from "fs/promises";
12005
12213
  import { z as z8 } from "zod";
12006
12214
  var SHA256_RE2 = /^[0-9a-f]{64}$/;
12007
12215
  var HEX_RE = /^#[0-9a-f]{6}$/;
@@ -12157,7 +12365,7 @@ async function measureImageQuality(file) {
12157
12365
  const absolute = path25.resolve(file);
12158
12366
  let bytes;
12159
12367
  try {
12160
- bytes = await readFile20(absolute);
12368
+ bytes = await readFile21(absolute);
12161
12369
  } catch (error) {
12162
12370
  throw new Error(`Cannot read quality image ${absolute}: ${message5(error)}`, { cause: error });
12163
12371
  }
@@ -12244,7 +12452,7 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
12244
12452
  });
12245
12453
  const data = Buffer.from(JSON.stringify(baseline, null, 2) + "\n");
12246
12454
  if (existsSync20(absolute) && !options.force) {
12247
- const current = await readFile20(absolute);
12455
+ const current = await readFile21(absolute);
12248
12456
  if (!current.equals(data)) {
12249
12457
  throw new Error(`Quality baseline already exists with different content: ${absolute}. Pass --force to replace it.`);
12250
12458
  }
@@ -12256,7 +12464,7 @@ async function readQualityBaseline(baselinePath) {
12256
12464
  const absolute = path25.resolve(baselinePath);
12257
12465
  let raw;
12258
12466
  try {
12259
- raw = JSON.parse(await readFile20(absolute, "utf8"));
12467
+ raw = JSON.parse(await readFile21(absolute, "utf8"));
12260
12468
  } catch (error) {
12261
12469
  throw new Error(`Could not read quality baseline ${absolute}: ${message5(error)}`, { cause: error });
12262
12470
  }
@@ -13054,7 +13262,7 @@ async function main() {
13054
13262
  }
13055
13263
  if (args.command === "--version" || args.command === "-v") {
13056
13264
  const pkg = JSON.parse(
13057
- await readFile21(new URL("../package.json", import.meta.url), "utf8")
13265
+ await readFile22(new URL("../package.json", import.meta.url), "utf8")
13058
13266
  );
13059
13267
  log(`${pkg.name} ${pkg.version}`);
13060
13268
  return;
@@ -13065,7 +13273,7 @@ async function main() {
13065
13273
  if (!args.out) throw new Error("quality snapshot needs --out <pixelkiln.quality.json>.");
13066
13274
  let raw;
13067
13275
  try {
13068
- raw = JSON.parse(await readFile21(path26.resolve(args.inputs), "utf8"));
13276
+ raw = JSON.parse(await readFile22(path26.resolve(args.inputs), "utf8"));
13069
13277
  } catch (error) {
13070
13278
  throw new Error(
13071
13279
  `Could not read quality inputs ${path26.resolve(args.inputs)}: ${error instanceof Error ? error.message : String(error)}`,
@@ -13356,7 +13564,7 @@ async function main() {
13356
13564
  if (args.primaryOnly || args.outputRoles.length) {
13357
13565
  throw new Error("--primary-only and --output-role require manifest-driven pack");
13358
13566
  }
13359
- const raw = JSON.parse(await readFile21(path26.resolve(args.inputs), "utf8"));
13567
+ const raw = JSON.parse(await readFile22(path26.resolve(args.inputs), "utf8"));
13360
13568
  const inputs = resolvePackInputs(raw, args.inputs);
13361
13569
  const { png, atlas, skipped, sources } = packSprites(inputs, { columns: args.columns });
13362
13570
  const base = path26.resolve(args.out.replace(/\.png$/, ""));