pixelkiln 0.21.0 → 0.22.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,13 @@ 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
+ }).strict().refine((patch) => Object.keys(patch).length > 0, { message: "nothing to change" })
7985
7990
  }).strict()
7986
7991
  ]);
7987
7992
  var ManifestDriftError = class extends Error {
@@ -8019,16 +8024,25 @@ function applyEdit(raw, edit) {
8019
8024
  if (edit.action === "patch-style") {
8020
8025
  const style = raw.styles && Object.hasOwn(raw.styles, edit.styleId) ? raw.styles[edit.styleId] : void 0;
8021
8026
  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
- );
8027
+ const { patch: patch2 } = edit;
8028
+ if (patch2.candidates !== void 0) {
8029
+ const provider = styleProvider(raw, edit.styleId);
8030
+ const option = CANDIDATE_OPTION[provider];
8031
+ if (!option) {
8032
+ throw new ManifestEditError(
8033
+ 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`
8034
+ );
8035
+ }
8036
+ const options = { ...style.providerOptions ?? {} };
8037
+ options[provider] = { ...options[provider] ?? {}, [option]: patch2.candidates };
8038
+ style.providerOptions = options;
8039
+ }
8040
+ if (patch2.promptPrefix !== void 0) setOrDelete(style, "promptPrefix", patch2.promptPrefix || null);
8041
+ if (patch2.promptSuffix !== void 0) setOrDelete(style, "promptSuffix", patch2.promptSuffix || null);
8042
+ if (patch2.palette !== void 0) {
8043
+ const colors = (patch2.palette ?? []).map((color) => "#" + color.replace(/^#/, "").toLowerCase());
8044
+ setOrDelete(style, "palette", colors.length ? colors : null);
8028
8045
  }
8029
- const options = { ...style.providerOptions ?? {} };
8030
- options[provider] = { ...options[provider] ?? {}, [option]: edit.patch.candidates };
8031
- style.providerOptions = options;
8032
8046
  return;
8033
8047
  }
8034
8048
  if (edit.action === "add-asset") {
@@ -8253,6 +8267,14 @@ async function buildGallerySnapshot(opts) {
8253
8267
  const media = /* @__PURE__ */ new Map();
8254
8268
  const plan = await buildPlan(specs, lock);
8255
8269
  const items = [];
8270
+ const manifestText = await readFile15(loaded.path, "utf8");
8271
+ const manifestSha256 = sha256(manifestText);
8272
+ let rawStyles = {};
8273
+ try {
8274
+ const raw = JSON.parse(manifestText);
8275
+ if (raw && typeof raw === "object" && raw.styles && typeof raw.styles === "object") rawStyles = raw.styles;
8276
+ } catch {
8277
+ }
8256
8278
  for (const planItem of plan.items) {
8257
8279
  const { spec, key } = planItem;
8258
8280
  const entry = lock.entries[key];
@@ -8387,6 +8409,8 @@ async function buildGallerySnapshot(opts) {
8387
8409
  const counts = new Set(styleItems.filter((item) => item.declared && item.candidates !== null).map((item) => item.candidates));
8388
8410
  const provider = style ? style.provider ?? loaded.manifest.provider : styleItems[0]?.provider ?? loaded.manifest.provider;
8389
8411
  const actionableItems = styleItems.filter((item) => item.declared && (item.state === "missing" || item.state === "stale" || item.state === "failed"));
8412
+ const declaredItems = styleItems.filter((item) => item.declared);
8413
+ const rawStyle = Object.hasOwn(rawStyles, id) ? rawStyles[id] : void 0;
8390
8414
  return {
8391
8415
  id,
8392
8416
  project: null,
@@ -8395,11 +8419,20 @@ async function buildGallerySnapshot(opts) {
8395
8419
  provider,
8396
8420
  generator: style?.generator ?? styleItems[0]?.generator ?? "map",
8397
8421
  outDir: style?.outDir ?? "",
8422
+ promptPrefix: style?.promptPrefix ?? "",
8423
+ promptSuffix: style?.promptSuffix ?? "",
8398
8424
  palette: style?.palette ?? [],
8399
8425
  quality: Boolean(style?.quality),
8400
8426
  tags: style?.tags ?? [],
8427
+ extends: typeof rawStyle?.extends === "string" ? rawStyle.extends : null,
8428
+ ownFields: rawStyle ? Object.keys(rawStyle).filter((key) => key !== "extends") : [],
8401
8429
  items: styleItems.length,
8402
8430
  spendByUnit: spend,
8431
+ regenerate: {
8432
+ assets: declaredItems.length,
8433
+ cost: declaredItems.reduce((sum, item) => sum + (item.estimatedCost ?? 0), 0),
8434
+ costUnit: declaredItems[0]?.costUnit ?? styleItems[0]?.costUnit ?? "generations"
8435
+ },
8403
8436
  candidates: counts.size === 1 ? [...counts][0] : null,
8404
8437
  candidatesEditable: Boolean(style) && Object.hasOwn(CANDIDATE_OPTION, provider),
8405
8438
  actionable: {
@@ -8420,7 +8453,7 @@ async function buildGallerySnapshot(opts) {
8420
8453
  root,
8421
8454
  provider: loaded.manifest.provider,
8422
8455
  account: null,
8423
- manifestSha256: await sha256File(loaded.path),
8456
+ manifestSha256,
8424
8457
  entries: Object.keys(lock.entries).length,
8425
8458
  items: items.length,
8426
8459
  spendByUnit: spendByUnit(lock),
@@ -8457,6 +8490,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
8457
8490
  const lock = await loadLock(lockPath);
8458
8491
  let projectItems = [];
8459
8492
  let projectStyles = [];
8493
+ let manifestSha256 = null;
8460
8494
  if (!excluded) {
8461
8495
  const specs = await resolveSpecs(loaded, { styles: styleFilter, assets: assetFilter });
8462
8496
  normalizeLockOutputPaths(lock, specs);
@@ -8469,6 +8503,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
8469
8503
  now: opts.now
8470
8504
  });
8471
8505
  for (const [id, file] of build.media) media.set(id, file);
8506
+ manifestSha256 = build.snapshot.project?.manifestSha256 ?? null;
8472
8507
  projectItems = build.snapshot.items.map((item) => ({
8473
8508
  ...item,
8474
8509
  id: `${project.id}:${item.key}`,
@@ -8486,7 +8521,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
8486
8521
  root: loaded.root,
8487
8522
  provider: loaded.manifest.provider,
8488
8523
  account: project.account ?? null,
8489
- manifestSha256: await sha256File(manifestPath),
8524
+ manifestSha256: manifestSha256 ?? sha256(await readFile15(manifestPath, "utf8")),
8490
8525
  entries: Object.keys(lock.entries).length,
8491
8526
  items: projectItems.length,
8492
8527
  spendByUnit: spendByUnit(lock),
@@ -8544,7 +8579,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
8544
8579
  import { spawn as spawn2 } from "child_process";
8545
8580
  import { randomBytes } from "crypto";
8546
8581
  import { createServer as createServer2 } from "http";
8547
- import { readFile as readFile15 } from "fs/promises";
8582
+ import { readFile as readFile16 } from "fs/promises";
8548
8583
 
8549
8584
  // src/gallery/page.ts
8550
8585
  function renderGallery(snapshot, opts = {}) {
@@ -8741,6 +8776,12 @@ function renderGallery(snapshot, opts = {}) {
8741
8776
  .shead .cand { display:inline-flex; align-items:center; gap:6px; font-size:12px; color:var(--dim); }
8742
8777
  .shead .cand input { width:56px; background:var(--panel-deep); border:1px solid var(--line); color:var(--text);
8743
8778
  padding:2px 6px; font:12px/1.4 inherit; border-radius:0; }
8779
+ form.edit.style-form { margin:0 0 14px; }
8780
+ form.edit .blast { padding:8px 10px; border:1px solid var(--line); font-size:12.5px; color:var(--dim); }
8781
+ form.edit .blast.hot { border-color:var(--warn); color:var(--text); }
8782
+ form.edit .blast b { color:var(--warn); font-weight:600; }
8783
+ form.edit .pal { margin-top:4px; }
8784
+ .style .notice { margin:0 0 14px; }
8744
8785
  #jobs { width:min(100%,var(--content)); margin:0 auto; padding:0 22px 12px; display:grid; gap:6px; }
8745
8786
  #jobs:empty { display:none; }
8746
8787
  .job { border:1px solid var(--line); background:var(--panel-deep); padding:8px 12px; display:grid;
@@ -9192,7 +9233,8 @@ function renderHeader() {
9192
9233
  t.lastChild.append(el('b', null, n), document.createTextNode(' lock ' + (n === 1 ? 'entry' : 'entries')));
9193
9234
  const declared = snap.items.filter((i) => i.declared).length;
9194
9235
  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'));
9236
+ const recorded = fmtSpend(snap.totals.spendByUnit);
9237
+ t.append(el('span', null, recorded === 'no spend recorded' ? recorded : recorded + ' recorded'));
9196
9238
  if (snap.filter.styles.length || snap.filter.assets.length) {
9197
9239
  t.append(el('span', 'state-warn', 'filtered: ' +
9198
9240
  [snap.filter.styles.length ? '--style ' + snap.filter.styles.join(',') : '',
@@ -9386,7 +9428,12 @@ function renderMain(items) {
9386
9428
  g.onclick = () => generateDialog(snap.items.filter((i) => i.project === s.project && s.actionable.keys.includes(i.key)), { project: s.project });
9387
9429
  tools.append(g);
9388
9430
  }
9431
+ const styleKey = 'style:' + (s.project || '') + ':' + s.id;
9389
9432
  if (EDITABLE && s.outDir) {
9433
+ const es = el('button', null, ui.editing === styleKey ? 'Cancel' : 'Edit style');
9434
+ es.type = 'button';
9435
+ es.onclick = () => { ui.editing = ui.editing === styleKey ? null : styleKey; ui.notice = null; render(); };
9436
+ tools.append(es);
9390
9437
  const add = el('button', 'add', ui.editing === addKey ? 'Cancel' : '+ Add asset');
9391
9438
  add.type = 'button';
9392
9439
  add.onclick = () => { ui.editing = ui.editing === addKey ? null : addKey; render(); };
@@ -9394,6 +9441,8 @@ function renderMain(items) {
9394
9441
  }
9395
9442
  if (tools.childNodes.length) head.append(tools);
9396
9443
  wrap.append(head);
9444
+ if (ui.notice && ui.notice.id === styleKey) wrap.append(el('div', 'notice', ui.notice.text));
9445
+ if (ui.editing === styleKey) wrap.append(styleForm(s));
9397
9446
  if (ui.editing === addKey) wrap.append(addAssetForm(s));
9398
9447
  }
9399
9448
  const grid = el('div', 'grid');
@@ -9627,6 +9676,123 @@ function openCompare() {
9627
9676
 
9628
9677
  // ---- editing (only when the server minted a session) ---------------------
9629
9678
 
9679
+ // Styles a parent edit reaches: every descendant (via extends) that does not
9680
+ // declare the field itself. Child-wins means an omitted field is inherited,
9681
+ // so the count follows the manifest's own rule rather than guessing.
9682
+ function inheritorsOf(style, field) {
9683
+ const byId = new Map(snap.styles.filter((x) => x.project === style.project).map((x) => [x.id, x]));
9684
+ const out = [];
9685
+ for (const other of byId.values()) {
9686
+ if (other.id === style.id) continue;
9687
+ let cur = other, blocked = false, hops = 0;
9688
+ while (cur && cur.id !== style.id && hops++ < 32) {
9689
+ if (cur.ownFields.includes(field)) { blocked = true; break; }
9690
+ cur = cur.extends ? byId.get(cur.extends) : null;
9691
+ }
9692
+ if (cur && cur.id === style.id && !blocked) out.push(other.id);
9693
+ }
9694
+ return out;
9695
+ }
9696
+ function blastRadius(style, fields) {
9697
+ const styleIds = new Set([style.id]);
9698
+ for (const field of fields) for (const id of inheritorsOf(style, field)) styleIds.add(id);
9699
+ const affected = snap.items.filter((i) => i.project === style.project && i.declared && styleIds.has(i.styleId));
9700
+ const units = new Map();
9701
+ for (const i of affected) if (i.estimatedCost !== null) units.set(i.costUnit, (units.get(i.costUnit) || 0) + i.estimatedCost);
9702
+ return {
9703
+ styles: [...styleIds],
9704
+ assets: affected.length,
9705
+ cost: [...units].map(([unit, n]) => fmtCost(unit, Math.round(n * 100) / 100)).join(' + '),
9706
+ };
9707
+ }
9708
+ const parsePalette = (value) => value.split(/[s,]+/).map((c) => c.trim()).filter(Boolean)
9709
+ .map((c) => '#' + c.replace(/^#/, '').toLowerCase());
9710
+ const paletteValid = (colors) => colors.every((c) => /^#[0-9a-f]{6}$/.test(c));
9711
+
9712
+ function styleForm(style) {
9713
+ const pr = snap.workspace ? snap.workspace.projects.find((x) => x.id === style.project) : snap.project;
9714
+ const own = (field) => style.ownFields.includes(field);
9715
+ const provenance = (field) => own(field) ? 'set on this style'
9716
+ : style.extends ? 'inherited from ' + style.extends + ' \u2014 saving sets a value on this style itself; ' + style.extends + ' is unchanged'
9717
+ : 'default \u2014 saving sets a value on this style itself';
9718
+ const form = el('form', 'edit style-form');
9719
+ form.append(el('h3', null, 'Edit style ' + style.id));
9720
+ const prefix = el('input'); prefix.type = 'text'; prefix.value = style.promptPrefix; prefix.placeholder = 'none';
9721
+ const suffix = el('input'); suffix.type = 'text'; suffix.value = style.promptSuffix; suffix.placeholder = 'none';
9722
+ const palette = el('input'); palette.type = 'text'; palette.value = style.palette.join(', '); palette.placeholder = '#rrggbb, #rrggbb \u2014 leave empty for no forced palette';
9723
+ const swatches = el('div', 'pal');
9724
+ const drawSwatches = () => {
9725
+ swatches.textContent = '';
9726
+ for (const c of parsePalette(palette.value)) { const i = el('i'); i.style.background = c; i.title = c; swatches.append(i); }
9727
+ };
9728
+ drawSwatches();
9729
+ form.append(
9730
+ field('prompt prefix', prefix, 'Prepended to every asset prompt in this style. ' + provenance('promptPrefix')),
9731
+ field('prompt suffix', suffix, 'Appended to every asset prompt in this style. ' + provenance('promptSuffix')),
9732
+ field('palette', palette, 'Forced colours where the provider supports them (pixflux). ' + provenance('palette')),
9733
+ swatches,
9734
+ );
9735
+ const blast = el('div', 'blast');
9736
+ const changed = () => {
9737
+ const out = [];
9738
+ if (prefix.value !== style.promptPrefix) out.push('promptPrefix');
9739
+ if (suffix.value !== style.promptSuffix) out.push('promptSuffix');
9740
+ if (parsePalette(palette.value).join(',') !== style.palette.map((c) => c.toLowerCase()).join(',')) out.push('palette');
9741
+ return out;
9742
+ };
9743
+ const updateBlast = () => {
9744
+ const fields = changed();
9745
+ drawSwatches();
9746
+ if (!fields.length) { blast.className = 'blast'; blast.textContent = 'No changes yet. A style change alters the request for every asset that uses it.'; return; }
9747
+ const r = blastRadius(style, fields);
9748
+ blast.className = 'blast hot';
9749
+ blast.textContent = '';
9750
+ const others = r.styles.filter((id) => id !== style.id);
9751
+ blast.append(el('b', null, 'Affects ' + r.assets + (r.assets === 1 ? ' asset' : ' assets')));
9752
+ blast.append(document.createTextNode(others.length
9753
+ ? ' in ' + style.id + ' and ' + others.length + (others.length === 1 ? ' style that inherits it (' : ' styles that inherit it (') + others.join(', ') + ').'
9754
+ : ' in ' + style.id + '.'));
9755
+ blast.append(document.createTextNode(' Generated ones become stale; regenerating all of them is about ' + (r.cost || 'nothing') + '. Nothing is spent until you generate.'));
9756
+ };
9757
+ for (const input of [prefix, suffix, palette]) input.addEventListener('input', updateBlast);
9758
+ updateBlast();
9759
+ form.append(blast);
9760
+ const actions = el('div', 'actions');
9761
+ const save = el('button', 'primary', 'Save to manifest'); save.type = 'submit';
9762
+ const cancel = el('button', null, 'Cancel'); cancel.type = 'button'; cancel.onclick = () => { ui.editing = null; render(); };
9763
+ const msg = el('span', 'msg');
9764
+ actions.append(save, cancel, msg);
9765
+ form.append(actions);
9766
+ form.onsubmit = async (e) => {
9767
+ e.preventDefault();
9768
+ const fields = changed();
9769
+ if (!fields.length) { ui.editing = null; render(); return; }
9770
+ const colors = parsePalette(palette.value);
9771
+ if (fields.includes('palette') && !paletteValid(colors)) { msg.className = 'msg bad'; msg.textContent = 'palette must be six-digit hex colours'; return; }
9772
+ const patch = {};
9773
+ if (fields.includes('promptPrefix')) patch.promptPrefix = prefix.value;
9774
+ if (fields.includes('promptSuffix')) patch.promptSuffix = suffix.value;
9775
+ if (fields.includes('palette')) patch.palette = colors;
9776
+ save.disabled = true; msg.className = 'msg'; msg.textContent = 'saving\u2026';
9777
+ const r = blastRadius(style, fields);
9778
+ try {
9779
+ const body = { action: 'patch-style', styleId: style.id, expectedSha256: pr.manifestSha256, patch };
9780
+ if (style.project) body.project = style.project;
9781
+ snap = await postEdit(body);
9782
+ ui.editing = null;
9783
+ ui.notice = { id: 'style:' + (style.project || '') + ':' + style.id,
9784
+ text: '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.' };
9785
+ render();
9786
+ } catch (err) {
9787
+ save.disabled = false;
9788
+ msg.className = 'msg bad';
9789
+ msg.textContent = err.message + (err.status === 409 ? ' \u2014 press Refresh.' : '');
9790
+ }
9791
+ };
9792
+ setTimeout(() => prefix.focus(), 0);
9793
+ return form;
9794
+ }
9795
+
9630
9796
  function addAssetForm(style) {
9631
9797
  const pr = snap.workspace ? snap.workspace.projects.find((x) => x.id === style.project) : snap.project;
9632
9798
  const siblings = snap.styles.filter((x) => x.project === style.project);
@@ -10352,7 +10518,7 @@ async function serveGallery(opts) {
10352
10518
  const asset = reviewAssets.get(jobId)?.get(url2.pathname);
10353
10519
  if (!asset) return fail(404, "no such review asset");
10354
10520
  try {
10355
- const bytes = await readFile15(asset.path);
10521
+ const bytes = await readFile16(asset.path);
10356
10522
  res.writeHead(200, {
10357
10523
  "Content-Type": asset.contentType,
10358
10524
  "Cache-Control": "no-store",
@@ -10401,7 +10567,7 @@ async function serveGallery(opts) {
10401
10567
  if (!asset) return fail(404, "no such gallery media");
10402
10568
  let bytes;
10403
10569
  try {
10404
- bytes = await readFile15(asset.path);
10570
+ bytes = await readFile16(asset.path);
10405
10571
  } catch {
10406
10572
  return fail(404, "gallery media is no longer on disk; refresh the page");
10407
10573
  }
@@ -10691,7 +10857,7 @@ function createGenerateHandlers(opts) {
10691
10857
  }
10692
10858
 
10693
10859
  // src/pipeline/salvage.ts
10694
- import { readFile as readFile16 } from "fs/promises";
10860
+ import { readFile as readFile17 } from "fs/promises";
10695
10861
  import { existsSync as existsSync17 } from "fs";
10696
10862
  import path21 from "path";
10697
10863
  function providerClaimId(provider, objectId) {
@@ -10703,7 +10869,7 @@ async function loadClaims(lockPaths, opts = {}) {
10703
10869
  if (!existsSync17(p)) throw new Error(`Claim lockfile not found: ${p}`);
10704
10870
  let parsed;
10705
10871
  try {
10706
- parsed = parseLock(JSON.parse(await readFile16(p, "utf8")));
10872
+ parsed = parseLock(JSON.parse(await readFile17(p, "utf8")));
10707
10873
  } catch {
10708
10874
  throw new Error(`Claim lockfile is malformed: ${p}`);
10709
10875
  }
@@ -10984,7 +11150,7 @@ async function workspaceStatus(ws, dir) {
10984
11150
 
10985
11151
  // src/pipeline/cache-health.ts
10986
11152
  import { existsSync as existsSync18 } from "fs";
10987
- import { readFile as readFile17, readdir as readdir2, rm as rm8 } from "fs/promises";
11153
+ import { readFile as readFile18, readdir as readdir2, rm as rm8 } from "fs/promises";
10988
11154
  import path22 from "path";
10989
11155
  async function inspectCaches(lock, lockPath, options = {}) {
10990
11156
  if (options.prune && !existsSync18(lockPath)) {
@@ -11014,7 +11180,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
11014
11180
  await saveCache(remotePath, { version: 1, hashes: {} });
11015
11181
  removed.resetRemoteHashCache = true;
11016
11182
  } else if (remoteHashes.invalidIds.length) {
11017
- const cache = parseCache(JSON.parse(await readFile17(remotePath, "utf8")));
11183
+ const cache = parseCache(JSON.parse(await readFile18(remotePath, "utf8")));
11018
11184
  for (const id of remoteHashes.invalidIds) delete cache.hashes[id];
11019
11185
  removed.remoteHashEntries = remoteHashes.invalidIds.length;
11020
11186
  await saveCache(remotePath, cache);
@@ -11058,7 +11224,7 @@ async function inspectContentCache(contentDir, referenced) {
11058
11224
  const expected = mediaType ? entry.name.slice(0, -4) : "";
11059
11225
  let bytes;
11060
11226
  try {
11061
- bytes = await readFile17(file);
11227
+ bytes = await readFile18(file);
11062
11228
  report.bytes += bytes.length;
11063
11229
  } catch (err) {
11064
11230
  report.invalid.push({
@@ -11105,7 +11271,7 @@ async function inspectRemoteHashCache(remotePath) {
11105
11271
  if (!report.exists) return report;
11106
11272
  let cache;
11107
11273
  try {
11108
- cache = parseCache(JSON.parse(await readFile17(remotePath, "utf8")));
11274
+ cache = parseCache(JSON.parse(await readFile18(remotePath, "utf8")));
11109
11275
  } catch (err) {
11110
11276
  report.error = err instanceof Error ? err.message : String(err);
11111
11277
  return report;
@@ -11376,7 +11542,7 @@ function isRecord2(value) {
11376
11542
  }
11377
11543
 
11378
11544
  // src/pick/salvage-server.ts
11379
- import { mkdir as mkdir6, writeFile as writeFile9, readFile as readFile18 } from "fs/promises";
11545
+ import { mkdir as mkdir6, writeFile as writeFile9, readFile as readFile19 } from "fs/promises";
11380
11546
  import path23 from "path";
11381
11547
 
11382
11548
  // src/pick/salvage-sheet.ts
@@ -11629,7 +11795,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
11629
11795
  }
11630
11796
  }
11631
11797
  await applyTags(provider, decisions, existingTags, { onProgress: log2 });
11632
- const raw = JSON.parse(await readFile18(ctx.manifestPath, "utf8"));
11798
+ const raw = JSON.parse(await readFile19(ctx.manifestPath, "utf8"));
11633
11799
  for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
11634
11800
  await writeFile9(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
11635
11801
  await saveLock(ctx.lockPath, ctx.lock);
@@ -11641,7 +11807,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
11641
11807
  // src/recipes.ts
11642
11808
  import path24 from "path";
11643
11809
  import { existsSync as existsSync19 } from "fs";
11644
- import { readdir as readdir3, readFile as readFile19 } from "fs/promises";
11810
+ import { readdir as readdir3, readFile as readFile20 } from "fs/promises";
11645
11811
  import { fileURLToPath } from "url";
11646
11812
  import { z as z7 } from "zod";
11647
11813
  var SHA256_RE = /^[0-9a-f]{64}$/;
@@ -11829,7 +11995,7 @@ async function readRecipeFile(recipePath, bundled) {
11829
11995
  const absolute = path24.resolve(recipePath);
11830
11996
  let raw;
11831
11997
  try {
11832
- raw = JSON.parse(await readFile19(absolute, "utf8"));
11998
+ raw = JSON.parse(await readFile20(absolute, "utf8"));
11833
11999
  } catch (error) {
11834
12000
  throw new Error(`Could not read recipe ${absolute}: ${message4(error)}`, { cause: error });
11835
12001
  }
@@ -11971,12 +12137,12 @@ async function installRecipe(target, options = {}) {
11971
12137
  ];
11972
12138
  const files = await Promise.all(sources.map(async (file) => ({
11973
12139
  path: path24.join(destination, ...file.path.split("/")),
11974
- data: await readFile19(file.source)
12140
+ data: await readFile20(file.source)
11975
12141
  })));
11976
12142
  if (!options.force) {
11977
12143
  for (const file of files) {
11978
12144
  if (!existsSync19(file.path)) continue;
11979
- const current = await readFile19(file.path);
12145
+ const current = await readFile20(file.path);
11980
12146
  if (!current.equals(file.data)) {
11981
12147
  throw new Error(
11982
12148
  `Recipe destination has local changes: ${file.path}. Choose another --out or pass --force to replace declared recipe files.`
@@ -12001,7 +12167,7 @@ async function installRecipe(target, options = {}) {
12001
12167
  // src/pipeline/quality-regression.ts
12002
12168
  import path25 from "path";
12003
12169
  import { existsSync as existsSync20 } from "fs";
12004
- import { readFile as readFile20 } from "fs/promises";
12170
+ import { readFile as readFile21 } from "fs/promises";
12005
12171
  import { z as z8 } from "zod";
12006
12172
  var SHA256_RE2 = /^[0-9a-f]{64}$/;
12007
12173
  var HEX_RE = /^#[0-9a-f]{6}$/;
@@ -12157,7 +12323,7 @@ async function measureImageQuality(file) {
12157
12323
  const absolute = path25.resolve(file);
12158
12324
  let bytes;
12159
12325
  try {
12160
- bytes = await readFile20(absolute);
12326
+ bytes = await readFile21(absolute);
12161
12327
  } catch (error) {
12162
12328
  throw new Error(`Cannot read quality image ${absolute}: ${message5(error)}`, { cause: error });
12163
12329
  }
@@ -12244,7 +12410,7 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
12244
12410
  });
12245
12411
  const data = Buffer.from(JSON.stringify(baseline, null, 2) + "\n");
12246
12412
  if (existsSync20(absolute) && !options.force) {
12247
- const current = await readFile20(absolute);
12413
+ const current = await readFile21(absolute);
12248
12414
  if (!current.equals(data)) {
12249
12415
  throw new Error(`Quality baseline already exists with different content: ${absolute}. Pass --force to replace it.`);
12250
12416
  }
@@ -12256,7 +12422,7 @@ async function readQualityBaseline(baselinePath) {
12256
12422
  const absolute = path25.resolve(baselinePath);
12257
12423
  let raw;
12258
12424
  try {
12259
- raw = JSON.parse(await readFile20(absolute, "utf8"));
12425
+ raw = JSON.parse(await readFile21(absolute, "utf8"));
12260
12426
  } catch (error) {
12261
12427
  throw new Error(`Could not read quality baseline ${absolute}: ${message5(error)}`, { cause: error });
12262
12428
  }
@@ -13054,7 +13220,7 @@ async function main() {
13054
13220
  }
13055
13221
  if (args.command === "--version" || args.command === "-v") {
13056
13222
  const pkg = JSON.parse(
13057
- await readFile21(new URL("../package.json", import.meta.url), "utf8")
13223
+ await readFile22(new URL("../package.json", import.meta.url), "utf8")
13058
13224
  );
13059
13225
  log(`${pkg.name} ${pkg.version}`);
13060
13226
  return;
@@ -13065,7 +13231,7 @@ async function main() {
13065
13231
  if (!args.out) throw new Error("quality snapshot needs --out <pixelkiln.quality.json>.");
13066
13232
  let raw;
13067
13233
  try {
13068
- raw = JSON.parse(await readFile21(path26.resolve(args.inputs), "utf8"));
13234
+ raw = JSON.parse(await readFile22(path26.resolve(args.inputs), "utf8"));
13069
13235
  } catch (error) {
13070
13236
  throw new Error(
13071
13237
  `Could not read quality inputs ${path26.resolve(args.inputs)}: ${error instanceof Error ? error.message : String(error)}`,
@@ -13356,7 +13522,7 @@ async function main() {
13356
13522
  if (args.primaryOnly || args.outputRoles.length) {
13357
13523
  throw new Error("--primary-only and --output-role require manifest-driven pack");
13358
13524
  }
13359
- const raw = JSON.parse(await readFile21(path26.resolve(args.inputs), "utf8"));
13525
+ const raw = JSON.parse(await readFile22(path26.resolve(args.inputs), "utf8"));
13360
13526
  const inputs = resolvePackInputs(raw, args.inputs);
13361
13527
  const { png, atlas, skipped, sources } = packSprites(inputs, { columns: args.columns });
13362
13528
  const base = path26.resolve(args.out.replace(/\.png$/, ""));