pixelkiln 0.80.0 → 0.82.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
@@ -68,7 +68,7 @@ function exitCodeFor(err) {
68
68
 
69
69
  // src/cli/main.ts
70
70
  import { existsSync as existsSync37 } from "fs";
71
- import { readFile as readFile32 } from "fs/promises";
71
+ import { readFile as readFile33 } from "fs/promises";
72
72
 
73
73
  // src/cli/args.ts
74
74
  import path from "path";
@@ -14628,10 +14628,11 @@ import { existsSync as existsSync26 } from "fs";
14628
14628
  // src/gallery/snapshot.ts
14629
14629
  import { createHash as createHash3 } from "crypto";
14630
14630
  import { existsSync as existsSync21 } from "fs";
14631
- import { readFile as readFile18, stat as stat2 } from "fs/promises";
14631
+ import { readFile as readFile19, stat as stat2 } from "fs/promises";
14632
14632
  import path30 from "path";
14633
14633
 
14634
14634
  // src/gallery/edit.ts
14635
+ import { readFile as readFile18, rename as rename9, writeFile as writeFile14 } from "fs/promises";
14635
14636
  import path28 from "path";
14636
14637
  import { z as z10 } from "zod";
14637
14638
 
@@ -14876,9 +14877,62 @@ var HandEditRequestSchema = z10.discriminatedUnion("action", [
14876
14877
  protocol: z10.number().int().positive()
14877
14878
  }).strict()
14878
14879
  ]);
