pixelkiln 0.81.0 → 0.83.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";
@@ -13945,10 +13945,10 @@ var NewAssetSchema = z6.object({
13945
13945
  styles: z6.array(z6.string().min(1)).optional(),
13946
13946
  /**
13947
13947
  * A controlled regeneration of another asset in the same manifest,
13948
- * instead of a fresh generation. `inpaint` needs a mask upload the
13949
- * gallery does not offer yet, so only `image-to-image` and
13950
- * `animate-skeleton` (whose keypoints file the gallery writes first) can
13951
- * be created here; hand-edit the manifest for the other modes.
13948
+ * instead of a fresh generation. `inpaint` needs a mask upload and
13949
+ * `interpolate` an ending-keyframe image the gallery does not offer, so
13950
+ * those two are hand-edited; every other mode can be created here
13951
+ * (`animate-skeleton` after the gallery writes its keypoints file).
13952
13952
  */
13953
13953
  revision: z6.discriminatedUnion("mode", [
13954
13954
  z6.object({
@@ -13957,6 +13957,29 @@ var NewAssetSchema = z6.object({
13957
13957
  from: z6.string().min(1),
13958
13958
  strength: z6.number().min(0).max(1).optional()
13959
13959
  }).strict(),
13960
+ z6.object({
13961
+ mode: z6.literal("reduce-colors"),
13962
+ from: z6.string().min(1),
13963
+ /** A target colour count; the gallery leaves PixelLab's default when this is omitted. */
13964
+ numColors: z6.number().int().min(2).max(256).optional(),
13965
+ dithering: RevisionDitheringSchema.optional(),
13966
+ ditheringStrength: z6.number().min(0).max(10).optional()
13967
+ }).strict(),
13968
+ z6.object({ mode: z6.literal("correct-pixelart"), from: z6.string().min(1), strength: z6.number().min(0).max(1).optional() }).strict(),
13969
+ z6.object({
13970
+ mode: z6.enum(["animate", "animate-pixminimax"]),
13971
+ from: z6.string().min(1),
13972
+ frames: z6.number().int().min(4).max(40).refine((n) => n % 2 === 0, "frames must be even").optional(),
13973
+ fps: z6.number().int().min(1).max(60).optional(),
13974
+ enhancePrompt: z6.boolean().optional(),
13975
+ /** `animate-pixminimax` only; the loader refuses it on `animate`. */
13976
+ direction: CharacterDirectionSchema.optional()
13977
+ }).strict(),
13978
+ z6.object({
13979
+ mode: z6.literal("edit-animation"),
13980
+ from: z6.string().min(1),
13981
+ fps: z6.number().int().min(1).max(60).optional()
13982
+ }).strict(),
13960
13983
  z6.object({
13961
13984
  mode: z6.literal("animate-skeleton"),
13962
13985
  from: z6.string().min(1),
@@ -14628,10 +14651,11 @@ import { existsSync as existsSync26 } from "fs";
14628
14651
  // src/gallery/snapshot.ts
14629
14652
  import { createHash as createHash3 } from "crypto";
14630
14653
  import { existsSync as existsSync21 } from "fs";
14631
- import { readFile as readFile18, stat as stat2 } from "fs/promises";
14654
+ import { readFile as readFile19, stat as stat2 } from "fs/promises";
14632
14655
  import path30 from "path";
14633
14656
 
14634
14657
  // src/gallery/edit.ts
14658
+ import { readFile as readFile18, rename as rename9, writeFile as writeFile14 } from "fs/promises";
14635
14659
  import path28 from "path";
14636
14660
  import { z as z10 } from "zod";
14637
14661
 
@@ -14876,9 +14900,62 @@ var HandEditRequestSchema = z10.discriminatedUnion("action", [
14876
14900
  protocol: z10.number().int().positive()
14877
14901
  }).strict()
14878
14902
  ]);
14903
+ var UNDO_DEPTH = 20;
14904
+ var UndoRequestSchema = z10.object({
14905
+ action: z10.literal("undo-edit"),
14906
+ project: z10.string().min(1).optional(),
14907
+ /** The manifest as the page last saw it; undo refuses a manifest someone else has changed since. */
14908
+ expectedSha256: z10.string().regex(/^[0-9a-f]{64}$/)
14909
+ }).strict();
14879
14910
  function createGalleryEditHandler(opts) {
14880
14911
  const log2 = opts.onProgress ?? (() => {
14881
14912
  });
14913
+ const apply = createEditApplier(opts, log2);
14914
+ const history = /* @__PURE__ */ new Map();
14915
+ const manifestOf = async (project) => {
14916
+ try {
14917
+ return path28.resolve(await opts.manifestFor(typeof project === "string" ? project : void 0));
14918
+ } catch {
14919
+ return null;
14920
+ }
14921
+ };
14922
+ return async (body) => {
14923
+ const undo = UndoRequestSchema.safeParse(body);
14924
+ if (undo.success) {
14925
+ const manifestPath2 = await manifestOf(undo.data.project);
14926
+ if (!manifestPath2) throw new ManifestEditError("no manifest to undo an edit in");
14927
+ const current = await readFile18(manifestPath2, "utf8");
14928
+ if (sha256(current) !== undo.data.expectedSha256) throw new ManifestDriftError(manifestPath2);
14929
+ const stack = history.get(manifestPath2) ?? [];
14930
+ const last = stack[stack.length - 1];
14931
+ if (!last) throw new ManifestEditError("nothing to undo: this gallery has not changed the manifest");
14932
+ if (last.afterSha !== sha256(current)) {
14933
+ history.delete(manifestPath2);
14934
+ throw Object.assign(new Error("the manifest changed outside the gallery since its last edit; undo would discard that change"), { status: 409 });
14935
+ }
14936
+ const tmp = manifestPath2 + ".pixelkiln-undo";
14937
+ await writeFile14(tmp, last.before);
14938
+ await rename9(tmp, manifestPath2);
14939
+ stack.pop();
14940
+ log2(` manifest edit undone in ${path28.relative(process.cwd(), manifestPath2) || manifestPath2}`);
14941
+ return opts.reload();
14942
+ }
14943
+ const manifestPath = await manifestOf(body?.project);
14944
+ const before = manifestPath ? await readFile18(manifestPath, "utf8").catch(() => null) : null;
14945
+ const build = await apply(body);
14946
+ if (manifestPath && before !== null) {
14947
+ const after = await readFile18(manifestPath, "utf8").catch(() => null);
14948
+ if (after !== null && after !== before) {
14949
+ const stack = history.get(manifestPath) ?? [];
14950
+ stack.push({ before, afterSha: sha256(after) });
14951
+ if (stack.length > UNDO_DEPTH) stack.shift();
14952
+ history.set(manifestPath, stack);
14953
+ }
14954
+ }
14955
+ return build;
14956
+ };
14957
+ }
14958
+ function createEditApplier(opts, log2) {
14882
14959
  return async (body) => {
14883
14960
  const handEdit = HandEditRequestSchema.safeParse(body);
14884
14961
  if (handEdit.success) {
@@ -15220,7 +15297,7 @@ function describeHistory(media, entry, cacheDir) {
15220
15297
  var MAX_COMPARED_BYTES = 8 * 1024 * 1024;
15221
15298
  async function samePixels(a, b) {
15222
15299
  try {
15223
- const [left, right] = await Promise.all([readFile18(a), readFile18(b)]);
15300
+ const [left, right] = await Promise.all([readFile19(a), readFile19(b)]);
15224
15301
  if (left.length > MAX_COMPARED_BYTES || right.length > MAX_COMPARED_BYTES) return false;
15225
15302
  const x = decodePng(left);
15226
15303
  const y = decodePng(right);
@@ -15378,7 +15455,7 @@ async function buildGallerySnapshot(opts) {
15378
15455
  const media = /* @__PURE__ */ new Map();
15379
15456
  const plan = await buildPlan(specs, lock);
15380
15457
  const items = [];
15381
- const manifestText = await readFile18(loaded.path, "utf8");
15458
+ const manifestText = await readFile19(loaded.path, "utf8");
15382
15459
  const manifestSha256 = sha256(manifestText);
15383
15460
  const cacheDir = path30.resolve(path30.dirname(opts.lockPath), ".pixelkiln", "cache");
15384
15461
  let rawStyles = {};
@@ -15730,7 +15807,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
15730
15807
  root: loaded.root,
15731
15808
  provider: loaded.manifest.provider,
15732
15809
  account: project.account ?? null,
15733
- manifestSha256: manifestSha256 ?? sha256(await readFile18(manifestPath, "utf8")),
15810
+ manifestSha256: manifestSha256 ?? sha256(await readFile19(manifestPath, "utf8")),
15734
15811
  historyLimit: historyLimit(loaded.manifest),
15735
15812
  entries: Object.keys(lock.entries).length,
15736
15813
  items: projectItems.length,
@@ -15789,7 +15866,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
15789
15866
  // src/gallery/server.ts
15790
15867
  import { randomBytes } from "crypto";
15791
15868
  import { createServer as createServer2 } from "http";
15792
- import { readFile as readFile19 } from "fs/promises";
15869
+ import { readFile as readFile20 } from "fs/promises";
15793
15870
 
15794
15871
  // src/gallery/page.ts
15795
15872
  import { existsSync as existsSync22, readFileSync as readFileSync4 } from "fs";
@@ -15900,6 +15977,7 @@ ${css}</style>
15900
15977
  </header>
15901
15978
  <main id="root"></main>
15902
15979
  <div id="selbar" class="selbar" hidden></div>
15980
+ <div id="toast" class="toast" role="status" aria-live="polite" hidden></div>
15903
15981
  <footer>
15904
15982
  Click a sprite for its full record; press <kbd>?</kbd> for every keyboard shortcut. This page reads the manifest, lockfile,
15905
15983
  and disk only. It never contacts a provider<span id="foot-edit"> and never writes anything</span><span id="foot-editing" hidden>.
@@ -16103,7 +16181,7 @@ async function serveGallery(opts) {
16103
16181
  const asset = reviewAssets.get(jobId)?.get(url2.pathname);
16104
16182
  if (!asset) return fail(404, "no such review asset");
16105
16183
  try {
16106
- const bytes = await readFile19(asset.path);
16184
+ const bytes = await readFile20(asset.path);
16107
16185
  res.writeHead(200, {
16108
16186
  "Content-Type": asset.contentType,
16109
16187
  "Cache-Control": "no-store",
@@ -16186,7 +16264,7 @@ async function serveGallery(opts) {
16186
16264
  if (!asset) return fail(404, "no such gallery media");
16187
16265
  let bytes;
16188
16266
  try {
16189
- bytes = await readFile19(asset.path);
16267
+ bytes = await readFile20(asset.path);
16190
16268
  } catch {
16191
16269
  return fail(404, "gallery media is no longer on disk; refresh the page");
16192
16270
  }
@@ -16642,10 +16720,10 @@ function ownedReferenceImageId(spec, lock, providerId) {
16642
16720
 
16643
16721
  // src/pick/server.ts
16644
16722
  import { existsSync as existsSync24 } from "fs";
16645
- import { readFile as readFile21 } from "fs/promises";
16723
+ import { readFile as readFile22 } from "fs/promises";
16646
16724
 
16647
16725
  // src/pipeline/init.ts
16648
- import { readdir, readFile as readFile20, writeFile as writeFile14 } from "fs/promises";
16726
+ import { readdir, readFile as readFile21, writeFile as writeFile15 } from "fs/promises";
16649
16727
  import { existsSync as existsSync23 } from "fs";
16650
16728
  import path31 from "path";
16651
16729
  function pngSize(buf) {
@@ -16675,7 +16753,7 @@ async function scanAssets(root, opts = {}) {
16675
16753
  const skipped = [];
16676
16754
  const seen = /* @__PURE__ */ new Map();
16677
16755
  for (const file of files.sort()) {
16678
- const size = pngSize(await readFile20(file));
16756
+ const size = pngSize(await readFile21(file));
16679
16757
  if (!size) {
16680
16758
  skipped.push(`${file} (not a readable PNG)`);
16681
16759
  continue;
@@ -16743,7 +16821,7 @@ async function writeManifestFile(target, manifest) {
16743
16821
  if (existsSync23(target)) {
16744
16822
  throw new Error(`${target} already exists; refusing to overwrite an existing manifest.`);
16745
16823
  }
16746
- await writeFile14(target, JSON.stringify(manifest, null, 2) + "\n");
16824
+ await writeFile15(target, JSON.stringify(manifest, null, 2) + "\n");
16747
16825
  }
16748
16826
 
16749
16827
  // src/pick/sheet.ts
@@ -17162,7 +17240,7 @@ async function currentArt(entry, spec) {
17162
17240
  let width = entry.width;
17163
17241
  let height = entry.height;
17164
17242
  if (contentType === "image/png") {
17165
- const size = pngSize(await readFile21(file));
17243
+ const size = pngSize(await readFile22(file));
17166
17244
  if (size) ({ width, height } = size);
17167
17245
  }
17168
17246
  return { path: file, contentType, width, height };
@@ -17695,13 +17773,13 @@ function createGenerateHandlers(opts) {
17695
17773
  }
17696
17774
 
17697
17775
  // src/gallery/editor.ts
17698
- import { readFile as readFile23 } from "fs/promises";
17776
+ import { readFile as readFile24 } from "fs/promises";
17699
17777
  import path33 from "path";
17700
17778
 
17701
17779
  // src/editor/install.ts
17702
17780
  import { createHash as createHash4 } from "crypto";
17703
17781
  import { existsSync as existsSync25 } from "fs";
17704
- import { mkdir as mkdir12, readFile as readFile22, rename as rename9, rm as rm10, stat as stat3, writeFile as writeFile15 } from "fs/promises";
17782
+ import { mkdir as mkdir12, readFile as readFile23, rename as rename10, rm as rm10, stat as stat3, writeFile as writeFile16 } from "fs/promises";
17705
17783
  import os2 from "os";
17706
17784
  import path32 from "path";
17707
17785
 
@@ -17785,7 +17863,7 @@ function editorBaseUrl(pin = EDITOR_PIN) {
17785
17863
  }
17786
17864
  async function sha256Of(file) {
17787
17865
  try {
17788
- return createHash4("sha256").update(await readFile22(file)).digest("hex");
17866
+ return createHash4("sha256").update(await readFile23(file)).digest("hex");
17789
17867
  } catch {
17790
17868
  return null;
17791
17869
  }
@@ -17859,8 +17937,8 @@ async function installEditor(opts = {}) {
17859
17937
  const final = path32.join(dir, name);
17860
17938
  const tmp = `${final}.${process.pid}.part`;
17861
17939
  try {
17862
- await writeFile15(tmp, bytes);
17863
- await rename9(tmp, final);
17940
+ await writeFile16(tmp, bytes);
17941
+ await rename10(tmp, final);
17864
17942
  } finally {
17865
17943
  await rm10(tmp, { force: true });
17866
17944
  }
@@ -17978,7 +18056,7 @@ function createGalleryEditorHandlers(opts = {}) {
17978
18056
  if (!status.installed) return null;
17979
18057
  let bytes;
17980
18058
  try {
17981
- bytes = await readFile23(path33.join(dir, name));
18059
+ bytes = await readFile24(path33.join(dir, name));
17982
18060
  } catch {
17983
18061
  verified = null;
17984
18062
  return null;
@@ -18445,7 +18523,7 @@ import { existsSync as existsSync28 } from "fs";
18445
18523
 
18446
18524
  // src/pipeline/cache-health.ts
18447
18525
  import { existsSync as existsSync27 } from "fs";
18448
- import { readFile as readFile24, readdir as readdir2, rm as rm11 } from "fs/promises";
18526
+ import { readFile as readFile25, readdir as readdir2, rm as rm11 } from "fs/promises";
18449
18527
  import path35 from "path";
18450
18528
  async function inspectCaches(lock, lockPath, options = {}) {
18451
18529
  if (options.prune && !existsSync27(lockPath)) {
@@ -18473,7 +18551,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
18473
18551
  await saveCache(remotePath, { version: 1, hashes: {} });
18474
18552
  removed.resetRemoteHashCache = true;
18475
18553
  } else if (remoteHashes.invalidIds.length) {
18476
- const cache = parseCache(JSON.parse(await readFile24(remotePath, "utf8")));
18554
+ const cache = parseCache(JSON.parse(await readFile25(remotePath, "utf8")));
18477
18555
  for (const id of remoteHashes.invalidIds) delete cache.hashes[id];
18478
18556
  removed.remoteHashEntries = remoteHashes.invalidIds.length;
18479
18557
  await saveCache(remotePath, cache);
@@ -18517,7 +18595,7 @@ async function inspectContentCache(contentDir, referenced) {
18517
18595
  const expected = mediaType ? entry.name.slice(0, -4) : "";
18518
18596
  let bytes;
18519
18597
  try {
18520
- bytes = await readFile24(file);
18598
+ bytes = await readFile25(file);
18521
18599
  report.bytes += bytes.length;
18522
18600
  } catch (err) {
18523
18601
  report.invalid.push({
@@ -18564,7 +18642,7 @@ async function inspectRemoteHashCache(remotePath) {
18564
18642
  if (!report.exists) return report;
18565
18643
  let cache;
18566
18644
  try {
18567
- cache = parseCache(JSON.parse(await readFile24(remotePath, "utf8")));
18645
+ cache = parseCache(JSON.parse(await readFile25(remotePath, "utf8")));
18568
18646
  } catch (err) {
18569
18647
  report.error = err instanceof Error ? err.message : String(err);
18570
18648
  return report;
@@ -18891,7 +18969,7 @@ async function nearestExistingDirectory(start) {
18891
18969
  }
18892
18970
 
18893
18971
  // src/pipeline/audit.ts
18894
- import { readFile as readFile25 } from "fs/promises";
18972
+ import { readFile as readFile26 } from "fs/promises";
18895
18973
  import { existsSync as existsSync31 } from "fs";
18896
18974
  import path38 from "path";
18897
18975
  function paletteDistance(asset, reference) {
@@ -18934,7 +19012,7 @@ async function auditStyle(loaded, specs, styleId, lock) {
18934
19012
  continue;
18935
19013
  }
18936
19014
  try {
18937
- const png = decodePng(await readFile25(output.absolutePath));
19015
+ const png = decodePng(await readFile26(output.absolutePath));
18938
19016
  const palette = extractPalette(png, 12);
18939
19017
  assets.push({
18940
19018
  assetId: spec.assetId,
@@ -18961,7 +19039,7 @@ async function auditStyle(loaded, specs, styleId, lock) {
18961
19039
  const abs = path38.resolve(loaded.root, img.path);
18962
19040
  if (!existsSync31(abs)) continue;
18963
19041
  try {
18964
- refPalettes.push(extractPalette(decodePng(await readFile25(abs)), 12));
19042
+ refPalettes.push(extractPalette(decodePng(await readFile26(abs)), 12));
18965
19043
  } catch {
18966
19044
  }
18967
19045
  }
@@ -19190,7 +19268,7 @@ async function runAudit(args) {
19190
19268
 
19191
19269
  // src/cli/commands/pack.ts
19192
19270
  import path40 from "path";
19193
- import { readFile as readFile26 } from "fs/promises";
19271
+ import { readFile as readFile27 } from "fs/promises";
19194
19272
 
19195
19273
  // src/pipeline/pack.ts
19196
19274
  import { readFileSync as readFileSync5 } from "fs";
@@ -19903,7 +19981,7 @@ async function runPack(args) {
19903
19981
  if (args.primaryOnly || args.outputRoles.length) {
19904
19982
  throw new Error("--primary-only and --output-role require manifest-driven pack");
19905
19983
  }
19906
- const raw = JSON.parse(await readFile26(path40.resolve(args.inputs), "utf8"));
19984
+ const raw = JSON.parse(await readFile27(path40.resolve(args.inputs), "utf8"));
19907
19985
  const inputs = resolvePackInputs(raw, args.inputs);
19908
19986
  const { png, atlas, skipped, sources } = packSprites(inputs, { columns: args.columns });
19909
19987
  const format = sheetFormat(args);
@@ -20132,7 +20210,7 @@ import path42 from "path";
20132
20210
 
20133
20211
  // src/pixellab-utils.ts
20134
20212
  import { existsSync as existsSync32 } from "fs";
20135
- import { mkdir as mkdir13, readFile as readFile27, writeFile as writeFile16 } from "fs/promises";
20213
+ import { mkdir as mkdir13, readFile as readFile28, writeFile as writeFile17 } from "fs/promises";
20136
20214
  import path41 from "path";
20137
20215
  var UNZOOM_MIN_SIDE = 256;
20138
20216
  var UNZOOM_MAX_AREA = 2048 * 2048;
@@ -20144,7 +20222,7 @@ async function unzoomFile(client2, input, opts = {}) {
20144
20222
  const out = path41.resolve(opts.out ?? defaultUnzoomOut(input));
20145
20223
  if (path41.resolve(input) === out) throw new Error("unzoom will not overwrite its own input; pass a different --out");
20146
20224
  if (existsSync32(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
20147
- const bytes = await readFile27(input);
20225
+ const bytes = await readFile28(input);
20148
20226
  const metadata = imageMetadata(bytes);
20149
20227
  if (!metadata) throw new Error(`${input} is not a readable PNG or JPEG`);
20150
20228
  const { width, height, format } = metadata;
@@ -20158,7 +20236,7 @@ async function unzoomFile(client2, input, opts = {}) {
20158
20236
  }
20159
20237
  const res = await client2.unzoom({ image: { base64: bytes.toString("base64"), format }, quantize: opts.quantize });
20160
20238
  await mkdir13(path41.dirname(out), { recursive: true });
20161
- await writeFile16(out, res.png);
20239
+ await writeFile17(out, res.png);
20162
20240
  return { out, original: res.originalSize, unzoomed: res.unzoomedSize, zoomFactor: res.zoomFactor, usage: res.usage };
20163
20241
  }
20164
20242
  var FONT_WEIGHTS = ["Bold", "Regular"];
@@ -20200,8 +20278,8 @@ async function generateFont(client2, opts) {
20200
20278
  client2.download(job.download_atlas_url)
20201
20279
  ]);
20202
20280
  await mkdir13(path41.dirname(ttf), { recursive: true });
20203
- await writeFile16(ttf, ttfBytes);
20204
- await writeFile16(atlas, atlasBytes);
20281
+ await writeFile17(ttf, ttfBytes);
20282
+ await writeFile17(atlas, atlasBytes);
20205
20283
  return { jobId, ttf, atlas, glyphPx: job.glyph_px ?? null, usage: job.usage };
20206
20284
  }
20207
20285
  if (Date.now() - started > timeout) {
@@ -20282,12 +20360,12 @@ async function runFont(args) {
20282
20360
 
20283
20361
  // src/cli/commands/quality.ts
20284
20362
  import path44 from "path";
20285
- import { readFile as readFile29 } from "fs/promises";
20363
+ import { readFile as readFile30 } from "fs/promises";
20286
20364
 
20287
20365
  // src/pipeline/quality-regression.ts
20288
20366
  import path43 from "path";
20289
20367
  import { existsSync as existsSync33 } from "fs";
20290
- import { readFile as readFile28 } from "fs/promises";
20368
+ import { readFile as readFile29 } from "fs/promises";
20291
20369
  import { z as z12 } from "zod";
20292
20370
  var SHA256_RE = /^[0-9a-f]{64}$/;
20293
20371
  var HEX_RE = /^#[0-9a-f]{6}$/;
@@ -20443,7 +20521,7 @@ async function measureImageQuality(file) {
20443
20521
  const absolute = path43.resolve(file);
20444
20522
  let bytes;
20445
20523
  try {
20446
- bytes = await readFile28(absolute);
20524
+ bytes = await readFile29(absolute);
20447
20525
  } catch (error) {
20448
20526
  throw new Error(`Cannot read quality image ${absolute}: ${message4(error)}`, { cause: error });
20449
20527
  }
@@ -20530,7 +20608,7 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
20530
20608
  });
20531
20609
  const data = Buffer.from(JSON.stringify(baseline, null, 2) + "\n");
20532
20610
  if (existsSync33(absolute) && !options.force) {
20533
- const current = await readFile28(absolute);
20611
+ const current = await readFile29(absolute);
20534
20612
  if (!current.equals(data)) {
20535
20613
  throw new OverwriteRefusedError(
20536
20614
  `Quality baseline already exists with different content: ${absolute}. Pass --force to replace it.`,
@@ -20545,7 +20623,7 @@ async function readQualityBaseline(baselinePath) {
20545
20623
  const absolute = path43.resolve(baselinePath);
20546
20624
  let raw;
20547
20625
  try {
20548
- raw = JSON.parse(await readFile28(absolute, "utf8"));
20626
+ raw = JSON.parse(await readFile29(absolute, "utf8"));
20549
20627
  } catch (error) {
20550
20628
  throw new Error(`Could not read quality baseline ${absolute}: ${message4(error)}`, { cause: error });
20551
20629
  }
@@ -20675,7 +20753,7 @@ async function runQuality(args) {
20675
20753
  if (!args.out) throw new Error("quality snapshot needs --out <pixelkiln.quality.json>.");
20676
20754
  let raw;
20677
20755
  try {
20678
- raw = JSON.parse(await readFile29(path44.resolve(args.inputs), "utf8"));
20756
+ raw = JSON.parse(await readFile30(path44.resolve(args.inputs), "utf8"));
20679
20757
  } catch (error) {
20680
20758
  throw new Error(
20681
20759
  `Could not read quality inputs ${path44.resolve(args.inputs)}: ${error instanceof Error ? error.message : String(error)}`,
@@ -20727,7 +20805,7 @@ import path46 from "path";
20727
20805
  // src/recipes.ts
20728
20806
  import path45 from "path";
20729
20807
  import { existsSync as existsSync34 } from "fs";
20730
- import { readdir as readdir3, readFile as readFile30 } from "fs/promises";
20808
+ import { readdir as readdir3, readFile as readFile31 } from "fs/promises";
20731
20809
  import { fileURLToPath as fileURLToPath2 } from "url";
20732
20810
  import { z as z13 } from "zod";
20733
20811
  var SHA256_RE2 = /^[0-9a-f]{64}$/;
@@ -20915,7 +20993,7 @@ async function readRecipeFile(recipePath, bundled) {
20915
20993
  const absolute = path45.resolve(recipePath);
20916
20994
  let raw;
20917
20995
  try {
20918
- raw = JSON.parse(await readFile30(absolute, "utf8"));
20996
+ raw = JSON.parse(await readFile31(absolute, "utf8"));
20919
20997
  } catch (error) {
20920
20998
  throw new Error(`Could not read recipe ${absolute}: ${message5(error)}`, { cause: error });
20921
20999
  }
@@ -21057,12 +21135,12 @@ async function installRecipe(target, options = {}) {
21057
21135
  ];
21058
21136
  const files = await Promise.all(sources.map(async (file) => ({
21059
21137
  path: path45.join(destination, ...file.path.split("/")),
21060
- data: await readFile30(file.source)
21138
+ data: await readFile31(file.source)
21061
21139
  })));
21062
21140
  if (!options.force) {
21063
21141
  for (const file of files) {
21064
21142
  if (!existsSync34(file.path)) continue;
21065
- const current = await readFile30(file.path);
21143
+ const current = await readFile31(file.path);
21066
21144
  if (!current.equals(file.data)) {
21067
21145
  throw new OverwriteRefusedError(
21068
21146
  `Recipe destination has local changes: ${file.path}. Choose another --out or pass --force to replace declared recipe files.`,
@@ -21302,7 +21380,7 @@ async function runRefine(args) {
21302
21380
  // src/cli/commands/skeleton.ts
21303
21381
  import path48 from "path";
21304
21382
  import { existsSync as existsSync35 } from "fs";
21305
- import { readFile as readFile31, writeFile as writeFile17 } from "fs/promises";
21383
+ import { readFile as readFile32, writeFile as writeFile18 } from "fs/promises";
21306
21384
 
21307
21385
  // src/skeleton-preview.ts
21308
21386
  var RIGHT = [255, 140, 40, 255];
@@ -21418,7 +21496,7 @@ async function runEstimateSkeleton(args) {
21418
21496
  }
21419
21497
  loadEnvFiles(path48.dirname(path48.resolve(args.manifest)));
21420
21498
  loadEnvFiles(process.cwd());
21421
- const bytes = await readFile31(path48.resolve(image));
21499
+ const bytes = await readFile32(path48.resolve(image));
21422
21500
  const metadata = imageMetadata(bytes);
21423
21501
  if (!metadata) throw new UsageError(`${image} is not a readable PNG or JPEG`);
21424
21502
  const sizeProblem = estimateSkeletonSizeProblem(metadata.width, metadata.height);
@@ -21430,7 +21508,7 @@ async function runEstimateSkeleton(args) {
21430
21508
  const frames = args.frames ?? DEFAULT_SCAFFOLD_FRAMES;
21431
21509
  const json = JSON.stringify(scaffoldSkeletonSet(res.keypoints, frames), null, 2);
21432
21510
  if (args.out) {
21433
- await writeFile17(path48.resolve(args.out), json + "\n");
21511
+ await writeFile18(path48.resolve(args.out), json + "\n");
21434
21512
  log(` wrote ${args.out}: the estimated pose and ${frames} frames to edit`);
21435
21513
  log(` preview it with: pixelkiln skeleton-preview ${args.out} --from ${image}`);
21436
21514
  } else {
@@ -21462,7 +21540,7 @@ async function runSkeletonPreview(args) {
21462
21540
  if (!existsSync35(keypointsFile)) throw new UsageError(`Keypoints file does not exist yet: ${keypointsFile}`);
21463
21541
  let json;
21464
21542
  try {
21465
- json = JSON.parse(await readFile31(keypointsFile, "utf8"));
21543
+ json = JSON.parse(await readFile32(keypointsFile, "utf8"));
21466
21544
  } catch (error) {
21467
21545
  throw new UsageError(`${keypointsFile} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
21468
21546
  }
@@ -21472,13 +21550,13 @@ async function runSkeletonPreview(args) {
21472
21550
  if (!existsSync35(sourceFile)) {
21473
21551
  log(` source image does not exist yet, drawing on a plain ground: ${sourceFile}`);
21474
21552
  } else {
21475
- const bytes = await readFile31(sourceFile);
21553
+ const bytes = await readFile32(sourceFile);
21476
21554
  if (imageMetadata(bytes)?.format !== "png") throw new UsageError(`${sourceFile} must be a PNG to preview over`);
21477
21555
  source = decodePng(bytes);
21478
21556
  }
21479
21557
  }
21480
21558
  const out = path48.resolve(args.out ?? keypointsFile.replace(/\.json$/, "") + ".preview.png");
21481
- await writeFile17(out, renderSkeletonSheet(set, { source }));
21559
+ await writeFile18(out, renderSkeletonSheet(set, { source }));
21482
21560
  log(` ${label}: ${set.frames.length} frames after the starting pose`);
21483
21561
  const shown = path48.relative(process.cwd(), out);
21484
21562
  log(` wrote ${shown.startsWith("..") ? out : shown}`);
@@ -21664,7 +21742,7 @@ async function runVersion() {
21664
21742
  for (const up of ["../package.json", "../../package.json"]) {
21665
21743
  const url = new URL(up, import.meta.url);
21666
21744
  if (!existsSync37(url)) continue;
21667
- const pkg = JSON.parse(await readFile32(url, "utf8"));
21745
+ const pkg = JSON.parse(await readFile33(url, "utf8"));
21668
21746
  if (pkg.name === "pixelkiln") {
21669
21747
  log(`${pkg.name} ${pkg.version}`);
21670
21748
  return;