14880
+ var UNDO_DEPTH = 20;
14881
+ var UndoRequestSchema = z10.object({
14882
+ action: z10.literal("undo-edit"),
14883
+ project: z10.string().min(1).optional(),
14884
+ /** The manifest as the page last saw it; undo refuses a manifest someone else has changed since. */
14885
+ expectedSha256: z10.string().regex(/^[0-9a-f]{64}$/)
14886
+ }).strict();
14879
14887
  function createGalleryEditHandler(opts) {
14880
14888
  const log2 = opts.onProgress ?? (() => {
14881
14889
  });
14890
+ const apply = createEditApplier(opts, log2);
14891
+ const history = /* @__PURE__ */ new Map();
14892
+ const manifestOf = async (project) => {
14893
+ try {
14894
+ return path28.resolve(await opts.manifestFor(typeof project === "string" ? project : void 0));
14895
+ } catch {
14896
+ return null;
14897
+ }
14898
+ };
14899
+ return async (body) => {
14900
+ const undo = UndoRequestSchema.safeParse(body);
14901
+ if (undo.success) {
14902
+ const manifestPath2 = await manifestOf(undo.data.project);
14903
+ if (!manifestPath2) throw new ManifestEditError("no manifest to undo an edit in");
14904
+ const current = await readFile18(manifestPath2, "utf8");
14905
+ if (sha256(current) !== undo.data.expectedSha256) throw new ManifestDriftError(manifestPath2);
14906
+ const stack = history.get(manifestPath2) ?? [];
14907
+ const last = stack[stack.length - 1];
14908
+ if (!last) throw new ManifestEditError("nothing to undo: this gallery has not changed the manifest");
14909
+ if (last.afterSha !== sha256(current)) {
14910
+ history.delete(manifestPath2);
14911
+ throw Object.assign(new Error("the manifest changed outside the gallery since its last edit; undo would discard that change"), { status: 409 });
14912
+ }
14913
+ const tmp = manifestPath2 + ".pixelkiln-undo";
14914
+ await writeFile14(tmp, last.before);
14915
+ await rename9(tmp, manifestPath2);
14916
+ stack.pop();
14917
+ log2(` manifest edit undone in ${path28.relative(process.cwd(), manifestPath2) || manifestPath2}`);
14918
+ return opts.reload();
14919
+ }
14920
+ const manifestPath = await manifestOf(body?.project);
14921
+ const before = manifestPath ? await readFile18(manifestPath, "utf8").catch(() => null) : null;
14922
+ const build = await apply(body);
14923
+ if (manifestPath && before !== null) {
14924
+ const after = await readFile18(manifestPath, "utf8").catch(() => null);
14925
+ if (after !== null && after !== before) {
14926
+ const stack = history.get(manifestPath) ?? [];
14927
+ stack.push({ before, afterSha: sha256(after) });
14928
+ if (stack.length > UNDO_DEPTH) stack.shift();
14929
+ history.set(manifestPath, stack);
14930
+ }
14931
+ }
14932
+ return build;
14933
+ };
14934
+ }
14935
+ function createEditApplier(opts, log2) {
14882
14936
  return async (body) => {
14883
14937
  const handEdit = HandEditRequestSchema.safeParse(body);
14884
14938
  if (handEdit.success) {
@@ -15220,7 +15274,7 @@ function describeHistory(media, entry, cacheDir) {
15220
15274
  var MAX_COMPARED_BYTES = 8 * 1024 * 1024;
15221
15275
  async function samePixels(a, b) {
15222
15276
  try {
15223
- const [left, right] = await Promise.all([readFile18(a), readFile18(b)]);
15277
+ const [left, right] = await Promise.all([readFile19(a), readFile19(b)]);
15224
15278
  if (left.length > MAX_COMPARED_BYTES || right.length > MAX_COMPARED_BYTES) return false;
15225
15279
  const x = decodePng(left);
15226
15280
  const y = decodePng(right);
@@ -15378,7 +15432,7 @@ async function buildGallerySnapshot(opts) {
15378
15432
  const media = /* @__PURE__ */ new Map();
15379
15433
  const plan = await buildPlan(specs, lock);
15380
15434
  const items = [];
15381
- const manifestText = await readFile18(loaded.path, "utf8");
15435
+ const manifestText = await readFile19(loaded.path, "utf8");
15382
15436
  const manifestSha256 = sha256(manifestText);
15383
15437
  const cacheDir = path30.resolve(path30.dirname(opts.lockPath), ".pixelkiln", "cache");
15384
15438
  let rawStyles = {};
@@ -15730,7 +15784,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
15730
15784
  root: loaded.root,
15731
15785
  provider: loaded.manifest.provider,
15732
15786
  account: project.account ?? null,
15733
- manifestSha256: manifestSha256 ?? sha256(await readFile18(manifestPath, "utf8")),
15787
+ manifestSha256: manifestSha256 ?? sha256(await readFile19(manifestPath, "utf8")),
15734
15788
  historyLimit: historyLimit(loaded.manifest),
15735
15789
  entries: Object.keys(lock.entries).length,
15736
15790
  items: projectItems.length,
@@ -15789,7 +15843,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
15789
15843
  // src/gallery/server.ts
15790
15844
  import { randomBytes } from "crypto";
15791
15845
  import { createServer as createServer2 } from "http";
15792
- import { readFile as readFile19 } from "fs/promises";
15846
+ import { readFile as readFile20 } from "fs/promises";
15793
15847
 
15794
15848
  // src/gallery/page.ts
15795
15849
  import { existsSync as existsSync22, readFileSync as readFileSync4 } from "fs";
@@ -15863,26 +15917,35 @@ ${css}</style>
15863
15917
  <span class="project" id="project"></span>
15864
15918
  <span class="grow"></span>
15865
15919
  <label class="search"><input id="q" type="search" placeholder="Search assets, prompts, ids, hashes" autocomplete="off"><kbd>/</kbd></label>
15866
- <select id="sort" aria-label="Sort">
15867
- <option value="key">Sort: style / asset</option>
15868
- <option value="newest">Sort: newest first</option>
15869
- <option value="oldest">Sort: oldest first</option>
15870
- <option value="cost">Sort: cost</option>
15871
- <option value="size">Sort: size</option>
15872
- </select>
15873
- <select id="group" aria-label="Group">
15874
- <option value="style">Group: by style</option>
15875
- <option value="family">Group: by family</option>
15876
- <option value="none">Group: none</option>
15877
- </select>
15920
+ <details class="menu" id="view-menu">
15921
+ <summary title="Sort, group, card size, backdrop, and playback">View</summary>
15922
+ <div class="menu-panel">
15923
+ <label class="menu-row"><span>Sort</span><select id="sort" aria-label="Sort">
15924
+ <option value="key">style / asset</option>
15925
+ <option value="newest">newest first</option>
15926
+ <option value="oldest">oldest first</option>
15927
+ <option value="cost">cost</option>
15928
+ <option value="size">size</option>
15929
+ </select></label>
15930
+ <label class="menu-row"><span>Group</span><select id="group" aria-label="Group">
15931
+ <option value="style">by style</option>
15932
+ <option value="family">by family</option>
15933
+ <option value="none">none</option>
15934
+ </select></label>
15935
+ <div class="menu-row"><span>Cards</span><span class="seg" id="size" role="group" aria-label="Card size">
15936
+ <button type="button" data-size="s" title="Small cards: more on screen">S</button><button type="button" data-size="m" title="Medium cards">M</button><button type="button" data-size="l" title="Large cards: sprites drawn bigger">L</button>
15937
+ </span></div>
15938
+ <div class="menu-row"><span>Backdrop</span><span id="backdrop-slot"></span></div>
15939
+ <label class="menu-row check" title="Play every loop on the grid; without this, a loop plays while the pointer is over it"><input id="playloops" type="checkbox"> Play every loop</label>
15940
+ <label class="menu-row check" title="Refresh every 5 seconds while this tab is visible"><input id="auto" type="checkbox"> Refresh every 5 seconds</label>
15941
+ <label class="menu-row check" id="notify-chip" hidden title="A system notification when a job finishes while this tab is in the background"><input id="notify" type="checkbox"> Notify when a job finishes</label>
15942
+ </div>
15943
+ </details>
15878
15944
  <span id="editing" class="editing" hidden title="This gallery can write the manifest. It never contacts a provider.">editing</span>
15879
15945
  <button id="select" type="button" title="Pick several records to generate, regenerate, compare, or tag together (or ctrl/\u2318-click a card)">Select</button>
15880
15946
  <button id="studio" type="button" hidden title="Draft a new character: its style, base, loops, and portrait, priced as you go">+ New character</button>
15881
15947
  <button id="refresh" type="button" title="Re-read the manifest, lockfile, and disk">Refresh</button>
15882
- <label class="chip" title="Refresh every 5 seconds while this tab is visible"><input id="auto" type="checkbox"> auto</label>
15883
- <label class="chip" title="Play every loop on the grid; without this, a loop plays while the pointer is over it"><input id="playloops" type="checkbox"> play loops</label>
15884
- <span id="backdrop-slot"></span>
15885
- <label class="chip" id="notify-chip" hidden title="A system notification when a job finishes while this tab is in the background"><input id="notify" type="checkbox"> notify</label>
15948
+ <button id="keys" type="button" title="Keyboard shortcuts (?)" aria-label="Keyboard shortcuts">?</button>
15886
15949
  </div>
15887
15950
  <div class="totals" id="totals"></div>
15888
15951
  <div class="chips" id="chips"></div>
@@ -15891,9 +15954,9 @@ ${css}</style>
15891
15954
  </header>
15892
15955
  <main id="root"></main>
15893
15956
  <div id="selbar" class="selbar" hidden></div>
15957
+ <div id="toast" class="toast" role="status" aria-live="polite" hidden></div>
15894
15958
  <footer>
15895
- Click a sprite for its full record; <kbd>shift</kbd>-click adds it to a side-by-side comparison. <kbd>\u2190</kbd>/<kbd>\u2192</kbd> step through the visible set while a record
15896
- is open, <kbd>,</kbd>/<kbd>.</kbd> step a loop's frames and <kbd>space</kbd> plays it, <kbd>Esc</kbd> closes it, and <kbd>/</kbd> jumps to search. This page reads the manifest, lockfile,
15959
+ Click a sprite for its full record; press <kbd>?</kbd> for every keyboard shortcut. This page reads the manifest, lockfile,
15897
15960
  and disk only. It never contacts a provider<span id="foot-edit"> and never writes anything</span><span id="foot-editing" hidden>.
15898
15961
  Editing is on: saving rewrites the manifest and nothing else</span><span id="foot-gen" hidden>.
15899
15962
  Generation is on under the session budget shown above; every run is the same submit, poll, and fetch as <code>pixelkiln gen</code></span>.
@@ -16095,7 +16158,7 @@ async function serveGallery(opts) {
16095
16158
  const asset = reviewAssets.get(jobId)?.get(url2.pathname);
16096
16159
  if (!asset) return fail(404, "no such review asset");
16097
16160
  try {
16098
- const bytes = await readFile19(asset.path);
16161
+ const bytes = await readFile20(asset.path);
16099
16162
  res.writeHead(200, {
16100
16163
  "Content-Type": asset.contentType,
16101
16164
  "Cache-Control": "no-store",
@@ -16178,7 +16241,7 @@ async function serveGallery(opts) {
16178
16241
  if (!asset) return fail(404, "no such gallery media");
16179
16242
  let bytes;
16180
16243
  try {
16181
- bytes = await readFile19(asset.path);
16244
+ bytes = await readFile20(asset.path);
16182
16245
  } catch {
16183
16246
  return fail(404, "gallery media is no longer on disk; refresh the page");
16184
16247
  }
@@ -16634,10 +16697,10 @@ function ownedReferenceImageId(spec, lock, providerId) {
16634
16697
 
16635
16698
  // src/pick/server.ts
16636
16699
  import { existsSync as existsSync24 } from "fs";
16637
- import { readFile as readFile21 } from "fs/promises";
16700
+ import { readFile as readFile22 } from "fs/promises";
16638
16701
 
16639
16702
  // src/pipeline/init.ts
16640
- import { readdir, readFile as readFile20, writeFile as writeFile14 } from "fs/promises";
16703
+ import { readdir, readFile as readFile21, writeFile as writeFile15 } from "fs/promises";
16641
16704
  import { existsSync as existsSync23 } from "fs";
16642
16705
  import path31 from "path";
16643
16706
  function pngSize(buf) {
@@ -16667,7 +16730,7 @@ async function scanAssets(root, opts = {}) {
16667
16730
  const skipped = [];
16668
16731
  const seen = /* @__PURE__ */ new Map();
16669
16732
  for (const file of files.sort()) {
16670
- const size = pngSize(await readFile20(file));
16733
+ const size = pngSize(await readFile21(file));
16671
16734
  if (!size) {
16672
16735
  skipped.push(`${file} (not a readable PNG)`);
16673
16736
  continue;
@@ -16735,7 +16798,7 @@ async function writeManifestFile(target, manifest) {
16735
16798
  if (existsSync23(target)) {
16736
16799
  throw new Error(`${target} already exists; refusing to overwrite an existing manifest.`);
16737
16800
  }
16738
- await writeFile14(target, JSON.stringify(manifest, null, 2) + "\n");
16801
+ await writeFile15(target, JSON.stringify(manifest, null, 2) + "\n");
16739
16802
  }
16740
16803
 
16741
16804
  // src/pick/sheet.ts
@@ -17154,7 +17217,7 @@ async function currentArt(entry, spec) {
17154
17217
  let width = entry.width;
17155
17218
  let height = entry.height;
17156
17219
  if (contentType === "image/png") {
17157
- const size = pngSize(await readFile21(file));
17220
+ const size = pngSize(await readFile22(file));
17158
17221
  if (size) ({ width, height } = size);
17159
17222
  }
17160
17223
  return { path: file, contentType, width, height };
@@ -17687,13 +17750,13 @@ function createGenerateHandlers(opts) {
17687
17750
  }
17688
17751
 
17689
17752
  // src/gallery/editor.ts
17690
- import { readFile as readFile23 } from "fs/promises";
17753
+ import { readFile as readFile24 } from "fs/promises";
17691
17754
  import path33 from "path";
17692
17755
 
17693
17756
  // src/editor/install.ts
17694
17757
  import { createHash as createHash4 } from "crypto";
17695
17758
  import { existsSync as existsSync25 } from "fs";
17696
- import { mkdir as mkdir12, readFile as readFile22, rename as rename9, rm as rm10, stat as stat3, writeFile as writeFile15 } from "fs/promises";
17759
+ import { mkdir as mkdir12, readFile as readFile23, rename as rename10, rm as rm10, stat as stat3, writeFile as writeFile16 } from "fs/promises";
17697
17760
  import os2 from "os";
17698
17761
  import path32 from "path";
17699
17762
 
@@ -17777,7 +17840,7 @@ function editorBaseUrl(pin = EDITOR_PIN) {
17777
17840
  }
17778
17841
  async function sha256Of(file) {
17779
17842
  try {
17780
- return createHash4("sha256").update(await readFile22(file)).digest("hex");
17843
+ return createHash4("sha256").update(await readFile23(file)).digest("hex");
17781
17844
  } catch {
17782
17845
  return null;
17783
17846
  }
@@ -17851,8 +17914,8 @@ async function installEditor(opts = {}) {
17851
17914
  const final = path32.join(dir, name);
17852
17915
  const tmp = `${final}.${process.pid}.part`;
17853
17916
  try {
17854
- await writeFile15(tmp, bytes);
17855
- await rename9(tmp, final);
17917
+ await writeFile16(tmp, bytes);
17918
+ await rename10(tmp, final);
17856
17919
  } finally {
17857
17920
  await rm10(tmp, { force: true });
17858
17921
  }
@@ -17970,7 +18033,7 @@ function createGalleryEditorHandlers(opts = {}) {
17970
18033
  if (!status.installed) return null;
17971
18034
  let bytes;
17972
18035
  try {
17973
- bytes = await readFile23(path33.join(dir, name));
18036
+ bytes = await readFile24(path33.join(dir, name));
17974
18037
  } catch {
17975
18038
  verified = null;
17976
18039
  return null;
@@ -18437,7 +18500,7 @@ import { existsSync as existsSync28 } from "fs";
18437
18500
 
18438
18501
  // src/pipeline/cache-health.ts
18439
18502
  import { existsSync as existsSync27 } from "fs";
18440
- import { readFile as readFile24, readdir as readdir2, rm as rm11 } from "fs/promises";
18503
+ import { readFile as readFile25, readdir as readdir2, rm as rm11 } from "fs/promises";
18441
18504
  import path35 from "path";
18442
18505
  async function inspectCaches(lock, lockPath, options = {}) {
18443
18506
  if (options.prune && !existsSync27(lockPath)) {
@@ -18465,7 +18528,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
18465
18528
  await saveCache(remotePath, { version: 1, hashes: {} });
18466
18529
  removed.resetRemoteHashCache = true;
18467
18530
  } else if (remoteHashes.invalidIds.length) {
18468
- const cache = parseCache(JSON.parse(await readFile24(remotePath, "utf8")));
18531
+ const cache = parseCache(JSON.parse(await readFile25(remotePath, "utf8")));
18469
18532
  for (const id of remoteHashes.invalidIds) delete cache.hashes[id];
18470
18533
  removed.remoteHashEntries = remoteHashes.invalidIds.length;
18471
18534
  await saveCache(remotePath, cache);
@@ -18509,7 +18572,7 @@ async function inspectContentCache(contentDir, referenced) {
18509
18572
  const expected = mediaType ? entry.name.slice(0, -4) : "";
18510
18573
  let bytes;
18511
18574
  try {
18512
- bytes = await readFile24(file);
18575
+ bytes = await readFile25(file);
18513
18576
  report.bytes += bytes.length;
18514
18577
  } catch (err) {
18515
18578
  report.invalid.push({
@@ -18556,7 +18619,7 @@ async function inspectRemoteHashCache(remotePath) {
18556
18619
  if (!report.exists) return report;
18557
18620
  let cache;
18558
18621
  try {
18559
- cache = parseCache(JSON.parse(await readFile24(remotePath, "utf8")));
18622
+ cache = parseCache(JSON.parse(await readFile25(remotePath, "utf8")));
18560
18623
  } catch (err) {
18561
18624
  report.error = err instanceof Error ? err.message : String(err);
18562
18625
  return report;
@@ -18883,7 +18946,7 @@ async function nearestExistingDirectory(start) {
18883
18946
  }
18884
18947
 
18885
18948
  // src/pipeline/audit.ts
18886
- import { readFile as readFile25 } from "fs/promises";
18949
+ import { readFile as readFile26 } from "fs/promises";
18887
18950
  import { existsSync as existsSync31 } from "fs";
18888
18951
  import path38 from "path";
18889
18952
  function paletteDistance(asset, reference) {
@@ -18926,7 +18989,7 @@ async function auditStyle(loaded, specs, styleId, lock) {
18926
18989
  continue;
18927
18990
  }
18928
18991
  try {
18929
- const png = decodePng(await readFile25(output.absolutePath));
18992
+ const png = decodePng(await readFile26(output.absolutePath));
18930
18993
  const palette = extractPalette(png, 12);
18931
18994
  assets.push({
18932
18995
  assetId: spec.assetId,
@@ -18953,7 +19016,7 @@ async function auditStyle(loaded, specs, styleId, lock) {
18953
19016
  const abs = path38.resolve(loaded.root, img.path);
18954
19017
  if (!existsSync31(abs)) continue;
18955
19018
  try {
18956
- refPalettes.push(extractPalette(decodePng(await readFile25(abs)), 12));
19019
+ refPalettes.push(extractPalette(decodePng(await readFile26(abs)), 12));
18957
19020
  } catch {
18958
19021
  }
18959
19022
  }
@@ -19182,7 +19245,7 @@ async function runAudit(args) {
19182
19245
 
19183
19246
  // src/cli/commands/pack.ts
19184
19247
  import path40 from "path";
19185
- import { readFile as readFile26 } from "fs/promises";
19248
+ import { readFile as readFile27 } from "fs/promises";
19186
19249
 
19187
19250
  // src/pipeline/pack.ts
19188
19251
  import { readFileSync as readFileSync5 } from "fs";
@@ -19895,7 +19958,7 @@ async function runPack(args) {
19895
19958
  if (args.primaryOnly || args.outputRoles.length) {
19896
19959
  throw new Error("--primary-only and --output-role require manifest-driven pack");
19897
19960
  }
19898
- const raw = JSON.parse(await readFile26(path40.resolve(args.inputs), "utf8"));
19961
+ const raw = JSON.parse(await readFile27(path40.resolve(args.inputs), "utf8"));
19899
19962
  const inputs = resolvePackInputs(raw, args.inputs);
19900
19963
  const { png, atlas, skipped, sources } = packSprites(inputs, { columns: args.columns });
19901
19964
  const format = sheetFormat(args);
@@ -20124,7 +20187,7 @@ import path42 from "path";
20124
20187
 
20125
20188
  // src/pixellab-utils.ts
20126
20189
  import { existsSync as existsSync32 } from "fs";
20127
- import { mkdir as mkdir13, readFile as readFile27, writeFile as writeFile16 } from "fs/promises";
20190
+ import { mkdir as mkdir13, readFile as readFile28, writeFile as writeFile17 } from "fs/promises";
20128
20191
  import path41 from "path";
20129
20192
  var UNZOOM_MIN_SIDE = 256;
20130
20193
  var UNZOOM_MAX_AREA = 2048 * 2048;
@@ -20136,7 +20199,7 @@ async function unzoomFile(client2, input, opts = {}) {
20136
20199
  const out = path41.resolve(opts.out ?? defaultUnzoomOut(input));
20137
20200
  if (path41.resolve(input) === out) throw new Error("unzoom will not overwrite its own input; pass a different --out");
20138
20201
  if (existsSync32(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
20139
- const bytes = await readFile27(input);
20202
+ const bytes = await readFile28(input);
20140
20203
  const metadata = imageMetadata(bytes);
20141
20204
  if (!metadata) throw new Error(`${input} is not a readable PNG or JPEG`);
20142
20205
  const { width, height, format } = metadata;
@@ -20150,7 +20213,7 @@ async function unzoomFile(client2, input, opts = {}) {
20150
20213
  }
20151
20214
  const res = await client2.unzoom({ image: { base64: bytes.toString("base64"), format }, quantize: opts.quantize });
20152
20215
  await mkdir13(path41.dirname(out), { recursive: true });
20153
- await writeFile16(out, res.png);
20216
+ await writeFile17(out, res.png);
20154
20217
  return { out, original: res.originalSize, unzoomed: res.unzoomedSize, zoomFactor: res.zoomFactor, usage: res.usage };
20155
20218
  }
20156
20219
  var FONT_WEIGHTS = ["Bold", "Regular"];
@@ -20192,8 +20255,8 @@ async function generateFont(client2, opts) {
20192
20255
  client2.download(job.download_atlas_url)
20193
20256
  ]);
20194
20257
  await mkdir13(path41.dirname(ttf), { recursive: true });
20195
- await writeFile16(ttf, ttfBytes);
20196
- await writeFile16(atlas, atlasBytes);
20258
+ await writeFile17(ttf, ttfBytes);
20259
+ await writeFile17(atlas, atlasBytes);
20197
20260
  return { jobId, ttf, atlas, glyphPx: job.glyph_px ?? null, usage: job.usage };
20198
20261
  }
20199
20262
  if (Date.now() - started > timeout) {
@@ -20274,12 +20337,12 @@ async function runFont(args) {
20274
20337
 
20275
20338
  // src/cli/commands/quality.ts
20276
20339
  import path44 from "path";
20277
- import { readFile as readFile29 } from "fs/promises";
20340
+ import { readFile as readFile30 } from "fs/promises";
20278
20341
 
20279
20342
  // src/pipeline/quality-regression.ts
20280
20343
  import path43 from "path";
20281
20344
  import { existsSync as existsSync33 } from "fs";
20282
- import { readFile as readFile28 } from "fs/promises";
20345
+ import { readFile as readFile29 } from "fs/promises";
20283
20346
  import { z as z12 } from "zod";
20284
20347
  var SHA256_RE = /^[0-9a-f]{64}$/;
20285
20348
  var HEX_RE = /^#[0-9a-f]{6}$/;
@@ -20435,7 +20498,7 @@ async function measureImageQuality(file) {
20435
20498
  const absolute = path43.resolve(file);
20436
20499
  let bytes;
20437
20500
  try {
20438
- bytes = await readFile28(absolute);
20501
+ bytes = await readFile29(absolute);
20439
20502
  } catch (error) {
20440
20503
  throw new Error(`Cannot read quality image ${absolute}: ${message4(error)}`, { cause: error });
20441
20504
  }
@@ -20522,7 +20585,7 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
20522
20585
  });
20523
20586
  const data = Buffer.from(JSON.stringify(baseline, null, 2) + "\n");
20524
20587
  if (existsSync33(absolute) && !options.force) {
20525
- const current = await readFile28(absolute);
20588
+ const current = await readFile29(absolute);
20526
20589
  if (!current.equals(data)) {
20527
20590
  throw new OverwriteRefusedError(
20528
20591
  `Quality baseline already exists with different content: ${absolute}. Pass --force to replace it.`,
@@ -20537,7 +20600,7 @@ async function readQualityBaseline(baselinePath) {
20537
20600
  const absolute = path43.resolve(baselinePath);
20538
20601
  let raw;
20539
20602
  try {
20540
- raw = JSON.parse(await readFile28(absolute, "utf8"));
20603
+ raw = JSON.parse(await readFile29(absolute, "utf8"));
20541
20604
  } catch (error) {
20542
20605
  throw new Error(`Could not read quality baseline ${absolute}: ${message4(error)}`, { cause: error });
20543
20606
  }
@@ -20667,7 +20730,7 @@ async function runQuality(args) {
20667
20730
  if (!args.out) throw new Error("quality snapshot needs --out <pixelkiln.quality.json>.");
20668
20731
  let raw;
20669
20732
  try {
20670
- raw = JSON.parse(await readFile29(path44.resolve(args.inputs), "utf8"));
20733
+ raw = JSON.parse(await readFile30(path44.resolve(args.inputs), "utf8"));
20671
20734
  } catch (error) {
20672
20735
  throw new Error(
20673
20736
  `Could not read quality inputs ${path44.resolve(args.inputs)}: ${error instanceof Error ? error.message : String(error)}`,
@@ -20719,7 +20782,7 @@ import path46 from "path";
20719
20782
  // src/recipes.ts
20720
20783
  import path45 from "path";
20721
20784
  import { existsSync as existsSync34 } from "fs";
20722
- import { readdir as readdir3, readFile as readFile30 } from "fs/promises";
20785
+ import { readdir as readdir3, readFile as readFile31 } from "fs/promises";
20723
20786
  import { fileURLToPath as fileURLToPath2 } from "url";
20724
20787
  import { z as z13 } from "zod";
20725
20788
  var SHA256_RE2 = /^[0-9a-f]{64}$/;
@@ -20907,7 +20970,7 @@ async function readRecipeFile(recipePath, bundled) {
20907
20970
  const absolute = path45.resolve(recipePath);
20908
20971
  let raw;
20909
20972
  try {
20910
- raw = JSON.parse(await readFile30(absolute, "utf8"));
20973
+ raw = JSON.parse(await readFile31(absolute, "utf8"));
20911
20974
  } catch (error) {
20912
20975
  throw new Error(`Could not read recipe ${absolute}: ${message5(error)}`, { cause: error });
20913
20976
  }
@@ -21049,12 +21112,12 @@ async function installRecipe(target, options = {}) {
21049
21112
  ];
21050
21113
  const files = await Promise.all(sources.map(async (file) => ({
21051
21114
  path: path45.join(destination, ...file.path.split("/")),
21052
- data: await readFile30(file.source)
21115
+ data: await readFile31(file.source)
21053
21116
  })));
21054
21117
  if (!options.force) {
21055
21118
  for (const file of files) {
21056
21119
  if (!existsSync34(file.path)) continue;
21057
- const current = await readFile30(file.path);
21120
+ const current = await readFile31(file.path);
21058
21121
  if (!current.equals(file.data)) {
21059
21122
  throw new OverwriteRefusedError(
21060
21123
  `Recipe destination has local changes: ${file.path}. Choose another --out or pass --force to replace declared recipe files.`,
@@ -21294,7 +21357,7 @@ async function runRefine(args) {
21294
21357
  // src/cli/commands/skeleton.ts
21295
21358
  import path48 from "path";
21296
21359
  import { existsSync as existsSync35 } from "fs";
21297
- import { readFile as readFile31, writeFile as writeFile17 } from "fs/promises";
21360
+ import { readFile as readFile32, writeFile as writeFile18 } from "fs/promises";
21298
21361
 
21299
21362
  // src/skeleton-preview.ts
21300
21363
  var RIGHT = [255, 140, 40, 255];
@@ -21410,7 +21473,7 @@ async function runEstimateSkeleton(args) {
21410
21473
  }
21411
21474
  loadEnvFiles(path48.dirname(path48.resolve(args.manifest)));
21412
21475
  loadEnvFiles(process.cwd());
21413
- const bytes = await readFile31(path48.resolve(image));
21476
+ const bytes = await readFile32(path48.resolve(image));
21414
21477
  const metadata = imageMetadata(bytes);
21415
21478
  if (!metadata) throw new UsageError(`${image} is not a readable PNG or JPEG`);
21416
21479
  const sizeProblem = estimateSkeletonSizeProblem(metadata.width, metadata.height);
@@ -21422,7 +21485,7 @@ async function runEstimateSkeleton(args) {
21422
21485
  const frames = args.frames ?? DEFAULT_SCAFFOLD_FRAMES;
21423
21486
  const json = JSON.stringify(scaffoldSkeletonSet(res.keypoints, frames), null, 2);
21424
21487
  if (args.out) {
21425
- await writeFile17(path48.resolve(args.out), json + "\n");
21488
+ await writeFile18(path48.resolve(args.out), json + "\n");
21426
21489
  log(` wrote ${args.out}: the estimated pose and ${frames} frames to edit`);
21427
21490
  log(` preview it with: pixelkiln skeleton-preview ${args.out} --from ${image}`);
21428
21491
  } else {
@@ -21454,7 +21517,7 @@ async function runSkeletonPreview(args) {
21454
21517
  if (!existsSync35(keypointsFile)) throw new UsageError(`Keypoints file does not exist yet: ${keypointsFile}`);
21455
21518
  let json;
21456
21519
  try {
21457
- json = JSON.parse(await readFile31(keypointsFile, "utf8"));
21520
+ json = JSON.parse(await readFile32(keypointsFile, "utf8"));
21458
21521
  } catch (error) {
21459
21522
  throw new UsageError(`${keypointsFile} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
21460
21523
  }
@@ -21464,13 +21527,13 @@ async function runSkeletonPreview(args) {
21464
21527
  if (!existsSync35(sourceFile)) {
21465
21528
  log(` source image does not exist yet, drawing on a plain ground: ${sourceFile}`);
21466
21529
  } else {
21467
- const bytes = await readFile31(sourceFile);
21530
+ const bytes = await readFile32(sourceFile);
21468
21531
  if (imageMetadata(bytes)?.format !== "png") throw new UsageError(`${sourceFile} must be a PNG to preview over`);
21469
21532
  source = decodePng(bytes);
21470
21533
  }
21471
21534
  }
21472
21535
  const out = path48.resolve(args.out ?? keypointsFile.replace(/\.json$/, "") + ".preview.png");
21473
- await writeFile17(out, renderSkeletonSheet(set, { source }));
21536
+ await writeFile18(out, renderSkeletonSheet(set, { source }));
21474
21537
  log(` ${label}: ${set.frames.length} frames after the starting pose`);
21475
21538
  const shown = path48.relative(process.cwd(), out);
21476
21539
  log(` wrote ${shown.startsWith("..") ? out : shown}`);
@@ -21656,7 +21719,7 @@ async function runVersion() {
21656
21719
  for (const up of ["../package.json", "../../package.json"]) {
21657
21720
  const url = new URL(up, import.meta.url);
21658
21721
  if (!existsSync37(url)) continue;
21659
- const pkg = JSON.parse(await readFile32(url, "utf8"));
21722
+ const pkg = JSON.parse(await readFile33(url, "utf8"));
21660
21723
  if (pkg.name === "pixelkiln") {
21661
21724
  log(`${pkg.name} ${pkg.version}`);
21662
21725
  return;