pixelkiln 0.68.1 → 0.70.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
@@ -67,8 +67,8 @@ function exitCodeFor(err) {
67
67
  }
68
68
 
69
69
  // src/cli/main.ts
70
- import { existsSync as existsSync33 } from "fs";
71
- import { readFile as readFile31 } from "fs/promises";
70
+ import { existsSync as existsSync35 } from "fs";
71
+ import { readFile as readFile32 } from "fs/promises";
72
72
 
73
73
  // src/cli/args.ts
74
74
  import path from "path";
@@ -107,7 +107,8 @@ var VALUE_FLAGS = [
107
107
  "--quantize",
108
108
  "--description",
109
109
  "--weight",
110
- "--glyph-px"
110
+ "--glyph-px",
111
+ "--frames"
111
112
  ];
112
113
  var BOOL_FLAGS = [
113
114
  "--force",
@@ -158,6 +159,7 @@ var COMMANDS = [
158
159
  "recipe",
159
160
  "workspace",
160
161
  "estimate-skeleton",
162
+ "skeleton-preview",
161
163
  "unzoom",
162
164
  "font",
163
165
  "help",
@@ -256,6 +258,12 @@ function parseArgs(argv) {
256
258
  target = rest[0]?.startsWith("-") ? void 0 : rest[0];
257
259
  if (target === void 0) throw new UsageError("estimate-skeleton needs an image path");
258
260
  rest = rest.slice(1);
261
+ } else if (command === "skeleton-preview") {
262
+ target = rest[0]?.startsWith("-") ? void 0 : rest[0];
263
+ if (target === void 0) {
264
+ throw new UsageError("skeleton-preview needs a keypoints file or an animate-skeleton asset id");
265
+ }
266
+ rest = rest.slice(1);
259
267
  } else if (command === "refine") {
260
268
  subcommand = rest[0]?.startsWith("-") || rest[0] === void 0 ? "run" : rest[0];
261
269
  if (!REFINE_SUBCOMMANDS.includes(subcommand)) {
@@ -421,7 +429,8 @@ function parseArgs(argv) {
421
429
  quantize: numberOption("--quantize", { min: -1, max: 256, integer: true }),
422
430
  description: get("--description"),
423
431
  weight: get("--weight"),
424
- glyphPx: numberOption("--glyph-px", { min: 8, max: 64, integer: true })
432
+ glyphPx: numberOption("--glyph-px", { min: 8, max: 64, integer: true }),
433
+ frames: numberOption("--frames", { min: 3, max: 15, integer: true })
425
434
  };
426
435
  }
427
436
 
@@ -461,11 +470,16 @@ Commands
461
470
  first. Local art is untouched.
462
471
  tag Push manifest tags to the objects upstream (free).
463
472
  balance Show the provider's remaining balance.
464
- estimate-skeleton <image> [--out <file>]
465
- PixelLab only: derive an 18-joint skeleton from an image, direct
466
- and un-budgeted (like balance, not a generation). Prints the
467
- keypoints JSON, or writes it with --out \u2014 the way to bootstrap
468
- an animate-skeleton revision's keypointsFile.
473
+ estimate-skeleton <image> [--out <file>] [--frames <n>] [--force]
474
+ PixelLab only: derive an 18-joint skeleton from a square 16-256px
475
+ image, direct and un-budgeted (like balance, not a generation).
476
+ Prints, or writes with --out, a ready keypoints file for an
477
+ animate-skeleton revision: the estimated pose plus --frames copies
478
+ (default 4) to edit. --out refuses to replace a file without --force.
479
+ skeleton-preview <keypoints.json | asset> [--from <image>] [--out <file>]
480
+ Draw every pose of a keypoints file over its source image as one
481
+ PNG, locally and free. Given an animate-skeleton asset id, reads
482
+ its keypointsFile and source from the manifest.
469
483
  status Summarise the lockfile.
470
484
  edit Hand-edit an asset in your own editor: copies the generated PNG to
471
485
  <outDir>/edits/, declares it as the asset's source, and opens it.
@@ -648,10 +662,10 @@ import { createReadStream } from "fs";
648
662
  function sha256(data) {
649
663
  return createHash("sha256").update(data).digest("hex");
650
664
  }
651
- async function sha256File(path48) {
665
+ async function sha256File(path49) {
652
666
  return new Promise((resolve, reject) => {
653
667
  const hash = createHash("sha256");
654
- const stream = createReadStream(path48);
668
+ const stream = createReadStream(path49);
655
669
  stream.on("data", (chunk2) => hash.update(chunk2));
656
670
  stream.on("error", reject);
657
671
  stream.on("end", () => resolve(hash.digest("hex")));
@@ -5756,14 +5770,61 @@ import { z as z3 } from "zod";
5756
5770
 
5757
5771
  // src/skeleton.ts
5758
5772
  import { z as z2 } from "zod";
5773
+ var SKELETON_LABELS = [
5774
+ "NOSE",
5775
+ "NECK",
5776
+ "RIGHT SHOULDER",
5777
+ "RIGHT ELBOW",
5778
+ "RIGHT ARM",
5779
+ "LEFT SHOULDER",
5780
+ "LEFT ELBOW",
5781
+ "LEFT ARM",
5782
+ "RIGHT HIP",
5783
+ "RIGHT KNEE",
5784
+ "RIGHT LEG",
5785
+ "LEFT HIP",
5786
+ "LEFT KNEE",
5787
+ "LEFT LEG",
5788
+ "RIGHT EYE",
5789
+ "LEFT EYE",
5790
+ "RIGHT EAR",
5791
+ "LEFT EAR"
5792
+ ];
5793
+ var SKELETON_BONES = [
5794
+ ["NOSE", "NECK"],
5795
+ ["NECK", "RIGHT SHOULDER"],
5796
+ ["RIGHT SHOULDER", "RIGHT ELBOW"],
5797
+ ["RIGHT ELBOW", "RIGHT ARM"],
5798
+ ["NECK", "LEFT SHOULDER"],
5799
+ ["LEFT SHOULDER", "LEFT ELBOW"],
5800
+ ["LEFT ELBOW", "LEFT ARM"],
5801
+ ["NECK", "RIGHT HIP"],
5802
+ ["RIGHT HIP", "RIGHT KNEE"],
5803
+ ["RIGHT KNEE", "RIGHT LEG"],
5804
+ ["NECK", "LEFT HIP"],
5805
+ ["LEFT HIP", "LEFT KNEE"],
5806
+ ["LEFT KNEE", "LEFT LEG"],
5807
+ ["NOSE", "RIGHT EYE"],
5808
+ ["RIGHT EYE", "RIGHT EAR"],
5809
+ ["NOSE", "LEFT EYE"],
5810
+ ["LEFT EYE", "LEFT EAR"]
5811
+ ];
5759
5812
  var SkeletonKeypointSchema = z2.object({
5760
- label: z2.string().min(1),
5813
+ label: z2.enum(SKELETON_LABELS),
5761
5814
  x: z2.number().min(0).max(1),
5762
5815
  y: z2.number().min(0).max(1),
5763
5816
  z_index: z2.number(),
5764
5817
  depth: z2.number().optional()
5765
5818
  }).strict();
5766
- var SkeletonFrameSchema = z2.array(SkeletonKeypointSchema).length(18);
5819
+ var SkeletonFrameSchema = z2.array(SkeletonKeypointSchema).length(18).superRefine((frame, context) => {
5820
+ const seen = /* @__PURE__ */ new Set();
5821
+ for (const [index, joint] of frame.entries()) {
5822
+ if (seen.has(joint.label)) {
5823
+ context.addIssue({ code: z2.ZodIssueCode.custom, message: `"${joint.label}" appears twice`, path: [index, "label"] });
5824
+ }
5825
+ seen.add(joint.label);
5826
+ }
5827
+ });
5767
5828
  var SkeletonSetSchema = z2.object({
5768
5829
  firstFrameKeypoints: SkeletonFrameSchema,
5769
5830
  frames: z2.array(SkeletonFrameSchema).min(3).max(15)
@@ -5776,6 +5837,15 @@ function parseSkeletonSet(json, file) {
5776
5837
  }
5777
5838
  return result.data;
5778
5839
  }
5840
+ var ESTIMATE_SKELETON_SIZES = [16, 32, 64, 128, 256];
5841
+ function estimateSkeletonSizeProblem(width, height) {
5842
+ return width === height && ESTIMATE_SKELETON_SIZES.includes(width) ? null : `${width}x${height}; estimate-skeleton takes a square image of ${ESTIMATE_SKELETON_SIZES.join(", ")} pixels`;
5843
+ }
5844
+ var DEFAULT_SCAFFOLD_FRAMES = 4;
5845
+ function scaffoldSkeletonSet(keypoints, frames = DEFAULT_SCAFFOLD_FRAMES) {
5846
+ const copy = () => keypoints.map((joint) => ({ ...joint }));
5847
+ return { firstFrameKeypoints: copy(), frames: Array.from({ length: frames }, copy) };
5848
+ }
5779
5849
 
5780
5850
  // src/client.ts
5781
5851
  var BASE = process.env.PIXELLAB_API_BASE ?? "https://api.pixellab.ai/v2";
@@ -6090,9 +6160,9 @@ var PixelLabClient = class {
6090
6160
  apiKey;
6091
6161
  http;
6092
6162
  /** Retries transport failures, 429, and 5xx; see http.ts for the policy. */
6093
- async request(path48, init) {
6163
+ async request(path49, init) {
6094
6164
  const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
6095
- const res = await this.http(`${BASE}${path48}`, {
6165
+ const res = await this.http(`${BASE}${path49}`, {
6096
6166
  ...init,
6097
6167
  headers: {
6098
6168
  Authorization: auth,
@@ -6102,13 +6172,13 @@ var PixelLabClient = class {
6102
6172
  });
6103
6173
  const text = await res.text();
6104
6174
  if (!res.ok) {
6105
- throw new PixelLabError(`${init?.method ?? "GET"} ${path48} \u2192 ${res.status}`, res.status, text);
6175
+ throw new PixelLabError(`${init?.method ?? "GET"} ${path49} \u2192 ${res.status}`, res.status, text);
6106
6176
  }
6107
6177
  if (!text) return {};
6108
6178
  try {
6109
6179
  return JSON.parse(text);
6110
6180
  } catch {
6111
- throw new Error(`${init?.method ?? "GET"} ${path48} returned invalid JSON`);
6181
+ throw new Error(`${init?.method ?? "GET"} ${path49} returned invalid JSON`);
6112
6182
  }
6113
6183
  }
6114
6184
  async balance() {
@@ -6500,10 +6570,10 @@ var PixelLabClient = class {
6500
6570
  const palette = args.paletteSwatchBase64 ? { color_image: { type: "base64", base64: args.paletteSwatchBase64, format: "png" }, force_colors: true } : {};
6501
6571
  const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
6502
6572
  const south = args.reference?.south;
6503
- let path48;
6573
+ let path49;
6504
6574
  let body;
6505
6575
  if (args.mode === "v3") {
6506
- path48 = "/create-character-v3";
6576
+ path49 = "/create-character-v3";
6507
6577
  body = {
6508
6578
  description: args.description,
6509
6579
  image_size: imageSize,
@@ -6517,7 +6587,7 @@ var PixelLabClient = class {
6517
6587
  ...args.enhancePrompt ? { enhance_prompt: true } : {}
6518
6588
  };
6519
6589
  } else if (args.mode === "pro-flash") {
6520
- path48 = "/create-character-pro-flash";
6590
+ path49 = "/create-character-pro-flash";
6521
6591
  const traits = args.styleTraits;
6522
6592
  body = {
6523
6593
  description: args.description,
@@ -6541,7 +6611,7 @@ var PixelLabClient = class {
6541
6611
  } : {}
6542
6612
  };
6543
6613
  } else if (args.mode === "pro") {
6544
- path48 = "/create-character-pro";
6614
+ path49 = "/create-character-pro";
6545
6615
  body = {
6546
6616
  description: args.description,
6547
6617
  image_size: imageSize,
@@ -6555,7 +6625,7 @@ var PixelLabClient = class {
6555
6625
  ...!south && args.styleCharacterId ? { style_character_id: args.styleCharacterId } : {}
6556
6626
  };
6557
6627
  } else {
6558
- path48 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
6628
+ path49 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
6559
6629
  body = {
6560
6630
  description: args.description,
6561
6631
  image_size: imageSize,
@@ -6572,8 +6642,8 @@ var PixelLabClient = class {
6572
6642
  ...palette
6573
6643
  };
6574
6644
  }
6575
- const raw = await this.request(path48, { method: "POST", body: JSON.stringify(body) });
6576
- return validateResponse(CharacterSubmitSchema, raw, path48.slice(1));
6645
+ const raw = await this.request(path49, { method: "POST", body: JSON.stringify(body) });
6646
+ return validateResponse(CharacterSubmitSchema, raw, path49.slice(1));
6577
6647
  }
6578
6648
  /** A text edit of an existing character, applied to every direction and stored as a sibling. */
6579
6649
  async createCharacterState(args) {
@@ -7128,7 +7198,7 @@ var PixelLabClient = class {
7128
7198
  * account (see `EstimateSkeletonResponseSchema`'s own caveat).
7129
7199
  */
7130
7200
  async estimateSkeleton(args) {
7131
- const body = { image: args.image };
7201
+ const body = { image: { type: "base64", base64: args.image.base64, format: args.image.format } };
7132
7202
  const res = validateResponse(
7133
7203
  EstimateSkeletonResponseSchema,
7134
7204
  await this.request("/estimate-skeleton", { method: "POST", body: JSON.stringify(body) }),
@@ -9420,9 +9490,9 @@ var RetroDiffusionClient = class {
9420
9490
  if (!response.ok) throw new ProviderError("retrodiffusion", `Retro Diffusion download failed (${response.status})`, { status: response.status });
9421
9491
  return Buffer.from(await response.arrayBuffer());
9422
9492
  }
9423
- async call(path48, init = {}) {
9493
+ async call(path49, init = {}) {
9424
9494
  if (!this.token) throw new Error("RD_API_KEY is not set");
9425
- const response = await this.request(`${this.baseUrl}${path48}`, {
9495
+ const response = await this.request(`${this.baseUrl}${path49}`, {
9426
9496
  ...init,
9427
9497
  headers: {
9428
9498
  "Content-Type": "application/json",
@@ -11727,10 +11797,10 @@ function isSha256Hash(value) {
11727
11797
  function parseCache(value) {
11728
11798
  return HashCacheSchema.parse(value);
11729
11799
  }
11730
- async function loadCache(path48) {
11731
- if (!existsSync10(path48)) return { version: 1, hashes: {} };
11800
+ async function loadCache(path49) {
11801
+ if (!existsSync10(path49)) return { version: 1, hashes: {} };
11732
11802
  try {
11733
- const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile8(path48, "utf8")));
11803
+ const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile8(path49, "utf8")));
11734
11804
  if (!parsed.success) return { version: 1, hashes: {} };
11735
11805
  return {
11736
11806
  version: 1,
@@ -11742,18 +11812,18 @@ async function loadCache(path48) {
11742
11812
  return { version: 1, hashes: {} };
11743
11813
  }
11744
11814
  }
11745
- async function saveCache(path48, cache) {
11815
+ async function saveCache(path49, cache) {
11746
11816
  const sorted = {};
11747
11817
  for (const key of Object.keys(cache.hashes).sort()) {
11748
11818
  const hash = cache.hashes[key];
11749
11819
  if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
11750
11820
  sorted[key] = hash;
11751
11821
  }
11752
- await mkdir5(pathModule.dirname(pathModule.resolve(path48)), { recursive: true });
11753
- const tmp = `${path48}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
11822
+ await mkdir5(pathModule.dirname(pathModule.resolve(path49)), { recursive: true });
11823
+ const tmp = `${path49}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
11754
11824
  try {
11755
11825
  await writeFile5(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
11756
- await rename5(tmp, path48);
11826
+ await rename5(tmp, path49);
11757
11827
  } finally {
11758
11828
  await rm5(tmp, { force: true });
11759
11829
  }
@@ -13757,15 +13827,27 @@ var NewAssetSchema = z6.object({
13757
13827
  /**
13758
13828
  * A controlled regeneration of another asset in the same manifest,
13759
13829
  * instead of a fresh generation. `inpaint` needs a mask upload the
13760
- * gallery does not offer yet, so only `image-to-image` can be created
13761
- * here; hand-edit the manifest for the other modes.
13830
+ * gallery does not offer yet, so only `image-to-image` and
13831
+ * `animate-skeleton` (whose keypoints file the gallery writes first) can
13832
+ * be created here; hand-edit the manifest for the other modes.
13762
13833
  */
13763
- revision: z6.object({
13764
- mode: z6.literal("image-to-image"),
13765
- /** Asset id of the parent this revises; must already exist. */
13766
- from: z6.string().min(1),
13767
- strength: z6.number().min(0).max(1).optional()
13768
- }).strict().optional(),
13834
+ revision: z6.discriminatedUnion("mode", [
13835
+ z6.object({
13836
+ mode: z6.literal("image-to-image"),
13837
+ /** Asset id of the parent this revises; must already exist. */
13838
+ from: z6.string().min(1),
13839
+ strength: z6.number().min(0).max(1).optional()
13840
+ }).strict(),
13841
+ z6.object({
13842
+ mode: z6.literal("animate-skeleton"),
13843
+ from: z6.string().min(1),
13844
+ /** Manifest-relative; checked when the manifest is loaded, like every revision input. */
13845
+ keypointsFile: z6.string().min(1),
13846
+ direction: CharacterDirectionSchema,
13847
+ description: z6.string().min(1).optional(),
13848
+ skeletonTemplate: z6.string().min(1).optional()
13849
+ }).strict()
13850
+ ]).optional(),
13769
13851
  /** A pose or outfit of an existing `character`/`objectPro` base or state. */
13770
13852
  state: NewStateSchema.optional(),
13771
13853
  /** A loop of an existing `character`/`objectPro` base or state. */
@@ -14290,52 +14372,165 @@ async function runEdit(args) {
14290
14372
  }
14291
14373
 
14292
14374
  // src/cli/commands/gallery.ts
14293
- import path32 from "path";
14294
- import { existsSync as existsSync23 } from "fs";
14375
+ import path33 from "path";
14376
+ import { existsSync as existsSync24 } from "fs";
14295
14377
 
14296
14378
  // src/gallery/snapshot.ts
14297
14379
  import { createHash as createHash3 } from "crypto";
14298
- import { existsSync as existsSync19 } from "fs";
14299
- import { readFile as readFile17, stat as stat2 } from "fs/promises";
14300
- import path28 from "path";
14380
+ import { existsSync as existsSync20 } from "fs";
14381
+ import { readFile as readFile18, stat as stat2 } from "fs/promises";
14382
+ import path29 from "path";
14301
14383
 
14302
14384
  // src/gallery/edit.ts
14385
+ import path27 from "path";
14386
+ import { z as z9 } from "zod";
14387
+
14388
+ // src/gallery/skeleton.ts
14389
+ import { existsSync as existsSync18 } from "fs";
14390
+ import { mkdir as mkdir10, readFile as readFile17, rm as rm9, writeFile as writeFile12 } from "fs/promises";
14303
14391
  import path26 from "path";
14304
14392
  import { z as z8 } from "zod";
14305
- var Base64 = z8.string().regex(/^[A-Za-z0-9+/]*={0,2}$/).max(Math.ceil(MAX_HAND_EDIT_BYTES / 3) * 4);
14306
- var HandEditRequestSchema = z8.discriminatedUnion("action", [
14307
- z8.object({
14308
- action: z8.literal("start-edit"),
14309
- project: z8.string().min(1).optional(),
14310
- assetId: z8.string().min(1),
14311
- styleId: z8.string().min(1),
14312
- expectedSha256: z8.string().regex(/^[0-9a-f]{64}$/),
14393
+ var SkeletonEstimateRequestSchema = z8.object({
14394
+ project: z8.string().min(1).optional(),
14395
+ styleId: z8.string().min(1),
14396
+ assetId: z8.string().min(1),
14397
+ frames: z8.number().int().min(3).max(15).optional()
14398
+ }).strict();
14399
+ var SkeletonAnimationRequestSchema = z8.object({
14400
+ action: z8.literal("create-skeleton-animation"),
14401
+ project: z8.string().min(1).optional(),
14402
+ expectedSha256: z8.string().regex(/^[0-9a-f]{64}$/),
14403
+ styleId: z8.string().min(1),
14404
+ /** The sprite the animation poses. */
14405
+ from: z8.string().min(1),
14406
+ assetId: z8.string().min(1).regex(/^[^/\\]+$/, "asset ids cannot contain slashes"),
14407
+ /** The motion, sent as PixelLab's `action`. */
14408
+ prompt: z8.string().min(1),
14409
+ direction: CharacterDirectionSchema,
14410
+ description: z8.string().min(1).optional(),
14411
+ skeletonTemplate: z8.string().min(1).optional(),
14412
+ /** Manifest-relative JSON path inside the project. */
14413
+ keypointsFile: z8.string().min(1),
14414
+ /** Poses to write to `keypointsFile`; omitted to use a file that already exists. */
14415
+ set: SkeletonSetSchema.optional(),
14416
+ /** Replace an existing `keypointsFile` with `set`. */
14417
+ overwrite: z8.boolean().optional()
14418
+ }).strict();
14419
+ function skeletonSourceFile(spec) {
14420
+ if (spec.source) return path26.resolve(spec.root, spec.source);
14421
+ if (spec.quality && existsSync18(spec.quality.outFile)) return spec.quality.outFile;
14422
+ return spec.outFile;
14423
+ }
14424
+ function createGallerySkeletonHandlers(opts) {
14425
+ const log2 = opts.onProgress ?? (() => {
14426
+ });
14427
+ return {
14428
+ async estimate(body) {
14429
+ const parsed = SkeletonEstimateRequestSchema.safeParse(body);
14430
+ if (!parsed.success) throw new ManifestEditError(`invalid estimate request: ${parsed.error.issues[0]?.message}`);
14431
+ const request = parsed.data;
14432
+ const ctx = await opts.loadProject(request.project);
14433
+ const key = lockKey(request.styleId, request.assetId);
14434
+ const spec = ctx.specs.find((candidate) => lockKey(candidate.styleId, candidate.assetId) === key);
14435
+ if (!spec) throw new ManifestEditError(`"${key}" is not declared by the manifest`);
14436
+ const file = skeletonSourceFile(spec);
14437
+ if (!existsSync18(file)) throw new ManifestEditError(`${key} has no image on disk to estimate from`);
14438
+ const bytes = await readFile17(file);
14439
+ const metadata = imageMetadata(bytes);
14440
+ if (!metadata) throw new ManifestEditError(`${path26.basename(file)} is not a readable PNG or JPEG`);
14441
+ const problem = estimateSkeletonSizeProblem(metadata.width, metadata.height);
14442
+ if (problem) throw new ManifestEditError(`${key} is ${problem}`);
14443
+ const client2 = (opts.client ?? clientFromEnv)();
14444
+ const res = await client2.estimateSkeleton({ image: { base64: bytes.toString("base64"), format: metadata.format } });
14445
+ log2(` estimated a skeleton for ${key} (PixelLab estimate-skeleton, not budgeted)`);
14446
+ return {
14447
+ set: scaffoldSkeletonSet(res.keypoints, request.frames ?? DEFAULT_SCAFFOLD_FRAMES),
14448
+ source: path26.relative(ctx.loaded.root, file).split(path26.sep).join("/")
14449
+ };
14450
+ }
14451
+ };
14452
+ }
14453
+ async function createSkeletonAnimation(manifestPath, request) {
14454
+ const root = path26.dirname(path26.resolve(manifestPath));
14455
+ const file = path26.resolve(root, request.keypointsFile);
14456
+ const relative = path26.relative(root, file);
14457
+ if (!request.keypointsFile.endsWith(".json") || relative.startsWith("..") || path26.isAbsolute(relative)) {
14458
+ throw new ManifestEditError("the keypoints file must be a .json path inside the project");
14459
+ }
14460
+ const existed = existsSync18(file);
14461
+ let previous = null;
14462
+ if (request.set) {
14463
+ if (existed && !request.overwrite) {
14464
+ throw new ManifestEditError(`${request.keypointsFile} already exists; choose another path, or replace it`);
14465
+ }
14466
+ if (existed) previous = await readFile17(file);
14467
+ await mkdir10(path26.dirname(file), { recursive: true });
14468
+ await writeFile12(file, JSON.stringify(request.set, null, 2) + "\n");
14469
+ } else if (!existed) {
14470
+ throw new ManifestEditError(`${request.keypointsFile} does not exist; paste poses or estimate them from the sprite`);
14471
+ }
14472
+ try {
14473
+ await applyManifestEdit(manifestPath, {
14474
+ action: "add-asset",
14475
+ assetId: request.assetId,
14476
+ expectedSha256: request.expectedSha256,
14477
+ asset: {
14478
+ prompt: request.prompt,
14479
+ styles: [request.styleId],
14480
+ revision: {
14481
+ mode: "animate-skeleton",
14482
+ from: request.from,
14483
+ keypointsFile: relative.split(path26.sep).join("/"),
14484
+ direction: request.direction,
14485
+ ...request.description ? { description: request.description } : {},
14486
+ ...request.skeletonTemplate ? { skeletonTemplate: request.skeletonTemplate } : {}
14487
+ }
14488
+ }
14489
+ });
14490
+ } catch (error) {
14491
+ if (request.set) {
14492
+ if (previous) await writeFile12(file, previous);
14493
+ else await rm9(file, { force: true });
14494
+ }
14495
+ throw error;
14496
+ }
14497
+ }
14498
+
14499
+ // src/gallery/edit.ts
14500
+ var Base64 = z9.string().regex(/^[A-Za-z0-9+/]*={0,2}$/).max(Math.ceil(MAX_HAND_EDIT_BYTES / 3) * 4);
14501
+ var HandEditRequestSchema = z9.discriminatedUnion("action", [
14502
+ z9.object({
14503
+ action: z9.literal("start-edit"),
14504
+ project: z9.string().min(1).optional(),
14505
+ assetId: z9.string().min(1),
14506
+ styleId: z9.string().min(1),
14507
+ expectedSha256: z9.string().regex(/^[0-9a-f]{64}$/),
14313
14508
  /** Also launch the author's editor on the file. */
14314
- open: z8.boolean().optional()
14509
+ open: z9.boolean().optional()
14315
14510
  }).strict(),
14316
- z8.object({
14317
- action: z8.literal("detach-edit"),
14318
- project: z8.string().min(1).optional(),
14319
- assetId: z8.string().min(1),
14320
- styleId: z8.string().min(1),
14321
- expectedSha256: z8.string().regex(/^[0-9a-f]{64}$/)
14511
+ z9.object({
14512
+ action: z9.literal("detach-edit"),
14513
+ project: z9.string().min(1).optional(),
14514
+ assetId: z9.string().min(1),
14515
+ styleId: z9.string().min(1),
14516
+ expectedSha256: z9.string().regex(/^[0-9a-f]{64}$/)
14322
14517
  }).strict(),
14323
14518
  /** From the in-browser editor: the page holds the bytes, the server validates and writes. */
14324
- z8.object({
14325
- action: z8.literal("save-edit"),
14326
- project: z8.string().min(1).optional(),
14327
- assetId: z8.string().min(1),
14328
- styleId: z8.string().min(1),
14329
- expectedSha256: z8.string().regex(/^[0-9a-f]{64}$/),
14519
+ z9.object({
14520
+ action: z9.literal("save-edit"),
14521
+ project: z9.string().min(1).optional(),
14522
+ assetId: z9.string().min(1),
14523
+ styleId: z9.string().min(1),
14524
+ expectedSha256: z9.string().regex(/^[0-9a-f]{64}$/),
14330
14525
  /** The flattened image; for a frame set, `frames` instead. */
14331
14526
  png: Base64.min(1).optional(),
14332
14527
  /** Every frame of a set, tagged with the role it was opened under (null for one added in the editor). */
14333
- frames: z8.array(z8.object({ role: z8.string().min(1).nullable(), png: Base64.min(1) }).strict()).min(1).max(256).optional(),
14528
+ frames: z9.array(z9.object({ role: z9.string().min(1).nullable(), png: Base64.min(1) }).strict()).min(1).max(256).optional(),
14334
14529
  /** The editor's layered project file, kept beside the edit. */
14335
14530
  pxo: Base64.optional(),
14336
14531
  /** Editor identity from the bridge's ready message. */
14337
- editor: z8.string().min(1).max(200),
14338
- protocol: z8.number().int().positive()
14532
+ editor: z9.string().min(1).max(200),
14533
+ protocol: z9.number().int().positive()
14339
14534
  }).strict()
14340
14535
  ]);
14341
14536
  function createGalleryEditHandler(opts) {
@@ -14376,6 +14571,23 @@ function createGalleryEditHandler(opts) {
14376
14571
  }
14377
14572
  return opts.reload();
14378
14573
  }
14574
+ const skeleton = SkeletonAnimationRequestSchema.safeParse(body);
14575
+ if (skeleton.success) {
14576
+ let manifestPath2;
14577
+ try {
14578
+ manifestPath2 = await opts.manifestFor(skeleton.data.project);
14579
+ } catch (error) {
14580
+ throw new ManifestEditError(error instanceof Error ? error.message : String(error));
14581
+ }
14582
+ await createSkeletonAnimation(manifestPath2, skeleton.data);
14583
+ log2(` manifest edited: added skeleton animation ${skeleton.data.assetId} (${skeleton.data.keypointsFile})`);
14584
+ return opts.reload();
14585
+ }
14586
+ if (body?.action === "create-skeleton-animation") {
14587
+ throw new ManifestEditError(
14588
+ "invalid skeleton animation: " + skeleton.error.issues.slice(0, 3).map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")
14589
+ );
14590
+ }
14379
14591
  const parsed = ManifestEditSchema.safeParse(body);
14380
14592
  if (!parsed.success) {
14381
14593
  throw new ManifestEditError(
@@ -14390,7 +14602,7 @@ function createGalleryEditHandler(opts) {
14390
14602
  }
14391
14603
  const result = await applyManifestEdit(manifestPath, parsed.data);
14392
14604
  if (result.changed) {
14393
- const relative = path26.relative(process.cwd(), result.manifestPath);
14605
+ const relative = path27.relative(process.cwd(), result.manifestPath);
14394
14606
  const shown = !relative || relative.startsWith("..") ? result.manifestPath : relative;
14395
14607
  const subject = parsed.data.action === "patch-style" ? `style ${parsed.data.styleId}` : parsed.data.action === "set-source" || parsed.data.action === "clear-source" ? `${parsed.data.styleId}/${parsed.data.assetId} source` : parsed.data.assetId;
14396
14608
  log2(` manifest edited: ${parsed.data.action === "add-asset" ? "added" : "changed"} ${subject} in ${shown}`);
@@ -14400,8 +14612,8 @@ function createGalleryEditHandler(opts) {
14400
14612
  }
14401
14613
 
14402
14614
  // src/pipeline/history.ts
14403
- import { existsSync as existsSync18 } from "fs";
14404
- import path27 from "path";
14615
+ import { existsSync as existsSync19 } from "fs";
14616
+ import path28 from "path";
14405
14617
  var HISTORY_ENV = "PIXELKILN_HISTORY";
14406
14618
  function historyLimit(manifest, env = process.env) {
14407
14619
  if (manifest?.history !== void 0) return manifest.history;
@@ -14488,10 +14700,10 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
14488
14700
  }
14489
14701
  for (let i = 0; i < entry.outputs.length; i++) {
14490
14702
  const file = currentEntryOutputPath(entry, spec, i);
14491
- if (!existsSync18(file)) continue;
14703
+ if (!existsSync19(file)) continue;
14492
14704
  if (await sha256File(file) !== entry.outputs[i].sha256 && !opts.force) {
14493
14705
  throw new OverwriteRefusedError(
14494
- `${path27.relative(process.cwd(), file)} was changed after download; keep it with pixelkiln edit, or pass --force to replace it`,
14706
+ `${path28.relative(process.cwd(), file)} was changed after download; keep it with pixelkiln edit, or pass --force to replace it`,
14495
14707
  [file]
14496
14708
  );
14497
14709
  }
@@ -14544,7 +14756,7 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
14544
14756
  // src/gallery/snapshot.ts
14545
14757
  var PROJECT_FILE_TYPE = "application/octet-stream";
14546
14758
  function galleryMediaId(absolutePath) {
14547
- return createHash3("sha256").update(path28.resolve(absolutePath)).digest("hex").slice(0, 24);
14759
+ return createHash3("sha256").update(path29.resolve(absolutePath)).digest("hex").slice(0, 24);
14548
14760
  }
14549
14761
  function galleryMediaRoute(id) {
14550
14762
  return `/media/${id}`;
@@ -14563,9 +14775,9 @@ function describeHistory(media, entry, cacheDir) {
14563
14775
  return (entry.history ?? []).map((generation, i) => {
14564
14776
  const outputs = generation.outputs.map((output) => {
14565
14777
  const mediaType = output.mediaType ?? MediaType.PNG;
14566
- const file = path28.join(cacheDir, cacheFileName(output.sha256, mediaType));
14778
+ const file = path29.join(cacheDir, cacheFileName(output.sha256, mediaType));
14567
14779
  let url = null;
14568
- if (existsSync19(file)) {
14780
+ if (existsSync20(file)) {
14569
14781
  const id = galleryMediaId(file);
14570
14782
  media.set(id, { path: file, contentType: mediaType });
14571
14783
  url = `${galleryMediaRoute(id)}?v=${output.sha256.slice(0, 24)}`;
@@ -14597,7 +14809,7 @@ function describeHistory(media, entry, cacheDir) {
14597
14809
  var MAX_COMPARED_BYTES = 8 * 1024 * 1024;
14598
14810
  async function samePixels(a, b) {
14599
14811
  try {
14600
- const [left, right] = await Promise.all([readFile17(a), readFile17(b)]);
14812
+ const [left, right] = await Promise.all([readFile18(a), readFile18(b)]);
14601
14813
  if (left.length > MAX_COMPARED_BYTES || right.length > MAX_COMPARED_BYTES) return false;
14602
14814
  const x = decodePng(left);
14603
14815
  const y = decodePng(right);
@@ -14695,10 +14907,10 @@ async function describeOutput(media, root, absolutePath, recorded = {}) {
14695
14907
  async function describeQuality(media, root, inspection) {
14696
14908
  const outputPaths = inspection.outputs ?? [inspection.output];
14697
14909
  const outputs = await Promise.all(
14698
- outputPaths.map((file) => describeOutput(media, root, path28.resolve(root, file)))
14910
+ outputPaths.map((file) => describeOutput(media, root, path29.resolve(root, file)))
14699
14911
  );
14700
- const recordPath = path28.resolve(root, inspection.record);
14701
- const recordExists = existsSync19(recordPath);
14912
+ const recordPath = path29.resolve(root, inspection.record);
14913
+ const recordExists = existsSync20(recordPath);
14702
14914
  let options = null;
14703
14915
  let check = null;
14704
14916
  if (recordExists) {
@@ -14717,7 +14929,7 @@ async function describeQuality(media, root, inspection) {
14717
14929
  return {
14718
14930
  state: inspection.state,
14719
14931
  reason: inspection.reason,
14720
- output: portableOutputPath(path28.resolve(root, inspection.output), root),
14932
+ output: portableOutputPath(path29.resolve(root, inspection.output), root),
14721
14933
  record: portableOutputPath(recordPath, root),
14722
14934
  recordExists,
14723
14935
  outputs,
@@ -14755,9 +14967,9 @@ async function buildGallerySnapshot(opts) {
14755
14967
  const media = /* @__PURE__ */ new Map();
14756
14968
  const plan = await buildPlan(specs, lock);
14757
14969
  const items = [];
14758
- const manifestText = await readFile17(loaded.path, "utf8");
14970
+ const manifestText = await readFile18(loaded.path, "utf8");
14759
14971
  const manifestSha256 = sha256(manifestText);
14760
- const cacheDir = path28.resolve(path28.dirname(opts.lockPath), ".pixelkiln", "cache");
14972
+ const cacheDir = path29.resolve(path29.dirname(opts.lockPath), ".pixelkiln", "cache");
14761
14973
  let rawStyles = {};
14762
14974
  try {
14763
14975
  const raw = JSON.parse(manifestText);
@@ -14781,12 +14993,12 @@ async function buildGallerySnapshot(opts) {
14781
14993
  )
14782
14994
  );
14783
14995
  if (spec.source) {
14784
- const editPath = path28.resolve(root, spec.source);
14996
+ const editPath = path29.resolve(root, spec.source);
14785
14997
  const memberCount = sourceIsStem(spec.source, entry, root) ? entry.outputs.length : 1;
14786
14998
  const editShas = await Promise.all(
14787
14999
  Array.from({ length: memberCount }, async (_, index) => {
14788
15000
  const file = sourceOutputPath(spec.source, entry, index, root);
14789
- return { file, sha256: existsSync19(file) ? await sha256File(file) : null };
15001
+ return { file, sha256: existsSync20(file) ? await sha256File(file) : null };
14790
15002
  })
14791
15003
  );
14792
15004
  edits = await Promise.all(editShas.map(({ file, sha256: editSha }) => describeOutput(media, root, file, { sha256: editSha })));
@@ -14794,7 +15006,7 @@ async function buildGallerySnapshot(opts) {
14794
15006
  const companion = await readHandEditCompanion(editPath);
14795
15007
  if (companion) {
14796
15008
  const projectPath = handEditProjectPath(editPath);
14797
- const project = companion.project && existsSync19(projectPath) ? await fileInfo(projectPath) : null;
15009
+ const project = companion.project && existsSync20(projectPath) ? await fileInfo(projectPath) : null;
14798
15010
  let projectUrl = null;
14799
15011
  if (project) {
14800
15012
  const id = galleryMediaId(projectPath);
@@ -14830,8 +15042,8 @@ async function buildGallerySnapshot(opts) {
14830
15042
  editStatus = edits.some((member) => !member.exists) ? "missing" : editChanged.every((changed) => !changed) ? "same" : regenerated ? "regenerated-since" : "edited";
14831
15043
  }
14832
15044
  } else if (spec.source) {
14833
- outputs = [await describeOutput(media, root, path28.resolve(root, spec.source))];
14834
- } else if (!entry && existsSync19(spec.outFile)) {
15045
+ outputs = [await describeOutput(media, root, path29.resolve(root, spec.source))];
15046
+ } else if (!entry && existsSync20(spec.outFile)) {
14835
15047
  outputs = [await describeOutput(media, root, spec.outFile)];
14836
15048
  } else {
14837
15049
  outputs = [];
@@ -14872,6 +15084,10 @@ async function buildGallerySnapshot(opts) {
14872
15084
  revision: entry?.revision ?? null,
14873
15085
  revisionParentKey: spec.revision ? lockKey(spec.styleId, spec.revision.sourceAssetId) : entry?.revision ? lockKey(spec.styleId, entry.revision.sourceAssetId) : null,
14874
15086
  mirrorOfKey: spec.mirror ? lockKey(spec.styleId, spec.mirror.sourceAssetId) : entry?.mirror ? lockKey(spec.styleId, entry.mirror.sourceAssetId) : null,
15087
+ skeleton: spec.revision?.mode === "animate-skeleton" && spec.revision.keypointsFile ? {
15088
+ keypointsFile: portableOutputPath(spec.revision.keypointsFile, root),
15089
+ set: spec.revision.skeleton ?? null
15090
+ } : null,
14875
15091
  character: describeCharacter(spec, entry),
14876
15092
  outputs,
14877
15093
  quality,
@@ -14936,6 +15152,7 @@ async function buildGallerySnapshot(opts) {
14936
15152
  revision: entry.revision,
14937
15153
  revisionParentKey: entry.revision ? lockKey(entry.styleId, entry.revision.sourceAssetId) : null,
14938
15154
  mirrorOfKey: entry.mirror ? lockKey(entry.styleId, entry.mirror.sourceAssetId) : null,
15155
+ skeleton: null,
14939
15156
  character: describeCharacter(void 0, entry),
14940
15157
  outputs,
14941
15158
  quality: null,
@@ -15026,7 +15243,7 @@ async function buildGallerySnapshot(opts) {
15026
15243
  id: loaded.manifest.name,
15027
15244
  name: loaded.manifest.name,
15028
15245
  manifest: loaded.path,
15029
- lock: path28.resolve(opts.lockPath),
15246
+ lock: path29.resolve(opts.lockPath),
15030
15247
  root,
15031
15248
  provider: loaded.manifest.provider,
15032
15249
  account: null,
@@ -15052,7 +15269,7 @@ async function buildGallerySnapshot(opts) {
15052
15269
  return { snapshot, media };
15053
15270
  }
15054
15271
  async function buildWorkspaceGallerySnapshot(opts) {
15055
- const dir = path28.dirname(path28.resolve(opts.workspacePath));
15272
+ const dir = path29.dirname(path29.resolve(opts.workspacePath));
15056
15273
  const media = /* @__PURE__ */ new Map();
15057
15274
  const projects = [];
15058
15275
  const items = [];
@@ -15095,11 +15312,11 @@ async function buildWorkspaceGallerySnapshot(opts) {
15095
15312
  id: project.id,
15096
15313
  name: loaded.manifest.name,
15097
15314
  manifest: manifestPath,
15098
- lock: path28.resolve(lockPath),
15315
+ lock: path29.resolve(lockPath),
15099
15316
  root: loaded.root,
15100
15317
  provider: loaded.manifest.provider,
15101
15318
  account: project.account ?? null,
15102
- manifestSha256: manifestSha256 ?? sha256(await readFile17(manifestPath, "utf8")),
15319
+ manifestSha256: manifestSha256 ?? sha256(await readFile18(manifestPath, "utf8")),
15103
15320
  historyLimit: historyLimit(loaded.manifest),
15104
15321
  entries: Object.keys(lock.entries).length,
15105
15322
  items: projectItems.length,
@@ -15111,8 +15328,8 @@ async function buildWorkspaceGallerySnapshot(opts) {
15111
15328
  id: project.id,
15112
15329
  name: project.id,
15113
15330
  manifest: manifestPath,
15114
- lock: path28.resolve(lockPath),
15115
- root: path28.dirname(manifestPath),
15331
+ lock: path29.resolve(lockPath),
15332
+ root: path29.dirname(manifestPath),
15116
15333
  provider: project.provider,
15117
15334
  account: project.account ?? null,
15118
15335
  manifestSha256: null,
@@ -15139,7 +15356,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
15139
15356
  version: 1,
15140
15357
  generatedAt: (opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
15141
15358
  project: null,
15142
- workspace: { path: path28.resolve(opts.workspacePath), dir, projects },
15359
+ workspace: { path: path29.resolve(opts.workspacePath), dir, projects },
15143
15360
  filter,
15144
15361
  totals: {
15145
15362
  entries: projects.reduce((sum, project) => sum + project.entries, 0),
@@ -15158,7 +15375,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
15158
15375
  // src/gallery/server.ts
15159
15376
  import { randomBytes } from "crypto";
15160
15377
  import { createServer as createServer2 } from "http";
15161
- import { readFile as readFile18 } from "fs/promises";
15378
+ import { readFile as readFile19 } from "fs/promises";
15162
15379
 
15163
15380
  // src/gallery/page.ts
15164
15381
  import { readFileSync as readFileSync4 } from "fs";
@@ -15268,7 +15485,7 @@ async function readJsonBody(req) {
15268
15485
  async function serveGallery(opts) {
15269
15486
  const log2 = opts.onProgress ?? (() => {
15270
15487
  });
15271
- const session = opts.edit || opts.generate || opts.editor ? randomBytes(16).toString("hex") : null;
15488
+ const session = opts.edit || opts.generate || opts.editor || opts.skeleton ? randomBytes(16).toString("hex") : null;
15272
15489
  let media = /* @__PURE__ */ new Map();
15273
15490
  let loading = null;
15274
15491
  const reviewAssets = /* @__PURE__ */ new Map();
@@ -15343,6 +15560,17 @@ async function serveGallery(opts) {
15343
15560
  }
15344
15561
  return;
15345
15562
  }
15563
+ if (req.method === "POST" && url2.pathname === "/api/skeleton/estimate") {
15564
+ if (!opts.skeleton || !session) return fail(405, "skeleton estimates need the gallery started with --edit");
15565
+ try {
15566
+ const body = await guardedBody("skeleton estimates");
15567
+ if (body === void 0) return;
15568
+ json(200, await opts.skeleton.estimate(body));
15569
+ } catch (err) {
15570
+ reportError("skeleton estimate", err);
15571
+ }
15572
+ return;
15573
+ }
15346
15574
  if (req.method === "POST" && url2.pathname === "/api/editor/install") {
15347
15575
  if (!opts.editor || !session) return fail(405, "the in-browser editor is off for this gallery");
15348
15576
  try {
@@ -15399,7 +15627,7 @@ async function serveGallery(opts) {
15399
15627
  const asset = reviewAssets.get(jobId)?.get(url2.pathname);
15400
15628
  if (!asset) return fail(404, "no such review asset");
15401
15629
  try {
15402
- const bytes = await readFile18(asset.path);
15630
+ const bytes = await readFile19(asset.path);
15403
15631
  res.writeHead(200, {
15404
15632
  "Content-Type": asset.contentType,
15405
15633
  "Cache-Control": "no-store",
@@ -15475,7 +15703,7 @@ async function serveGallery(opts) {
15475
15703
  if (!asset) return fail(404, "no such gallery media");
15476
15704
  let bytes;
15477
15705
  try {
15478
- bytes = await readFile18(asset.path);
15706
+ bytes = await readFile19(asset.path);
15479
15707
  } catch {
15480
15708
  return fail(404, "gallery media is no longer on disk; refresh the page");
15481
15709
  }
@@ -15519,7 +15747,7 @@ async function serveGallery(opts) {
15519
15747
 
15520
15748
  // src/gallery/generate.ts
15521
15749
  import { randomBytes as randomBytes2 } from "crypto";
15522
- import { z as z9 } from "zod";
15750
+ import { z as z10 } from "zod";
15523
15751
 
15524
15752
  // src/pipeline/poll.ts
15525
15753
  var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -15930,13 +16158,13 @@ function ownedReferenceImageId(spec, lock, providerId) {
15930
16158
  }
15931
16159
 
15932
16160
  // src/pick/server.ts
15933
- import { existsSync as existsSync21 } from "fs";
15934
- import { readFile as readFile20 } from "fs/promises";
16161
+ import { existsSync as existsSync22 } from "fs";
16162
+ import { readFile as readFile21 } from "fs/promises";
15935
16163
 
15936
16164
  // src/pipeline/init.ts
15937
- import { readdir, readFile as readFile19, writeFile as writeFile12 } from "fs/promises";
15938
- import { existsSync as existsSync20 } from "fs";
15939
- import path29 from "path";
16165
+ import { readdir, readFile as readFile20, writeFile as writeFile13 } from "fs/promises";
16166
+ import { existsSync as existsSync21 } from "fs";
16167
+ import path30 from "path";
15940
16168
  function pngSize(buf) {
15941
16169
  if (buf.length < 24) return null;
15942
16170
  const isPng = buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834;
@@ -15950,7 +16178,7 @@ function slugify(value) {
15950
16178
  async function walk(dir, exclude) {
15951
16179
  const out = [];
15952
16180
  for (const entry of await readdir(dir, { withFileTypes: true })) {
15953
- const full = path29.join(dir, entry.name);
16181
+ const full = path30.join(dir, entry.name);
15954
16182
  if (exclude.some((x) => full.includes(x))) continue;
15955
16183
  if (entry.isDirectory()) out.push(...await walk(full, exclude));
15956
16184
  else if (entry.name.toLowerCase().endsWith(".png")) out.push(full);
@@ -15964,14 +16192,14 @@ async function scanAssets(root, opts = {}) {
15964
16192
  const skipped = [];
15965
16193
  const seen = /* @__PURE__ */ new Map();
15966
16194
  for (const file of files.sort()) {
15967
- const size = pngSize(await readFile19(file));
16195
+ const size = pngSize(await readFile20(file));
15968
16196
  if (!size) {
15969
16197
  skipped.push(`${file} (not a readable PNG)`);
15970
16198
  continue;
15971
16199
  }
15972
- const rel = path29.relative(root, file);
15973
- const category = path29.dirname(rel) === "." ? "" : path29.dirname(rel);
15974
- let id = slugify(path29.basename(rel));
16200
+ const rel = path30.relative(root, file);
16201
+ const category = path30.dirname(rel) === "." ? "" : path30.dirname(rel);
16202
+ let id = slugify(path30.basename(rel));
15975
16203
  if (seen.has(id)) {
15976
16204
  const n = seen.get(id) + 1;
15977
16205
  seen.set(id, n);
@@ -16029,10 +16257,10 @@ function mode(values) {
16029
16257
  return [...counts.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? 64;
16030
16258
  }
16031
16259
  async function writeManifestFile(target, manifest) {
16032
- if (existsSync20(target)) {
16260
+ if (existsSync21(target)) {
16033
16261
  throw new Error(`${target} already exists; refusing to overwrite an existing manifest.`);
16034
16262
  }
16035
- await writeFile12(target, JSON.stringify(manifest, null, 2) + "\n");
16263
+ await writeFile13(target, JSON.stringify(manifest, null, 2) + "\n");
16036
16264
  }
16037
16265
 
16038
16266
  // src/pick/sheet.ts
@@ -16445,13 +16673,13 @@ async function currentArt(entry, spec) {
16445
16673
  const previous = primaryOutput({ ...entry, outputs: entry.supersededOutputs });
16446
16674
  if (!previous) return null;
16447
16675
  const file = resolveOutputPath(previous.path, spec.root);
16448
- if (!existsSync21(file)) return null;
16676
+ if (!existsSync22(file)) return null;
16449
16677
  const contentType = previous.mediaType ?? mediaTypeFromExtension(file);
16450
16678
  if (!contentType) return null;
16451
16679
  let width = entry.width;
16452
16680
  let height = entry.height;
16453
16681
  if (contentType === "image/png") {
16454
- const size = pngSize(await readFile20(file));
16682
+ const size = pngSize(await readFile21(file));
16455
16683
  if (size) ({ width, height } = size);
16456
16684
  }
16457
16685
  return { path: file, contentType, width, height };
@@ -16619,26 +16847,26 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
16619
16847
  }
16620
16848
 
16621
16849
  // src/gallery/generate.ts
16622
- var GenerateRequestSchema = z9.object({
16623
- project: z9.string().min(1).optional(),
16624
- keys: z9.array(z9.string().min(1)).min(1).max(500),
16850
+ var GenerateRequestSchema = z10.object({
16851
+ project: z10.string().min(1).optional(),
16852
+ keys: z10.array(z10.string().min(1)).min(1).max(500),
16625
16853
  /**
16626
16854
  * Regenerate work that is up to date, as `gen --force` does, and let the
16627
16855
  * fetch replace a locally modified or untracked file. With `restore`, put
16628
16856
  * the recorded bytes back over a modified file.
16629
16857
  */
16630
- force: z9.boolean().optional(),
16858
+ force: z10.boolean().optional(),
16631
16859
  /** Advance in-flight, review, or selected work without submitting anything. */
16632
- resume: z9.boolean().optional(),
16860
+ resume: z10.boolean().optional(),
16633
16861
  /** Re-download downloaded outputs and replace files whose object changed upstream. */
16634
- refresh: z9.boolean().optional(),
16862
+ refresh: z10.boolean().optional(),
16635
16863
  /** Put recorded outputs back on disk from the cache or the provider, without generating. */
16636
- restore: z9.boolean().optional(),
16864
+ restore: z10.boolean().optional(),
16637
16865
  /**
16638
16866
  * Bring a replaced generation back: 1-based position in the record's
16639
16867
  * history (newest first) or an output hash prefix. One key at a time.
16640
16868
  */
16641
- revert: z9.string().min(1).max(64).optional()
16869
+ revert: z10.string().min(1).max(64).optional()
16642
16870
  }).strict().refine((request) => [request.resume, request.refresh, request.restore, request.revert !== void 0].filter(Boolean).length <= 1, {
16643
16871
  message: "resume, refresh, restore, and revert are exclusive"
16644
16872
  }).refine((request) => request.revert === void 0 || request.keys.length === 1, { message: "revert takes one key" });
@@ -16948,15 +17176,15 @@ function createGenerateHandlers(opts) {
16948
17176
  }
16949
17177
 
16950
17178
  // src/gallery/editor.ts
16951
- import { readFile as readFile22 } from "fs/promises";
16952
- import path31 from "path";
17179
+ import { readFile as readFile23 } from "fs/promises";
17180
+ import path32 from "path";
16953
17181
 
16954
17182
  // src/editor/install.ts
16955
17183
  import { createHash as createHash4 } from "crypto";
16956
- import { existsSync as existsSync22 } from "fs";
16957
- import { mkdir as mkdir10, readFile as readFile21, rename as rename9, rm as rm9, stat as stat3, writeFile as writeFile13 } from "fs/promises";
17184
+ import { existsSync as existsSync23 } from "fs";
17185
+ import { mkdir as mkdir11, readFile as readFile22, rename as rename9, rm as rm10, stat as stat3, writeFile as writeFile14 } from "fs/promises";
16958
17186
  import os2 from "os";
16959
- import path30 from "path";
17187
+ import path31 from "path";
16960
17188
 
16961
17189
  // tools/pixelorama-bridge/pin.json
16962
17190
  var pin_default = {
@@ -17020,15 +17248,15 @@ var EditorInstallError = class extends Error {
17020
17248
  };
17021
17249
  function editorToolsRoot() {
17022
17250
  const override = process.env.PIXELKILN_TOOLS_DIR?.trim();
17023
- if (override) return path30.resolve(override);
17024
- if (process.platform === "darwin") return path30.join(os2.homedir(), "Library", "Caches", "pixelkiln", "tools");
17251
+ if (override) return path31.resolve(override);
17252
+ if (process.platform === "darwin") return path31.join(os2.homedir(), "Library", "Caches", "pixelkiln", "tools");
17025
17253
  if (process.platform === "win32") {
17026
- return path30.join(process.env.LOCALAPPDATA ?? path30.join(os2.homedir(), "AppData", "Local"), "pixelkiln", "tools");
17254
+ return path31.join(process.env.LOCALAPPDATA ?? path31.join(os2.homedir(), "AppData", "Local"), "pixelkiln", "tools");
17027
17255
  }
17028
- return path30.join(process.env.XDG_CACHE_HOME ?? path30.join(os2.homedir(), ".cache"), "pixelkiln", "tools");
17256
+ return path31.join(process.env.XDG_CACHE_HOME ?? path31.join(os2.homedir(), ".cache"), "pixelkiln", "tools");
17029
17257
  }
17030
17258
  function editorDir(pin = EDITOR_PIN, root = editorToolsRoot()) {
17031
- return path30.join(root, "pixelorama", pin.release ?? "unpublished");
17259
+ return path31.join(root, "pixelorama", pin.release ?? "unpublished");
17032
17260
  }
17033
17261
  function editorBaseUrl(pin = EDITOR_PIN) {
17034
17262
  const override = process.env.PIXELKILN_EDITOR_URL?.trim();
@@ -17038,7 +17266,7 @@ function editorBaseUrl(pin = EDITOR_PIN) {
17038
17266
  }
17039
17267
  async function sha256Of(file) {
17040
17268
  try {
17041
- return createHash4("sha256").update(await readFile21(file)).digest("hex");
17269
+ return createHash4("sha256").update(await readFile22(file)).digest("hex");
17042
17270
  } catch {
17043
17271
  return null;
17044
17272
  }
@@ -17051,8 +17279,8 @@ async function editorStatus(opts = {}) {
17051
17279
  let totalBytes = 0;
17052
17280
  for (const [name, expected] of Object.entries(pin.files)) {
17053
17281
  totalBytes += expected.bytes;
17054
- const file = path30.join(dir, name);
17055
- const info = existsSync22(file) ? await stat3(file).catch(() => null) : null;
17282
+ const file = path31.join(dir, name);
17283
+ const info = existsSync23(file) ? await stat3(file).catch(() => null) : null;
17056
17284
  if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
17057
17285
  installedBytes += expected.bytes;
17058
17286
  } else {
@@ -17081,7 +17309,7 @@ async function installEditor(opts = {}) {
17081
17309
  );
17082
17310
  }
17083
17311
  if (!baseUrl) throw new EditorInstallError("no download location for the editor build");
17084
- await mkdir10(dir, { recursive: true });
17312
+ await mkdir11(dir, { recursive: true });
17085
17313
  const downloaded = [];
17086
17314
  const skipped = Object.keys(pin.files).filter((name) => !before.missing.includes(name));
17087
17315
  let fetchedBytes = before.installedBytes;
@@ -17109,13 +17337,13 @@ async function installEditor(opts = {}) {
17109
17337
  if (digest2 !== expected.sha256) {
17110
17338
  throw new EditorInstallError(`${name}: sha256 ${digest2.slice(0, 12)}\u2026 does not match the pinned ${expected.sha256.slice(0, 12)}\u2026`, name);
17111
17339
  }
17112
- const final = path30.join(dir, name);
17340
+ const final = path31.join(dir, name);
17113
17341
  const tmp = `${final}.${process.pid}.part`;
17114
17342
  try {
17115
- await writeFile13(tmp, bytes);
17343
+ await writeFile14(tmp, bytes);
17116
17344
  await rename9(tmp, final);
17117
17345
  } finally {
17118
- await rm9(tmp, { force: true });
17346
+ await rm10(tmp, { force: true });
17119
17347
  }
17120
17348
  fetchedBytes += bytes.length;
17121
17349
  downloaded.push(name);
@@ -17155,7 +17383,7 @@ var EDITOR_CONTENT_TYPES = {
17155
17383
  ".json": "application/json; charset=utf-8"
17156
17384
  };
17157
17385
  function editorContentType(name) {
17158
- return EDITOR_CONTENT_TYPES[path30.extname(name).toLowerCase()] ?? null;
17386
+ return EDITOR_CONTENT_TYPES[path31.extname(name).toLowerCase()] ?? null;
17159
17387
  }
17160
17388
 
17161
17389
  // src/gallery/editor.ts
@@ -17231,7 +17459,7 @@ function createGalleryEditorHandlers(opts = {}) {
17231
17459
  if (!status.installed) return null;
17232
17460
  let bytes;
17233
17461
  try {
17234
- bytes = await readFile22(path31.join(dir, name));
17462
+ bytes = await readFile23(path32.join(dir, name));
17235
17463
  } catch {
17236
17464
  verified = null;
17237
17465
  return null;
@@ -17271,13 +17499,13 @@ async function serveUntilStopped(initial, reload, args, access2) {
17271
17499
  };
17272
17500
  const loadProject = async (project) => {
17273
17501
  const ctx = await access2.loadProject(project);
17274
- const dir = path32.dirname(ctx.loaded.path);
17502
+ const dir = path33.dirname(ctx.loaded.path);
17275
17503
  const declared = readEnvFiles(dir);
17276
17504
  const credentialNames = new Set(availableProviders().flatMap((id) => providerCredentialEnvs(providerFactory(id))));
17277
17505
  for (const [name, value] of Object.entries(declared)) {
17278
17506
  if (credentialNames.has(name) && name in process.env && process.env[name] !== value) {
17279
17507
  throw new Error(
17280
- `${path32.relative(process.cwd(), dir) || "."} sets ${name} to a different value than the one this gallery already loaded; run a separate gallery for that project so its work uses its own account.`
17508
+ `${path33.relative(process.cwd(), dir) || "."} sets ${name} to a different value than the one this gallery already loaded; run a separate gallery for that project so its work uses its own account.`
17281
17509
  );
17282
17510
  }
17283
17511
  }
@@ -17305,6 +17533,8 @@ async function serveUntilStopped(initial, reload, args, access2) {
17305
17533
  ),
17306
17534
  ...args.edit ? { edit: createGalleryEditHandler({ manifestFor: access2.manifestFor, loadProject: access2.loadProject, reload, onProgress: log }) } : {},
17307
17535
  ...budget ? { generate: createGenerateHandlers({ loadProject, providerFor, budget, reload, onProgress: log }) } : {},
17536
+ // Estimating acts on the author's behalf and feeds a manifest edit, so it follows the write gate.
17537
+ ...args.edit ? { skeleton: createGallerySkeletonHandlers({ loadProject, onProgress: log }) } : {},
17308
17538
  // The editor exists to write hand edits back, so it follows the write gate.
17309
17539
  ...args.edit && !args.noEditor ? { editor: createGalleryEditorHandlers({ onProgress: log }) } : {}
17310
17540
  });
@@ -17322,8 +17552,8 @@ async function serveUntilStopped(initial, reload, args, access2) {
17322
17552
  }
17323
17553
  async function runGallery(args) {
17324
17554
  if (args.workspace) {
17325
- const workspacePath = path32.resolve(args.workspace);
17326
- if (!existsSync23(workspacePath)) {
17555
+ const workspacePath = path33.resolve(args.workspace);
17556
+ if (!existsSync24(workspacePath)) {
17327
17557
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
17328
17558
  }
17329
17559
  const filter2 = { styles: args.styles, assets: args.assets };
@@ -17343,7 +17573,7 @@ async function runGallery(args) {
17343
17573
  const registered = async (projectId) => {
17344
17574
  const project = (await loadWorkspace(workspacePath)).projects.find((candidate) => candidate.id === projectId);
17345
17575
  if (!projectId || !project) throw new Error(`unknown workspace project "${projectId ?? ""}"`);
17346
- return resolveProject(path32.dirname(workspacePath), project);
17576
+ return resolveProject(path33.dirname(workspacePath), project);
17347
17577
  };
17348
17578
  await serveUntilStopped(initial, build2, args, {
17349
17579
  manifestFor: async (projectId) => (await registered(projectId)).manifestPath,
@@ -17368,7 +17598,7 @@ async function runGallery(args) {
17368
17598
  return buildGallerySnapshot({ ...fresh, filter });
17369
17599
  };
17370
17600
  await serveUntilStopped(await build(), reload, args, {
17371
- manifestFor: () => path32.resolve(args.manifest),
17601
+ manifestFor: () => path33.resolve(args.manifest),
17372
17602
  loadProject: () => openProject(args.manifest, { lockPath: args.lock, env: false })
17373
17603
  });
17374
17604
  }
@@ -17677,18 +17907,18 @@ async function runRestoreGeneration(args) {
17677
17907
  }
17678
17908
 
17679
17909
  // src/cli/commands/housekeeping.ts
17680
- import { existsSync as existsSync25 } from "fs";
17910
+ import { existsSync as existsSync26 } from "fs";
17681
17911
 
17682
17912
  // src/pipeline/cache-health.ts
17683
- import { existsSync as existsSync24 } from "fs";
17684
- import { readFile as readFile23, readdir as readdir2, rm as rm10 } from "fs/promises";
17685
- import path33 from "path";
17913
+ import { existsSync as existsSync25 } from "fs";
17914
+ import { readFile as readFile24, readdir as readdir2, rm as rm11 } from "fs/promises";
17915
+ import path34 from "path";
17686
17916
  async function inspectCaches(lock, lockPath, options = {}) {
17687
- if (options.prune && !existsSync24(lockPath)) {
17688
- throw new Error(`Refusing to prune without an existing lockfile at ${path33.resolve(lockPath)}`);
17917
+ if (options.prune && !existsSync25(lockPath)) {
17918
+ throw new Error(`Refusing to prune without an existing lockfile at ${path34.resolve(lockPath)}`);
17689
17919
  }
17690
- const contentDir = path33.resolve(path33.dirname(lockPath), ".pixelkiln", "cache");
17691
- const remotePath = path33.resolve(cachePathFor(lockPath));
17920
+ const contentDir = path34.resolve(path34.dirname(lockPath), ".pixelkiln", "cache");
17921
+ const remotePath = path34.resolve(cachePathFor(lockPath));
17692
17922
  const referenced = referencedHashes(lock);
17693
17923
  let content = await inspectContentCache(contentDir, referenced);
17694
17924
  let remoteHashes = await inspectRemoteHashCache(remotePath);
@@ -17700,7 +17930,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
17700
17930
  ]);
17701
17931
  for (const name of names) {
17702
17932
  try {
17703
- await rm10(path33.join(contentDir, name), { force: true });
17933
+ await rm11(path34.join(contentDir, name), { force: true });
17704
17934
  removed.contentFiles++;
17705
17935
  } catch {
17706
17936
  }
@@ -17709,7 +17939,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
17709
17939
  await saveCache(remotePath, { version: 1, hashes: {} });
17710
17940
  removed.resetRemoteHashCache = true;
17711
17941
  } else if (remoteHashes.invalidIds.length) {
17712
- const cache = parseCache(JSON.parse(await readFile23(remotePath, "utf8")));
17942
+ const cache = parseCache(JSON.parse(await readFile24(remotePath, "utf8")));
17713
17943
  for (const id of remoteHashes.invalidIds) delete cache.hashes[id];
17714
17944
  removed.remoteHashEntries = remoteHashes.invalidIds.length;
17715
17945
  await saveCache(remotePath, cache);
@@ -17727,7 +17957,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
17727
17957
  async function inspectContentCache(contentDir, referenced) {
17728
17958
  const report = {
17729
17959
  path: contentDir,
17730
- exists: existsSync24(contentDir),
17960
+ exists: existsSync25(contentDir),
17731
17961
  files: 0,
17732
17962
  bytes: 0,
17733
17963
  valid: 0,
@@ -17748,12 +17978,12 @@ async function inspectContentCache(contentDir, referenced) {
17748
17978
  continue;
17749
17979
  }
17750
17980
  report.files++;
17751
- const file = path33.join(contentDir, entry.name);
17981
+ const file = path34.join(contentDir, entry.name);
17752
17982
  const mediaType = mediaTypeFromExtension(entry.name);
17753
17983
  const expected = mediaType ? entry.name.slice(0, -4) : "";
17754
17984
  let bytes;
17755
17985
  try {
17756
- bytes = await readFile23(file);
17986
+ bytes = await readFile24(file);
17757
17987
  report.bytes += bytes.length;
17758
17988
  } catch (err) {
17759
17989
  report.invalid.push({
@@ -17791,7 +18021,7 @@ async function inspectContentCache(contentDir, referenced) {
17791
18021
  async function inspectRemoteHashCache(remotePath) {
17792
18022
  const report = {
17793
18023
  path: remotePath,
17794
- exists: existsSync24(remotePath),
18024
+ exists: existsSync25(remotePath),
17795
18025
  entries: 0,
17796
18026
  valid: 0,
17797
18027
  invalidIds: [],
@@ -17800,7 +18030,7 @@ async function inspectRemoteHashCache(remotePath) {
17800
18030
  if (!report.exists) return report;
17801
18031
  let cache;
17802
18032
  try {
17803
- cache = parseCache(JSON.parse(await readFile23(remotePath, "utf8")));
18033
+ cache = parseCache(JSON.parse(await readFile24(remotePath, "utf8")));
17804
18034
  } catch (err) {
17805
18035
  report.error = err instanceof Error ? err.message : String(err);
17806
18036
  return report;
@@ -17864,7 +18094,7 @@ async function runAccept(args) {
17864
18094
  let intact = true;
17865
18095
  for (const output of entry.outputs) {
17866
18096
  const file = resolveOutputPath(output.path, item.spec.root);
17867
- if (!existsSync25(file) || await sha256File(file) !== output.sha256) {
18097
+ if (!existsSync26(file) || await sha256File(file) !== output.sha256) {
17868
18098
  intact = false;
17869
18099
  break;
17870
18100
  }
@@ -17919,42 +18149,42 @@ This drops their provenance from the lockfile. The art on disk is`);
17919
18149
  }
17920
18150
 
17921
18151
  // src/cli/commands/init.ts
17922
- import path34 from "path";
17923
- import { existsSync as existsSync26 } from "fs";
18152
+ import path35 from "path";
18153
+ import { existsSync as existsSync27 } from "fs";
17924
18154
  async function runInit(args) {
17925
18155
  if (!args.from) throw new Error("init needs --from <dir> pointing at your existing PNGs.");
17926
- const root = path34.resolve(args.from);
17927
- if (!existsSync26(root)) throw new Error(`No directory at ${root}`);
18156
+ const root = path35.resolve(args.from);
18157
+ if (!existsSync27(root)) throw new Error(`No directory at ${root}`);
17928
18158
  const generator = args.generator ?? "map";
17929
18159
  if (generator !== "1dir" && generator !== "map") {
17930
18160
  throw new Error(`--generator must be "1dir" or "map", got "${args.generator}".`);
17931
18161
  }
17932
- const target = path34.resolve(args.out ?? "pixelkiln.manifest.json");
18162
+ const target = path35.resolve(args.out ?? "pixelkiln.manifest.json");
17933
18163
  const { assets, skipped } = await scanAssets(root, { exclude: args.exclude });
17934
18164
  if (!assets.length) throw new Error(`No PNGs found under ${root}`);
17935
18165
  const manifest = buildManifest(
17936
- args.name ?? path34.basename(path34.dirname(target)),
18166
+ args.name ?? path35.basename(path35.dirname(target)),
17937
18167
  args.styles[0] ?? "base",
17938
18168
  generator,
17939
- path34.relative(path34.dirname(target), root) || ".",
18169
+ path35.relative(path35.dirname(target), root) || ".",
17940
18170
  assets
17941
18171
  );
17942
18172
  await writeManifestFile(target, manifest);
17943
- log(` scanned ${assets.length} PNG(s) under ${path34.relative(process.cwd(), root)}`);
18173
+ log(` scanned ${assets.length} PNG(s) under ${path35.relative(process.cwd(), root)}`);
17944
18174
  if (skipped.length) log(` skipped ${skipped.length} unreadable file(s)`);
17945
- log(` wrote ${path34.relative(process.cwd(), target)}`);
18175
+ log(` wrote ${path35.relative(process.cwd(), target)}`);
17946
18176
  log(`
17947
18177
  Prompts are intentionally empty. To recover the real ones from your`);
17948
18178
  log(` PixelLab account instead of inventing them:`);
17949
18179
  log(`
17950
- pixelkiln adopt --manifest ${path34.relative(process.cwd(), target)} --write-prompts
18180
+ pixelkiln adopt --manifest ${path35.relative(process.cwd(), target)} --write-prompts
17951
18181
  `);
17952
18182
  }
17953
18183
 
17954
18184
  // src/pipeline/doctor.ts
17955
- import { constants, existsSync as existsSync27 } from "fs";
18185
+ import { constants, existsSync as existsSync28 } from "fs";
17956
18186
  import { access, stat as stat4 } from "fs/promises";
17957
- import path35 from "path";
18187
+ import path36 from "path";
17958
18188
  async function doctor(loaded, specs, lock, lockPath, opts = {}) {
17959
18189
  const checks = [];
17960
18190
  const add = (id, level, message6) => checks.push({ id, level, message: message6 });
@@ -17965,8 +18195,8 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
17965
18195
  );
17966
18196
  add("lock", "ok", `${Object.keys(lock.entries).length} valid v2 lock entr(ies)`);
17967
18197
  const dirs = [...new Set(specs.flatMap((spec) => [
17968
- path35.dirname(spec.outFile),
17969
- ...spec.quality ? [path35.dirname(spec.quality.outFile)] : []
18198
+ path36.dirname(spec.outFile),
18199
+ ...spec.quality ? [path36.dirname(spec.quality.outFile)] : []
17970
18200
  ]))];
17971
18201
  const unwritable = [];
17972
18202
  for (const dir of dirs) {
@@ -17989,7 +18219,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
17989
18219
  for (let index = 0; index < entry.outputs.length; index++) {
17990
18220
  const output = entry.outputs[index];
17991
18221
  const spec = specByKey.get(key);
17992
- const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, path35.dirname(lockPath));
18222
+ const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, path36.dirname(lockPath));
17993
18223
  const owner = outputOwners.get(absolute);
17994
18224
  if (owner && owner !== key) duplicateOutputs.push(`${output.path} (${owner}, ${key})`);
17995
18225
  outputOwners.set(absolute, key);
@@ -18004,7 +18234,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
18004
18234
  } else {
18005
18235
  add("lock-outputs", "ok", `${outputOwners.size} recorded output path(s) are uniquely owned`);
18006
18236
  }
18007
- const cacheDir = path35.join(path35.dirname(lockPath), ".pixelkiln", "cache");
18237
+ const cacheDir = path36.join(path36.dirname(lockPath), ".pixelkiln", "cache");
18008
18238
  const stranded = [];
18009
18239
  const invalidState = [];
18010
18240
  const staleMapUrls = [];
@@ -18020,11 +18250,11 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
18020
18250
  const hasSource = Boolean(entry.sourceUrl || entry.sourceUrls?.length);
18021
18251
  let hasCache = false;
18022
18252
  for (const output of entry.outputs) {
18023
- const cached = path35.join(
18253
+ const cached = path36.join(
18024
18254
  cacheDir,
18025
18255
  cacheFileName(output.sha256, output.mediaType ?? MediaType.PNG)
18026
18256
  );
18027
- if (existsSync27(cached) && await sha256File(cached) === output.sha256) {
18257
+ if (existsSync28(cached) && await sha256File(cached) === output.sha256) {
18028
18258
  hasCache = true;
18029
18259
  break;
18030
18260
  }
@@ -18120,16 +18350,16 @@ async function nearestExistingDirectory(start) {
18120
18350
  if ((await stat4(current)).isDirectory()) return current;
18121
18351
  } catch {
18122
18352
  }
18123
- const parent = path35.dirname(current);
18353
+ const parent = path36.dirname(current);
18124
18354
  if (parent === current) return current;
18125
18355
  current = parent;
18126
18356
  }
18127
18357
  }
18128
18358
 
18129
18359
  // src/pipeline/audit.ts
18130
- import { readFile as readFile24 } from "fs/promises";
18131
- import { existsSync as existsSync28 } from "fs";
18132
- import path36 from "path";
18360
+ import { readFile as readFile25 } from "fs/promises";
18361
+ import { existsSync as existsSync29 } from "fs";
18362
+ import path37 from "path";
18133
18363
  function paletteDistance(asset, reference) {
18134
18364
  if (!asset.length || !reference.length) return 0;
18135
18365
  let total = 0;
@@ -18165,12 +18395,12 @@ async function auditStyle(loaded, specs, styleId, lock) {
18165
18395
  const unreadable = [];
18166
18396
  for (const spec of mine) {
18167
18397
  for (const output of resolveSpecOutputs(spec, lock, loaded.root)) {
18168
- if (!existsSync28(output.absolutePath)) {
18398
+ if (!existsSync29(output.absolutePath)) {
18169
18399
  missing.push(output.id);
18170
18400
  continue;
18171
18401
  }
18172
18402
  try {
18173
- const png = decodePng(await readFile24(output.absolutePath));
18403
+ const png = decodePng(await readFile25(output.absolutePath));
18174
18404
  const palette = extractPalette(png, 12);
18175
18405
  assets.push({
18176
18406
  assetId: spec.assetId,
@@ -18194,10 +18424,10 @@ async function auditStyle(loaded, specs, styleId, lock) {
18194
18424
  let referenceFromStyleImages = false;
18195
18425
  const refPalettes = [];
18196
18426
  for (const img of style.styleImages) {
18197
- const abs = path36.resolve(loaded.root, img.path);
18198
- if (!existsSync28(abs)) continue;
18427
+ const abs = path37.resolve(loaded.root, img.path);
18428
+ if (!existsSync29(abs)) continue;
18199
18429
  try {
18200
- refPalettes.push(extractPalette(decodePng(await readFile24(abs)), 12));
18430
+ refPalettes.push(extractPalette(decodePng(await readFile25(abs)), 12));
18201
18431
  } catch {
18202
18432
  }
18203
18433
  }
@@ -18425,24 +18655,24 @@ async function runAudit(args) {
18425
18655
  }
18426
18656
 
18427
18657
  // src/cli/commands/pack.ts
18428
- import path38 from "path";
18429
- import { readFile as readFile25 } from "fs/promises";
18658
+ import path39 from "path";
18659
+ import { readFile as readFile26 } from "fs/promises";
18430
18660
 
18431
18661
  // src/pipeline/pack.ts
18432
18662
  import { readFileSync as readFileSync5 } from "fs";
18433
- import path37 from "path";
18663
+ import path38 from "path";
18434
18664
  var DEFAULT_FRAME_SET_FPS = 12;
18435
18665
  function resolvePackInputs(raw, inputsFilePath) {
18436
18666
  if (!Array.isArray(raw) || !raw.length) {
18437
18667
  throw new Error("--inputs must be a non-empty JSON array of { id, path }");
18438
18668
  }
18439
- const baseDir = path37.dirname(path37.resolve(inputsFilePath));
18669
+ const baseDir = path38.dirname(path38.resolve(inputsFilePath));
18440
18670
  return raw.map((entry, i) => {
18441
18671
  const e = entry;
18442
18672
  if (typeof e.id !== "string" || typeof e.path !== "string") {
18443
18673
  throw new Error(`--inputs[${i}] needs string "id" and "path"`);
18444
18674
  }
18445
- return { id: e.id, path: path37.resolve(baseDir, e.path) };
18675
+ return { id: e.id, path: path38.resolve(baseDir, e.path) };
18446
18676
  });
18447
18677
  }
18448
18678
  function packSprites(inputs, options = {}) {
@@ -18526,7 +18756,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
18526
18756
  }
18527
18757
  const inputs2 = Object.entries(options.sourceOverrides).map(([id, source]) => ({
18528
18758
  id,
18529
- path: path37.resolve(source)
18759
+ path: path38.resolve(source)
18530
18760
  }));
18531
18761
  if (!inputs2.length) throw new Error(`No approved quality outputs for style "${styleId}".`);
18532
18762
  const packed2 = packSprites(inputs2, options);
@@ -18550,7 +18780,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
18550
18780
  let outputs = resolveEntryOutputs(entry, id, manifestDir);
18551
18781
  const source = sources[id];
18552
18782
  if (source && outputs.length <= 1) {
18553
- inputs.push({ id, path: path37.resolve(manifestDir, source) });
18783
+ inputs.push({ id, path: path38.resolve(manifestDir, source) });
18554
18784
  continue;
18555
18785
  }
18556
18786
  if (source && sourceIsStem(source, entry, manifestDir)) {
@@ -18584,7 +18814,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
18584
18814
  }
18585
18815
  }
18586
18816
  for (const [id, source] of Object.entries(sources)) {
18587
- if (!locked.has(id)) inputs.push({ id, path: path37.resolve(manifestDir, source) });
18817
+ if (!locked.has(id)) inputs.push({ id, path: path38.resolve(manifestDir, source) });
18588
18818
  }
18589
18819
  if (!inputs.length) {
18590
18820
  throw new Error(`No readable sprites for style "${styleId}"; ${noOutput.length} skipped.`);
@@ -18692,7 +18922,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
18692
18922
  const source = sources[id];
18693
18923
  const entry = lock.entries[`${styleId}/${id}`];
18694
18924
  if (source && !(entry && sourceIsStem(source, entry, manifestDir))) {
18695
- placements.push({ id, path: path37.resolve(manifestDir, source), cell });
18925
+ placements.push({ id, path: path38.resolve(manifestDir, source), cell });
18696
18926
  continue;
18697
18927
  }
18698
18928
  if (source && entry) {
@@ -18725,7 +18955,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
18725
18955
  `Nothing to mount for style "${styleId}": ${ids.length} asset(s) declare a cell, but none has generated output or a \`source\`. Run \`pixelkiln gen --style ${styleId}\`, or point each at a file.${detail}`
18726
18956
  );
18727
18957
  }
18728
- const basePng = mount.base ? readFileSync5(path37.resolve(manifestDir, mount.base)) : void 0;
18958
+ const basePng = mount.base ? readFileSync5(path38.resolve(manifestDir, mount.base)) : void 0;
18729
18959
  const mounted = mountSprites(placements, {
18730
18960
  cellWidth: mount.cellWidth,
18731
18961
  cellHeight: mount.cellHeight,
@@ -18735,7 +18965,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
18735
18965
  ...mounted,
18736
18966
  sources: mount.base ? [{
18737
18967
  id: "$base",
18738
- path: path37.resolve(manifestDir, mount.base),
18968
+ path: path38.resolve(manifestDir, mount.base),
18739
18969
  sha256: sha256(basePng),
18740
18970
  included: true
18741
18971
  }, ...mounted.sources] : mounted.sources,
@@ -19139,12 +19369,12 @@ async function runPack(args) {
19139
19369
  if (args.primaryOnly || args.outputRoles.length) {
19140
19370
  throw new Error("--primary-only and --output-role require manifest-driven pack");
19141
19371
  }
19142
- const raw = JSON.parse(await readFile25(path38.resolve(args.inputs), "utf8"));
19372
+ const raw = JSON.parse(await readFile26(path39.resolve(args.inputs), "utf8"));
19143
19373
  const inputs = resolvePackInputs(raw, args.inputs);
19144
19374
  const { png, atlas, skipped, sources } = packSprites(inputs, { columns: args.columns });
19145
19375
  const format = sheetFormat(args);
19146
- const base = path38.resolve(args.out.replace(/\.(?:png|json|tres)$/i, ""));
19147
- const { extension, document } = renderSheetDocument(format, atlas, { imageName: path38.basename(`${base}.png`) });
19376
+ const base = path39.resolve(args.out.replace(/\.(?:png|json|tres)$/i, ""));
19377
+ const { extension, document } = renderSheetDocument(format, atlas, { imageName: path39.basename(`${base}.png`) });
19148
19378
  const outputs = [
19149
19379
  { path: `${base}.png`, data: png },
19150
19380
  { path: `${base}${extension}`, data: document }
@@ -19157,7 +19387,7 @@ async function runPack(args) {
19157
19387
  log(
19158
19388
  ` ${atlas.frames.length} sprite(s), ${atlas.sheet.width}x${atlas.sheet.height} in ${atlas.columns} column(s), ${(png.length / 1024).toFixed(1)} KB (${format})`
19159
19389
  );
19160
- log(` ${path38.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
19390
+ log(` ${path39.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
19161
19391
  for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
19162
19392
  return;
19163
19393
  }
@@ -19165,7 +19395,7 @@ async function runPack(args) {
19165
19395
  if (args.primaryOnly && args.outputRoles.length) {
19166
19396
  throw new Error("pack accepts either --primary-only or --output-role, not both");
19167
19397
  }
19168
- const manifestDir = path38.dirname(path38.resolve(args.manifest));
19398
+ const manifestDir = path39.dirname(path39.resolve(args.manifest));
19169
19399
  const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
19170
19400
  for (const styleId of styleIds) {
19171
19401
  const style = loaded.manifest.styles[styleId];
@@ -19180,9 +19410,9 @@ async function runPack(args) {
19180
19410
  sourceOverrides: qualitySources,
19181
19411
  sources: manifestSources(loaded.manifest, styleId)
19182
19412
  });
19183
- const base = args.out ? path38.resolve(args.out.replace(/\.(?:png|json|tres)$/i, "")) : path38.resolve(manifestDir, style.outDir, `${styleId}-sheet`);
19413
+ const base = args.out ? path39.resolve(args.out.replace(/\.(?:png|json|tres)$/i, "")) : path39.resolve(manifestDir, style.outDir, `${styleId}-sheet`);
19184
19414
  const format = sheetFormat(args);
19185
- const { extension, document } = renderSheetDocument(format, atlas, { imageName: path38.basename(`${base}.png`) });
19415
+ const { extension, document } = renderSheetDocument(format, atlas, { imageName: path39.basename(`${base}.png`) });
19186
19416
  const outputs = [
19187
19417
  { path: `${base}.png`, data: png },
19188
19418
  { path: `${base}${extension}`, data: document }
@@ -19214,13 +19444,13 @@ async function runPack(args) {
19214
19444
  log(
19215
19445
  ` ${styleId}: ${atlas.frames.length} sprite(s), ${atlas.sheet.width}x${atlas.sheet.height} in ${atlas.columns} column(s)` + (atlas.sets?.length ? `, ${atlas.sets.length} set(s)` : "") + ` (${format})`
19216
19446
  );
19217
- log(` ${path38.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
19447
+ log(` ${path39.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
19218
19448
  for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
19219
19449
  }
19220
19450
  }
19221
19451
  async function runMount(args) {
19222
19452
  const { loaded, lock } = await openProject2(args);
19223
- const manifestDir = path38.dirname(path38.resolve(args.manifest));
19453
+ const manifestDir = path39.dirname(path39.resolve(args.manifest));
19224
19454
  const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
19225
19455
  for (const styleId of styleIds) {
19226
19456
  const style = loaded.manifest.styles[styleId];
@@ -19259,9 +19489,9 @@ async function runMount(args) {
19259
19489
  sources,
19260
19490
  outputRoles
19261
19491
  );
19262
- const out = path38.resolve(manifestDir, style.mount.out);
19492
+ const out = path39.resolve(manifestDir, style.mount.out);
19263
19493
  const format = sheetFormat(args);
19264
- const { extension, document } = renderSheetDocument(format, atlas, { imageName: path38.basename(out) });
19494
+ const { extension, document } = renderSheetDocument(format, atlas, { imageName: path39.basename(out) });
19265
19495
  const metadata = out.replace(/\.png$/, "") + extension;
19266
19496
  const companion = out.replace(/\.png$/, "") + ".pixelkiln.json";
19267
19497
  const outputs = [
@@ -19281,7 +19511,7 @@ async function runMount(args) {
19281
19511
  await provenanceFile("$lock", args.lock),
19282
19512
  ...qualityRecords,
19283
19513
  ...artifactSources.filter(
19284
- (source) => source.id !== "$base" || path38.resolve(source.path) !== out
19514
+ (source) => source.id !== "$base" || path39.resolve(source.path) !== out
19285
19515
  )
19286
19516
  ],
19287
19517
  options: {
@@ -19295,14 +19525,14 @@ async function runMount(args) {
19295
19525
  log(
19296
19526
  ` ${styleId}: ${atlas.frames.length} cell(s) into ${atlas.sheet.width}x${atlas.sheet.height}` + (overBase ? ` over ${style.mount.base}` : " (new sheet)")
19297
19527
  );
19298
- log(` ${path38.relative(process.cwd(), out)} + atlas/provenance JSON`);
19528
+ log(` ${path39.relative(process.cwd(), out)} + atlas/provenance JSON`);
19299
19529
  for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
19300
19530
  }
19301
19531
  }
19302
19532
  async function runExport(args) {
19303
19533
  const { loaded, specs, lock } = await openProject2(args);
19304
19534
  const format = args.format ?? "generic";
19305
- const manifestDir = path38.dirname(path38.resolve(args.manifest));
19535
+ const manifestDir = path39.dirname(path39.resolve(args.manifest));
19306
19536
  const selected = specs.filter((spec) => {
19307
19537
  if (spec.generator !== "tiles" && spec.generator !== "terrain") return false;
19308
19538
  if (args.styles.length && !args.styles.includes(spec.styleId)) return false;
@@ -19318,12 +19548,12 @@ async function runExport(args) {
19318
19548
  for (const spec of selected) {
19319
19549
  const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
19320
19550
  const style = loaded.manifest.styles[spec.styleId];
19321
- const defaultBase = path38.resolve(manifestDir, style.outDir, `${spec.assetId}-tileset`);
19322
- const base = args.out ? path38.resolve(args.out.replace(/\.(?:png|json|tsj|tres)$/i, "")) : defaultBase;
19551
+ const defaultBase = path39.resolve(manifestDir, style.outDir, `${spec.assetId}-tileset`);
19552
+ const base = args.out ? path39.resolve(args.out.replace(/\.(?:png|json|tsj|tres)$/i, "")) : defaultBase;
19323
19553
  const result = exportTileset(entry, spec, {
19324
19554
  format,
19325
19555
  manifestDir,
19326
- imageName: path38.basename(`${base}.png`),
19556
+ imageName: path39.basename(`${base}.png`),
19327
19557
  columns: args.columns
19328
19558
  });
19329
19559
  const outputs = [
@@ -19341,7 +19571,7 @@ async function runExport(args) {
19341
19571
  asset: spec.assetId,
19342
19572
  columns: args.columns ?? null,
19343
19573
  format,
19344
- image: path38.basename(`${base}.png`),
19574
+ image: path39.basename(`${base}.png`),
19345
19575
  providerRules: result.generic.providerRules,
19346
19576
  style: spec.styleId,
19347
19577
  tileType: spec.tileType ?? null
@@ -19351,7 +19581,7 @@ async function runExport(args) {
19351
19581
  ` ${spec.styleId}/${spec.assetId}: ${result.generic.tiles.length} tile(s), ${result.generic.sheet.width}x${result.generic.sheet.height} (${format})`
19352
19582
  );
19353
19583
  log(
19354
- ` ${path38.relative(process.cwd(), base)}.png + ${path38.basename(base)}${result.extension} + .pixelkiln.json`
19584
+ ` ${path39.relative(process.cwd(), base)}.png + ${path39.basename(base)}${result.extension} + .pixelkiln.json`
19355
19585
  );
19356
19586
  }
19357
19587
  }
@@ -19364,23 +19594,23 @@ function sheetFormat(args) {
19364
19594
  }
19365
19595
 
19366
19596
  // src/cli/commands/pixellab-utils.ts
19367
- import path40 from "path";
19597
+ import path41 from "path";
19368
19598
 
19369
19599
  // src/pixellab-utils.ts
19370
- import { existsSync as existsSync29 } from "fs";
19371
- import { mkdir as mkdir11, readFile as readFile26, writeFile as writeFile14 } from "fs/promises";
19372
- import path39 from "path";
19600
+ import { existsSync as existsSync30 } from "fs";
19601
+ import { mkdir as mkdir12, readFile as readFile27, writeFile as writeFile15 } from "fs/promises";
19602
+ import path40 from "path";
19373
19603
  var UNZOOM_MIN_SIDE = 256;
19374
19604
  var UNZOOM_MAX_AREA = 2048 * 2048;
19375
19605
  function defaultUnzoomOut(input) {
19376
- const ext = path39.extname(input);
19606
+ const ext = path40.extname(input);
19377
19607
  return `${ext ? input.slice(0, -ext.length) : input}.unzoomed.png`;
19378
19608
  }
19379
19609
  async function unzoomFile(client2, input, opts = {}) {
19380
- const out = path39.resolve(opts.out ?? defaultUnzoomOut(input));
19381
- if (path39.resolve(input) === out) throw new Error("unzoom will not overwrite its own input; pass a different --out");
19382
- if (existsSync29(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
19383
- const bytes = await readFile26(input);
19610
+ const out = path40.resolve(opts.out ?? defaultUnzoomOut(input));
19611
+ if (path40.resolve(input) === out) throw new Error("unzoom will not overwrite its own input; pass a different --out");
19612
+ if (existsSync30(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
19613
+ const bytes = await readFile27(input);
19384
19614
  const metadata = imageMetadata(bytes);
19385
19615
  if (!metadata) throw new Error(`${input} is not a readable PNG or JPEG`);
19386
19616
  const { width, height, format } = metadata;
@@ -19393,22 +19623,22 @@ async function unzoomFile(client2, input, opts = {}) {
19393
19623
  throw new Error(`${input} is ${width}x${height}; PixelLab's unzoom takes at most 2048x2048 worth of pixels`);
19394
19624
  }
19395
19625
  const res = await client2.unzoom({ image: { base64: bytes.toString("base64"), format }, quantize: opts.quantize });
19396
- await mkdir11(path39.dirname(out), { recursive: true });
19397
- await writeFile14(out, res.png);
19626
+ await mkdir12(path40.dirname(out), { recursive: true });
19627
+ await writeFile15(out, res.png);
19398
19628
  return { out, original: res.originalSize, unzoomed: res.unzoomedSize, zoomFactor: res.zoomFactor, usage: res.usage };
19399
19629
  }
19400
19630
  var FONT_WEIGHTS = ["Bold", "Regular"];
19401
19631
  var FONT_GLYPH_SIZES = [8, 16, 32, 64];
19402
19632
  var FONT_COST_GENERATIONS = 25;
19403
19633
  function fontOutputPaths(out) {
19404
- const base = path39.resolve(out.replace(/\.(ttf|png)$/i, ""));
19634
+ const base = path40.resolve(out.replace(/\.(ttf|png)$/i, ""));
19405
19635
  return { ttf: `${base}.ttf`, atlas: `${base}.png` };
19406
19636
  }
19407
19637
  async function generateFont(client2, opts) {
19408
19638
  const { ttf, atlas } = fontOutputPaths(opts.out);
19409
19639
  if (!opts.force) {
19410
19640
  for (const file of [ttf, atlas]) {
19411
- if (existsSync29(file)) throw new Error(`${file} already exists; pass --force to replace it`);
19641
+ if (existsSync30(file)) throw new Error(`${file} already exists; pass --force to replace it`);
19412
19642
  }
19413
19643
  }
19414
19644
  const sleep3 = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
@@ -19435,9 +19665,9 @@ async function generateFont(client2, opts) {
19435
19665
  client2.download(job.download_ttf_url),
19436
19666
  client2.download(job.download_atlas_url)
19437
19667
  ]);
19438
- await mkdir11(path39.dirname(ttf), { recursive: true });
19439
- await writeFile14(ttf, ttfBytes);
19440
- await writeFile14(atlas, atlasBytes);
19668
+ await mkdir12(path40.dirname(ttf), { recursive: true });
19669
+ await writeFile15(ttf, ttfBytes);
19670
+ await writeFile15(atlas, atlasBytes);
19441
19671
  return { jobId, ttf, atlas, glyphPx: job.glyph_px ?? null, usage: job.usage };
19442
19672
  }
19443
19673
  if (Date.now() - started > timeout) {
@@ -19451,7 +19681,7 @@ async function generateFont(client2, opts) {
19451
19681
 
19452
19682
  // src/cli/commands/pixellab-utils.ts
19453
19683
  function loadKeys(args) {
19454
- loadEnvFiles(path40.dirname(path40.resolve(args.manifest)));
19684
+ loadEnvFiles(path41.dirname(path41.resolve(args.manifest)));
19455
19685
  loadEnvFiles(process.cwd());
19456
19686
  }
19457
19687
  async function runUnzoom(args) {
@@ -19517,19 +19747,19 @@ async function runFont(args) {
19517
19747
  }
19518
19748
 
19519
19749
  // src/cli/commands/quality.ts
19520
- import path42 from "path";
19521
- import { readFile as readFile28 } from "fs/promises";
19750
+ import path43 from "path";
19751
+ import { readFile as readFile29 } from "fs/promises";
19522
19752
 
19523
19753
  // src/pipeline/quality-regression.ts
19524
- import path41 from "path";
19525
- import { existsSync as existsSync30 } from "fs";
19526
- import { readFile as readFile27 } from "fs/promises";
19527
- import { z as z10 } from "zod";
19754
+ import path42 from "path";
19755
+ import { existsSync as existsSync31 } from "fs";
19756
+ import { readFile as readFile28 } from "fs/promises";
19757
+ import { z as z11 } from "zod";
19528
19758
  var SHA256_RE = /^[0-9a-f]{64}$/;
19529
19759
  var HEX_RE = /^#[0-9a-f]{6}$/;
19530
19760
  var MAX_REDMEAN_DISTANCE = 765;
19531
- var QualityPathSchema = z10.string().min(1).refine(
19532
- (value) => !path41.posix.isAbsolute(value) && !value.includes("\\"),
19761
+ var QualityPathSchema = z11.string().min(1).refine(
19762
+ (value) => !path42.posix.isAbsolute(value) && !value.includes("\\"),
19533
19763
  "expected a portable relative path"
19534
19764
  );
19535
19765
  var DEFAULT_QUALITY_TOLERANCES = {
@@ -19542,53 +19772,53 @@ var DEFAULT_QUALITY_TOLERANCES = {
19542
19772
  maxEdgeContrastDrop: 0.03,
19543
19773
  maxIsolatedPixelIncrease: 5e-3
19544
19774
  };
19545
- var QualityToleranceSchema = z10.object({
19546
- requireExactHash: z10.boolean(),
19547
- maxColorCountIncrease: z10.number().int().min(0),
19548
- maxNewColors: z10.number().int().min(0),
19549
- maxTransparencyDelta: z10.number().min(0).max(1),
19550
- maxPartialAlphaIncrease: z10.number().min(0).max(1),
19551
- maxEdgeDensityDelta: z10.number().min(0).max(1),
19552
- maxEdgeContrastDrop: z10.number().min(0).max(1),
19553
- maxIsolatedPixelIncrease: z10.number().min(0).max(1)
19775
+ var QualityToleranceSchema = z11.object({
19776
+ requireExactHash: z11.boolean(),
19777
+ maxColorCountIncrease: z11.number().int().min(0),
19778
+ maxNewColors: z11.number().int().min(0),
19779
+ maxTransparencyDelta: z11.number().min(0).max(1),
19780
+ maxPartialAlphaIncrease: z11.number().min(0).max(1),
19781
+ maxEdgeDensityDelta: z11.number().min(0).max(1),
19782
+ maxEdgeContrastDrop: z11.number().min(0).max(1),
19783
+ maxIsolatedPixelIncrease: z11.number().min(0).max(1)
19554
19784
  }).strict();
19555
- var ImageQualityMetricsSchema = z10.object({
19556
- sha256: z10.string().regex(SHA256_RE),
19557
- width: z10.number().int().min(1),
19558
- height: z10.number().int().min(1),
19559
- colorCount: z10.number().int().min(0),
19560
- colors: z10.array(z10.string().regex(HEX_RE)),
19561
- transparency: z10.number().min(0).max(1),
19562
- partialAlpha: z10.number().min(0).max(1),
19563
- edgeDensity: z10.number().min(0).max(1),
19564
- meanEdgeContrast: z10.number().min(0).max(1),
19565
- isolatedPixelRatio: z10.number().min(0).max(1)
19785
+ var ImageQualityMetricsSchema = z11.object({
19786
+ sha256: z11.string().regex(SHA256_RE),
19787
+ width: z11.number().int().min(1),
19788
+ height: z11.number().int().min(1),
19789
+ colorCount: z11.number().int().min(0),
19790
+ colors: z11.array(z11.string().regex(HEX_RE)),
19791
+ transparency: z11.number().min(0).max(1),
19792
+ partialAlpha: z11.number().min(0).max(1),
19793
+ edgeDensity: z11.number().min(0).max(1),
19794
+ meanEdgeContrast: z11.number().min(0).max(1),
19795
+ isolatedPixelRatio: z11.number().min(0).max(1)
19566
19796
  }).strict();
19567
- var QualityBaselineCaseSchema = z10.object({
19568
- id: z10.string().regex(/^[a-z0-9][a-z0-9/_-]*$/),
19797
+ var QualityBaselineCaseSchema = z11.object({
19798
+ id: z11.string().regex(/^[a-z0-9][a-z0-9/_-]*$/),
19569
19799
  file: QualityPathSchema,
19570
- record: z10.object({
19800
+ record: z11.object({
19571
19801
  path: QualityPathSchema,
19572
- sha256: z10.string().regex(SHA256_RE)
19802
+ sha256: z11.string().regex(SHA256_RE)
19573
19803
  }).strict().optional(),
19574
19804
  expected: ImageQualityMetricsSchema,
19575
19805
  tolerances: QualityToleranceSchema
19576
19806
  }).strict();
19577
- var QualityBaselineSchema = z10.object({
19578
- $schema: z10.string().url().optional(),
19579
- format: z10.literal("pixelkiln-quality-baseline"),
19580
- schemaVersion: z10.literal(1),
19581
- cases: z10.array(QualityBaselineCaseSchema).min(1)
19807
+ var QualityBaselineSchema = z11.object({
19808
+ $schema: z11.string().url().optional(),
19809
+ format: z11.literal("pixelkiln-quality-baseline"),
19810
+ schemaVersion: z11.literal(1),
19811
+ cases: z11.array(QualityBaselineCaseSchema).min(1)
19582
19812
  }).strict().superRefine((baseline, context) => {
19583
19813
  const ids = baseline.cases.map((entry) => entry.id);
19584
19814
  if (new Set(ids).size !== ids.length) {
19585
- context.addIssue({ code: z10.ZodIssueCode.custom, path: ["cases"], message: "duplicate case id" });
19815
+ context.addIssue({ code: z11.ZodIssueCode.custom, path: ["cases"], message: "duplicate case id" });
19586
19816
  }
19587
19817
  });
19588
- var QualityInputSchema = z10.object({
19589
- id: z10.string().regex(/^[a-z0-9][a-z0-9/_-]*$/),
19590
- path: z10.string().min(1),
19591
- record: z10.string().min(1).optional(),
19818
+ var QualityInputSchema = z11.object({
19819
+ id: z11.string().regex(/^[a-z0-9][a-z0-9/_-]*$/),
19820
+ path: z11.string().min(1),
19821
+ record: z11.string().min(1).optional(),
19592
19822
  tolerances: QualityToleranceSchema.partial().optional()
19593
19823
  }).strict();
19594
19824
  function message4(error) {
@@ -19676,10 +19906,10 @@ function measureDecodedImageQuality(png, digest2) {
19676
19906
  };
19677
19907
  }
19678
19908
  async function measureImageQuality(file) {
19679
- const absolute = path41.resolve(file);
19909
+ const absolute = path42.resolve(file);
19680
19910
  let bytes;
19681
19911
  try {
19682
- bytes = await readFile27(absolute);
19912
+ bytes = await readFile28(absolute);
19683
19913
  } catch (error) {
19684
19914
  throw new Error(`Cannot read quality image ${absolute}: ${message4(error)}`, { cause: error });
19685
19915
  }
@@ -19690,7 +19920,7 @@ async function measureImageQuality(file) {
19690
19920
  }
19691
19921
  }
19692
19922
  function resolveQualityInputs(raw, inputsFilePath) {
19693
- const parsed = z10.array(QualityInputSchema).min(1).safeParse(raw);
19923
+ const parsed = z11.array(QualityInputSchema).min(1).safeParse(raw);
19694
19924
  if (!parsed.success) {
19695
19925
  throw new Error(
19696
19926
  "--inputs must be a non-empty JSON array of quality cases:\n" + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
@@ -19698,11 +19928,11 @@ function resolveQualityInputs(raw, inputsFilePath) {
19698
19928
  }
19699
19929
  const ids = parsed.data.map((entry) => entry.id);
19700
19930
  if (new Set(ids).size !== ids.length) throw new Error("--inputs contains a duplicate quality case id.");
19701
- const root = path41.dirname(path41.resolve(inputsFilePath));
19931
+ const root = path42.dirname(path42.resolve(inputsFilePath));
19702
19932
  return parsed.data.map((entry) => ({
19703
19933
  id: entry.id,
19704
- file: path41.resolve(root, entry.path),
19705
- ...entry.record ? { record: path41.resolve(root, entry.record) } : {},
19934
+ file: path42.resolve(root, entry.path),
19935
+ ...entry.record ? { record: path42.resolve(root, entry.record) } : {},
19706
19936
  tolerances: QualityToleranceSchema.parse({
19707
19937
  ...DEFAULT_QUALITY_TOLERANCES,
19708
19938
  ...entry.tolerances
@@ -19710,7 +19940,7 @@ function resolveQualityInputs(raw, inputsFilePath) {
19710
19940
  }));
19711
19941
  }
19712
19942
  function portableRelative2(from, to) {
19713
- return path41.relative(from, path41.resolve(to)).split(path41.sep).join("/") || ".";
19943
+ return path42.relative(from, path42.resolve(to)).split(path42.sep).join("/") || ".";
19714
19944
  }
19715
19945
  async function verifyRecordForImage(recordPath, imagePath) {
19716
19946
  const verification = await verifyArtifactBundle(recordPath);
@@ -19725,20 +19955,20 @@ async function verifyRecordForImage(recordPath, imagePath) {
19725
19955
  if (record.kind !== "refine" || options === null || typeof options !== "object" || options.schema !== "pixelkiln-quality") {
19726
19956
  throw new Error(`Quality record ${recordPath} is not a PixelKiln refinement record.`);
19727
19957
  }
19728
- const outputs = record.outputs.map((output) => path41.resolve(path41.dirname(recordPath), output.path));
19729
- if (!outputs.includes(path41.resolve(imagePath))) {
19958
+ const outputs = record.outputs.map((output) => path42.resolve(path42.dirname(recordPath), output.path));
19959
+ if (!outputs.includes(path42.resolve(imagePath))) {
19730
19960
  throw new Error(`Quality record ${recordPath} does not own ${imagePath}.`);
19731
19961
  }
19732
19962
  }
19733
19963
  async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
19734
19964
  if (!inputs.length) throw new Error("Cannot snapshot an empty quality baseline.");
19735
- const absolute = path41.resolve(baselinePath);
19965
+ const absolute = path42.resolve(baselinePath);
19736
19966
  for (const input of inputs) {
19737
- if (path41.resolve(input.file) === absolute || input.record && path41.resolve(input.record) === absolute) {
19967
+ if (path42.resolve(input.file) === absolute || input.record && path42.resolve(input.record) === absolute) {
19738
19968
  throw new Error(`Quality baseline output would overwrite an input: ${absolute}.`);
19739
19969
  }
19740
19970
  }
19741
- const root = path41.dirname(absolute);
19971
+ const root = path42.dirname(absolute);
19742
19972
  const cases = [];
19743
19973
  for (const input of [...inputs].sort((a, b) => a.id.localeCompare(b.id))) {
19744
19974
  const expected = await measureImageQuality(input.file);
@@ -19765,8 +19995,8 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
19765
19995
  cases
19766
19996
  });
19767
19997
  const data = Buffer.from(JSON.stringify(baseline, null, 2) + "\n");
19768
- if (existsSync30(absolute) && !options.force) {
19769
- const current = await readFile27(absolute);
19998
+ if (existsSync31(absolute) && !options.force) {
19999
+ const current = await readFile28(absolute);
19770
20000
  if (!current.equals(data)) {
19771
20001
  throw new OverwriteRefusedError(
19772
20002
  `Quality baseline already exists with different content: ${absolute}. Pass --force to replace it.`,
@@ -19778,10 +20008,10 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
19778
20008
  return { baseline, path: absolute, changed: result.changed.length > 0 };
19779
20009
  }
19780
20010
  async function readQualityBaseline(baselinePath) {
19781
- const absolute = path41.resolve(baselinePath);
20011
+ const absolute = path42.resolve(baselinePath);
19782
20012
  let raw;
19783
20013
  try {
19784
- raw = JSON.parse(await readFile27(absolute, "utf8"));
20014
+ raw = JSON.parse(await readFile28(absolute, "utf8"));
19785
20015
  } catch (error) {
19786
20016
  throw new Error(`Could not read quality baseline ${absolute}: ${message4(error)}`, { cause: error });
19787
20017
  }
@@ -19851,12 +20081,12 @@ function regressionViolations(expected, actual, tolerances) {
19851
20081
  return { violations, warnings };
19852
20082
  }
19853
20083
  async function checkQualityBaseline(baselinePath) {
19854
- const absolute = path41.resolve(baselinePath);
20084
+ const absolute = path42.resolve(baselinePath);
19855
20085
  const baseline = await readQualityBaseline(absolute);
19856
- const root = path41.dirname(absolute);
20086
+ const root = path42.dirname(absolute);
19857
20087
  const cases = [];
19858
20088
  for (const entry of baseline.cases) {
19859
- const file = path41.resolve(root, entry.file);
20089
+ const file = path42.resolve(root, entry.file);
19860
20090
  let actual = null;
19861
20091
  const violations = [];
19862
20092
  const warnings = [];
@@ -19869,7 +20099,7 @@ async function checkQualityBaseline(baselinePath) {
19869
20099
  violations.push(message4(error));
19870
20100
  }
19871
20101
  if (entry.record) {
19872
- const recordPath = path41.resolve(root, entry.record.path);
20102
+ const recordPath = path42.resolve(root, entry.record.path);
19873
20103
  try {
19874
20104
  const recordHash = await sha256File(recordPath);
19875
20105
  if (recordHash !== entry.record.sha256) violations.push("quality record hash changed");
@@ -19911,15 +20141,15 @@ async function runQuality(args) {
19911
20141
  if (!args.out) throw new Error("quality snapshot needs --out <pixelkiln.quality.json>.");
19912
20142
  let raw;
19913
20143
  try {
19914
- raw = JSON.parse(await readFile28(path42.resolve(args.inputs), "utf8"));
20144
+ raw = JSON.parse(await readFile29(path43.resolve(args.inputs), "utf8"));
19915
20145
  } catch (error) {
19916
20146
  throw new Error(
19917
- `Could not read quality inputs ${path42.resolve(args.inputs)}: ${error instanceof Error ? error.message : String(error)}`,
20147
+ `Could not read quality inputs ${path43.resolve(args.inputs)}: ${error instanceof Error ? error.message : String(error)}`,
19918
20148
  { cause: error }
19919
20149
  );
19920
20150
  }
19921
20151
  const inputs = resolveQualityInputs(raw, args.inputs);
19922
- if (path42.resolve(args.inputs) === path42.resolve(args.out)) {
20152
+ if (path43.resolve(args.inputs) === path43.resolve(args.out)) {
19923
20153
  throw new Error("quality snapshot --out must not overwrite its --inputs file.");
19924
20154
  }
19925
20155
  const result = await snapshotQualityBaseline(inputs, args.out, { force: args.force });
@@ -19932,7 +20162,7 @@ async function runQuality(args) {
19932
20162
  }, null, 2));
19933
20163
  } else {
19934
20164
  log(
19935
- ` ${result.changed ? "wrote" : "unchanged"} ${path42.relative(process.cwd(), result.path)} (${result.baseline.cases.length} case(s))`
20165
+ ` ${result.changed ? "wrote" : "unchanged"} ${path43.relative(process.cwd(), result.path)} (${result.baseline.cases.length} case(s))`
19936
20166
  );
19937
20167
  log(` Review the tolerances, commit the baseline, then run quality check in CI.`);
19938
20168
  }
@@ -19958,54 +20188,54 @@ async function runQuality(args) {
19958
20188
  }
19959
20189
 
19960
20190
  // src/cli/commands/recipe.ts
19961
- import path44 from "path";
20191
+ import path45 from "path";
19962
20192
 
19963
20193
  // src/recipes.ts
19964
- import path43 from "path";
19965
- import { existsSync as existsSync31 } from "fs";
19966
- import { readdir as readdir3, readFile as readFile29 } from "fs/promises";
20194
+ import path44 from "path";
20195
+ import { existsSync as existsSync32 } from "fs";
20196
+ import { readdir as readdir3, readFile as readFile30 } from "fs/promises";
19967
20197
  import { fileURLToPath } from "url";
19968
- import { z as z11 } from "zod";
20198
+ import { z as z12 } from "zod";
19969
20199
  var SHA256_RE2 = /^[0-9a-f]{64}$/;
19970
20200
  var RECIPE_ID_RE = /^[a-z0-9][a-z0-9-]*\/[a-z0-9][a-z0-9-]*$/;
19971
20201
  var VERSION_RE = /^\d+\.\d+\.\d+$/;
19972
20202
  var RECIPE_FILE = "pixelkiln.recipe.json";
19973
20203
  var RECIPE_DIR_TOKEN = "{{recipeDir}}";
19974
- var PortablePathSchema = z11.string().min(1).refine((value) => {
19975
- if (value.includes("\\") || path43.posix.isAbsolute(value)) return false;
20204
+ var PortablePathSchema = z12.string().min(1).refine((value) => {
20205
+ if (value.includes("\\") || path44.posix.isAbsolute(value)) return false;
19976
20206
  const parts = value.split("/");
19977
20207
  return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
19978
20208
  }, "expected a portable relative path without '.', '..', or backslashes");
19979
- var RecipeBindingSchema = z11.object({
19980
- nodeId: z11.string().min(1),
19981
- input: z11.string().min(1)
20209
+ var RecipeBindingSchema = z12.object({
20210
+ nodeId: z12.string().min(1),
20211
+ input: z12.string().min(1)
19982
20212
  }).strict();
19983
- var RecipeSchema = z11.object({
19984
- $schema: z11.string().url().optional(),
19985
- format: z11.literal("pixelkiln-recipe"),
19986
- schemaVersion: z11.literal(1),
19987
- id: z11.string().regex(RECIPE_ID_RE, "expected provider/name"),
19988
- version: z11.string().regex(VERSION_RE, "expected x.y.z"),
19989
- provider: z11.string().regex(/^[a-z0-9][a-z0-9-]*$/),
19990
- summary: z11.string().min(1).max(240),
19991
- files: z11.array(z11.object({
20213
+ var RecipeSchema = z12.object({
20214
+ $schema: z12.string().url().optional(),
20215
+ format: z12.literal("pixelkiln-recipe"),
20216
+ schemaVersion: z12.literal(1),
20217
+ id: z12.string().regex(RECIPE_ID_RE, "expected provider/name"),
20218
+ version: z12.string().regex(VERSION_RE, "expected x.y.z"),
20219
+ provider: z12.string().regex(/^[a-z0-9][a-z0-9-]*$/),
20220
+ summary: z12.string().min(1).max(240),
20221
+ files: z12.array(z12.object({
19992
20222
  path: PortablePathSchema,
19993
- role: z11.enum(["workflow", "reference"]),
19994
- sha256: z11.string().regex(SHA256_RE2)
20223
+ role: z12.enum(["workflow", "reference"]),
20224
+ sha256: z12.string().regex(SHA256_RE2)
19995
20225
  }).strict()).default([]),
19996
- models: z11.array(z11.object({
20226
+ models: z12.array(z12.object({
19997
20227
  path: PortablePathSchema,
19998
- sha256: z11.string().regex(SHA256_RE2),
19999
- source: z11.string().url(),
20000
- license: z11.string().min(1)
20228
+ sha256: z12.string().regex(SHA256_RE2),
20229
+ source: z12.string().url(),
20230
+ license: z12.string().min(1)
20001
20231
  }).strict()).default([]),
20002
- styleId: z11.string().regex(/^[a-z0-9][a-z0-9-]*$/),
20232
+ styleId: z12.string().regex(/^[a-z0-9][a-z0-9-]*$/),
20003
20233
  style: StyleSchema,
20004
- workflow: z11.object({
20234
+ workflow: z12.object({
20005
20235
  path: PortablePathSchema,
20006
- outputNodeId: z11.string().min(1),
20007
- numImages: z11.number().int().min(1).max(16),
20008
- bindings: z11.object({
20236
+ outputNodeId: z12.string().min(1),
20237
+ numImages: z12.number().int().min(1).max(16),
20238
+ bindings: z12.object({
20009
20239
  prompt: RecipeBindingSchema,
20010
20240
  width: RecipeBindingSchema.optional(),
20011
20241
  height: RecipeBindingSchema.optional(),
@@ -20016,67 +20246,67 @@ var RecipeSchema = z11.object({
20016
20246
  strength: RecipeBindingSchema.optional()
20017
20247
  }).strict()
20018
20248
  }).strict().optional(),
20019
- quality: z11.object({
20020
- stage: z11.enum(["composition-source", "production-candidate"]),
20021
- workingCanvas: z11.object({
20022
- width: z11.number().int().min(16),
20023
- height: z11.number().int().min(16)
20249
+ quality: z12.object({
20250
+ stage: z12.enum(["composition-source", "production-candidate"]),
20251
+ workingCanvas: z12.object({
20252
+ width: z12.number().int().min(16),
20253
+ height: z12.number().int().min(16)
20024
20254
  }).strict(),
20025
- recommendedNativeSize: z11.object({
20026
- min: z11.number().int().min(1),
20027
- max: z11.number().int().min(1)
20255
+ recommendedNativeSize: z12.object({
20256
+ min: z12.number().int().min(1),
20257
+ max: z12.number().int().min(1)
20028
20258
  }).strict(),
20029
- paletteColors: z11.object({
20030
- min: z11.number().int().min(1),
20031
- max: z11.number().int().min(1)
20259
+ paletteColors: z12.object({
20260
+ min: z12.number().int().min(1),
20261
+ max: z12.number().int().min(1)
20032
20262
  }).strict(),
20033
- background: z11.enum(["opaque", "transparent", "full-bleed"]),
20034
- checks: z11.array(z11.enum([
20263
+ background: z12.enum(["opaque", "transparent", "full-bleed"]),
20264
+ checks: z12.array(z12.enum([
20035
20265
  "prompt-coverage",
20036
20266
  "native-grid",
20037
20267
  "final-palette",
20038
20268
  "alpha-edge",
20039
20269
  "human-1x"
20040
20270
  ])).min(1),
20041
- notes: z11.array(z11.string().min(1)).default([])
20271
+ notes: z12.array(z12.string().min(1)).default([])
20042
20272
  }).strict(),
20043
- integrity: z11.object({
20044
- algorithm: z11.literal("sha256"),
20045
- digest: z11.string().regex(SHA256_RE2)
20273
+ integrity: z12.object({
20274
+ algorithm: z12.literal("sha256"),
20275
+ digest: z12.string().regex(SHA256_RE2)
20046
20276
  }).strict()
20047
20277
  }).strict().superRefine((recipe, context) => {
20048
20278
  if (recipe.style.provider && recipe.style.provider !== recipe.provider) {
20049
20279
  context.addIssue({
20050
- code: z11.ZodIssueCode.custom,
20280
+ code: z12.ZodIssueCode.custom,
20051
20281
  path: ["style", "provider"],
20052
20282
  message: "must match the recipe provider"
20053
20283
  });
20054
20284
  }
20055
20285
  const filePaths = recipe.files.map((file) => file.path);
20056
20286
  if (new Set(filePaths).size !== filePaths.length) {
20057
- context.addIssue({ code: z11.ZodIssueCode.custom, path: ["files"], message: "duplicate file path" });
20287
+ context.addIssue({ code: z12.ZodIssueCode.custom, path: ["files"], message: "duplicate file path" });
20058
20288
  }
20059
20289
  const modelPaths = recipe.models.map((model) => model.path);
20060
20290
  if (new Set(modelPaths).size !== modelPaths.length) {
20061
- context.addIssue({ code: z11.ZodIssueCode.custom, path: ["models"], message: "duplicate model path" });
20291
+ context.addIssue({ code: z12.ZodIssueCode.custom, path: ["models"], message: "duplicate model path" });
20062
20292
  }
20063
20293
  if (filePaths.includes(RECIPE_FILE)) {
20064
20294
  context.addIssue({
20065
- code: z11.ZodIssueCode.custom,
20295
+ code: z12.ZodIssueCode.custom,
20066
20296
  path: ["files"],
20067
20297
  message: `${RECIPE_FILE} cannot hash itself`
20068
20298
  });
20069
20299
  }
20070
20300
  if (recipe.quality.recommendedNativeSize.min > recipe.quality.recommendedNativeSize.max) {
20071
20301
  context.addIssue({
20072
- code: z11.ZodIssueCode.custom,
20302
+ code: z12.ZodIssueCode.custom,
20073
20303
  path: ["quality", "recommendedNativeSize"],
20074
20304
  message: "min cannot exceed max"
20075
20305
  });
20076
20306
  }
20077
20307
  if (recipe.quality.paletteColors.min > recipe.quality.paletteColors.max) {
20078
20308
  context.addIssue({
20079
- code: z11.ZodIssueCode.custom,
20309
+ code: z12.ZodIssueCode.custom,
20080
20310
  path: ["quality", "paletteColors"],
20081
20311
  message: "min cannot exceed max"
20082
20312
  });
@@ -20085,7 +20315,7 @@ var RecipeSchema = z11.object({
20085
20315
  (file) => file.path === recipe.workflow.path && file.role === "workflow"
20086
20316
  )) {
20087
20317
  context.addIssue({
20088
- code: z11.ZodIssueCode.custom,
20318
+ code: z12.ZodIssueCode.custom,
20089
20319
  path: ["workflow", "path"],
20090
20320
  message: "must name a file whose role is workflow"
20091
20321
  });
@@ -20093,7 +20323,7 @@ var RecipeSchema = z11.object({
20093
20323
  if (recipe.provider === "comfyui") {
20094
20324
  if (!recipe.workflow) {
20095
20325
  context.addIssue({
20096
- code: z11.ZodIssueCode.custom,
20326
+ code: z12.ZodIssueCode.custom,
20097
20327
  path: ["workflow"],
20098
20328
  message: "ComfyUI recipes require workflow metadata"
20099
20329
  });
@@ -20102,7 +20332,7 @@ var RecipeSchema = z11.object({
20102
20332
  const options = recipe.style.providerOptions.comfyui;
20103
20333
  if (!options || typeof options !== "object" || Array.isArray(options)) {
20104
20334
  context.addIssue({
20105
- code: z11.ZodIssueCode.custom,
20335
+ code: z12.ZodIssueCode.custom,
20106
20336
  path: ["style", "providerOptions", "comfyui"],
20107
20337
  message: "ComfyUI recipes require ComfyUI provider options"
20108
20338
  });
@@ -20116,7 +20346,7 @@ var RecipeSchema = z11.object({
20116
20346
  };
20117
20347
  if (JSON.stringify(canonical3(options)) !== JSON.stringify(canonical3(expected))) {
20118
20348
  context.addIssue({
20119
- code: z11.ZodIssueCode.custom,
20349
+ code: z12.ZodIssueCode.custom,
20120
20350
  path: ["style", "providerOptions", "comfyui"],
20121
20351
  message: "must match workflow metadata and use {{recipeDir}} for workflowFile"
20122
20352
  });
@@ -20148,10 +20378,10 @@ function message5(error) {
20148
20378
  return error instanceof Error ? error.message : String(error);
20149
20379
  }
20150
20380
  async function readRecipeFile(recipePath, bundled) {
20151
- const absolute = path43.resolve(recipePath);
20381
+ const absolute = path44.resolve(recipePath);
20152
20382
  let raw;
20153
20383
  try {
20154
- raw = JSON.parse(await readFile29(absolute, "utf8"));
20384
+ raw = JSON.parse(await readFile30(absolute, "utf8"));
20155
20385
  } catch (error) {
20156
20386
  throw new Error(`Could not read recipe ${absolute}: ${message5(error)}`, { cause: error });
20157
20387
  }
@@ -20162,13 +20392,13 @@ async function readRecipeFile(recipePath, bundled) {
20162
20392
  ` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
20163
20393
  );
20164
20394
  }
20165
- return { recipe: parsed.data, authored: raw, path: absolute, dir: path43.dirname(absolute), bundled };
20395
+ return { recipe: parsed.data, authored: raw, path: absolute, dir: path44.dirname(absolute), bundled };
20166
20396
  }
20167
20397
  async function walkRecipeFiles(root) {
20168
- if (!existsSync31(root)) return [];
20398
+ if (!existsSync32(root)) return [];
20169
20399
  const out = [];
20170
20400
  for (const entry of await readdir3(root, { withFileTypes: true })) {
20171
- const resolved = path43.join(root, entry.name);
20401
+ const resolved = path44.join(root, entry.name);
20172
20402
  if (entry.isDirectory()) out.push(...await walkRecipeFiles(resolved));
20173
20403
  else if (entry.isFile() && entry.name === RECIPE_FILE) out.push(resolved);
20174
20404
  }
@@ -20179,7 +20409,7 @@ function bundledRecipeRoot() {
20179
20409
  }
20180
20410
  async function listBundledRecipes(root = bundledRecipeRoot()) {
20181
20411
  const recipes = await Promise.all(
20182
- (await walkRecipeFiles(path43.resolve(root))).map((file) => readRecipeFile(file, true))
20412
+ (await walkRecipeFiles(path44.resolve(root))).map((file) => readRecipeFile(file, true))
20183
20413
  );
20184
20414
  return recipes.sort(
20185
20415
  (a, b) => a.recipe.id.localeCompare(b.recipe.id) || compareVersions(b.recipe.version, a.recipe.version)
@@ -20202,9 +20432,9 @@ function parseSelector(selector) {
20202
20432
  return { id: match[1], version: match[2] };
20203
20433
  }
20204
20434
  async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
20205
- const local = path43.resolve(target);
20206
- if (existsSync31(local)) {
20207
- const recipePath = path43.basename(local) === RECIPE_FILE ? local : path43.join(local, RECIPE_FILE);
20435
+ const local = path44.resolve(target);
20436
+ if (existsSync32(local)) {
20437
+ const recipePath = path44.basename(local) === RECIPE_FILE ? local : path44.join(local, RECIPE_FILE);
20208
20438
  return readRecipeFile(recipePath, false);
20209
20439
  }
20210
20440
  if (target.startsWith(".") || target.startsWith("/") || target.includes("\\")) {
@@ -20218,8 +20448,8 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
20218
20448
  return matches[0];
20219
20449
  }
20220
20450
  async function verifyFile(root, file) {
20221
- const absolute = path43.resolve(root, ...file.path.split("/"));
20222
- if (!existsSync31(absolute)) {
20451
+ const absolute = path44.resolve(root, ...file.path.split("/"));
20452
+ if (!existsSync32(absolute)) {
20223
20453
  return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
20224
20454
  }
20225
20455
  const actualSha256 = await sha256File(absolute);
@@ -20234,7 +20464,7 @@ async function verifyRecipe(target, options = {}) {
20234
20464
  const loaded = await resolveRecipe(target, options.bundledRoot);
20235
20465
  const actualIntegrity = recipeDigest(loaded.authored);
20236
20466
  const files = await Promise.all(loaded.recipe.files.map((file) => verifyFile(loaded.dir, file)));
20237
- const modelRoot = options.modelRoot ? path43.resolve(options.modelRoot) : null;
20467
+ const modelRoot = options.modelRoot ? path44.resolve(options.modelRoot) : null;
20238
20468
  const models = modelRoot ? await Promise.all(loaded.recipe.models.map(async (model) => ({
20239
20469
  ...await verifyFile(modelRoot, model),
20240
20470
  source: model.source,
@@ -20268,7 +20498,7 @@ async function verifyRecipe(target, options = {}) {
20268
20498
  };
20269
20499
  }
20270
20500
  function renderStyle(style, destination, cwd) {
20271
- const portableDir = path43.relative(cwd, destination).split(path43.sep).join("/") || ".";
20501
+ const portableDir = path44.relative(cwd, destination).split(path44.sep).join("/") || ".";
20272
20502
  const replace = (value) => {
20273
20503
  if (typeof value === "string") return value.replaceAll(RECIPE_DIR_TOKEN, portableDir);
20274
20504
  if (Array.isArray(value)) return value.map(replace);
@@ -20280,25 +20510,25 @@ function renderStyle(style, destination, cwd) {
20280
20510
  return replace(style);
20281
20511
  }
20282
20512
  async function installRecipe(target, options = {}) {
20283
- const cwd = path43.resolve(options.cwd ?? process.cwd());
20513
+ const cwd = path44.resolve(options.cwd ?? process.cwd());
20284
20514
  const loaded = await resolveRecipe(target, options.bundledRoot);
20285
20515
  const verification = await verifyRecipe(loaded.path);
20286
20516
  if (!verification.ok) {
20287
20517
  throw new Error(`Recipe ${loaded.recipe.id}@${loaded.recipe.version} failed integrity verification.`);
20288
20518
  }
20289
- const destination = options.out ? path43.resolve(cwd, options.out) : path43.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
20519
+ const destination = options.out ? path44.resolve(cwd, options.out) : path44.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
20290
20520
  const sources = [
20291
20521
  { path: RECIPE_FILE, source: loaded.path },
20292
- ...loaded.recipe.files.map((file) => ({ path: file.path, source: path43.join(loaded.dir, ...file.path.split("/")) }))
20522
+ ...loaded.recipe.files.map((file) => ({ path: file.path, source: path44.join(loaded.dir, ...file.path.split("/")) }))
20293
20523
  ];
20294
20524
  const files = await Promise.all(sources.map(async (file) => ({
20295
- path: path43.join(destination, ...file.path.split("/")),
20296
- data: await readFile29(file.source)
20525
+ path: path44.join(destination, ...file.path.split("/")),
20526
+ data: await readFile30(file.source)
20297
20527
  })));
20298
20528
  if (!options.force) {
20299
20529
  for (const file of files) {
20300
- if (!existsSync31(file.path)) continue;
20301
- const current = await readFile29(file.path);
20530
+ if (!existsSync32(file.path)) continue;
20531
+ const current = await readFile30(file.path);
20302
20532
  if (!current.equals(file.data)) {
20303
20533
  throw new OverwriteRefusedError(
20304
20534
  `Recipe destination has local changes: ${file.path}. Choose another --out or pass --force to replace declared recipe files.`,
@@ -20392,14 +20622,14 @@ async function runRecipe(args) {
20392
20622
  }, null, 2));
20393
20623
  } else {
20394
20624
  log(` installed ${result.recipe.id}@${result.recipe.version}`);
20395
- log(` ${path44.relative(process.cwd(), result.destination) || "."}`);
20625
+ log(` ${path45.relative(process.cwd(), result.destination) || "."}`);
20396
20626
  log(`
20397
20627
  Add this entry under your manifest's styles object:`);
20398
20628
  log(JSON.stringify({ [result.styleId]: result.style }, null, 2));
20399
20629
  if (result.recipe.models.length) {
20400
20630
  log(`
20401
20631
  Models are not downloaded automatically. Verify them with:`);
20402
- log(` pixelkiln recipe verify ${path44.relative(process.cwd(), result.destination)} --model-root <ComfyUI/models>`);
20632
+ log(` pixelkiln recipe verify ${path45.relative(process.cwd(), result.destination)} --model-root <ComfyUI/models>`);
20403
20633
  }
20404
20634
  }
20405
20635
  return;
@@ -20407,7 +20637,7 @@ async function runRecipe(args) {
20407
20637
  }
20408
20638
 
20409
20639
  // src/cli/commands/refine.ts
20410
- import path45 from "path";
20640
+ import path46 from "path";
20411
20641
  async function runRefine(args) {
20412
20642
  if (args.from || args.subcommand === "approve") {
20413
20643
  if (!args.from) {
@@ -20437,12 +20667,12 @@ async function runRefine(args) {
20437
20667
  ` recovered ${result3.detection.columns}x${result3.detection.rows} native grid (${result3.detection.confidence}: ${result3.detection.consensus})`
20438
20668
  );
20439
20669
  log(` enforced ${result3.palette.length}-color palette without dithering`);
20440
- log(` wrote ${path45.relative(process.cwd(), result3.output)}`);
20441
- log(` quality record: ${path45.relative(process.cwd(), result3.record)}`);
20670
+ log(` wrote ${path46.relative(process.cwd(), result3.output)}`);
20671
+ log(` quality record: ${path46.relative(process.cwd(), result3.record)}`);
20442
20672
  log(`
20443
20673
  Automated checks passed. Human 1\xD7 review is still required:`);
20444
20674
  log(
20445
- ` pixelkiln refine approve --from ${path45.relative(process.cwd(), result3.record)} --reviewer "Your Name"`
20675
+ ` pixelkiln refine approve --from ${path46.relative(process.cwd(), result3.record)} --reviewer "Your Name"`
20446
20676
  );
20447
20677
  }
20448
20678
  return;
@@ -20470,7 +20700,7 @@ async function runRefine(args) {
20470
20700
  if (args.json) log(JSON.stringify(result3, null, 2));
20471
20701
  else {
20472
20702
  log(
20473
- result3.options.frameSet ? ` approved ${result3.outputs.length} frames in ${path45.relative(process.cwd(), result3.record)} by ${args.reviewer.trim()}` : ` approved ${path45.relative(process.cwd(), result3.output)} by ${args.reviewer.trim()}`
20703
+ result3.options.frameSet ? ` approved ${result3.outputs.length} frames in ${path46.relative(process.cwd(), result3.record)} by ${args.reviewer.trim()}` : ` approved ${path46.relative(process.cwd(), result3.output)} by ${args.reviewer.trim()}`
20474
20704
  );
20475
20705
  }
20476
20706
  return;
@@ -20479,7 +20709,7 @@ async function runRefine(args) {
20479
20709
  if (args.json) {
20480
20710
  log(JSON.stringify(result2, null, 2));
20481
20711
  } else {
20482
- log(` ${result2.safe ? "release-ready" : "not release-ready"}: ${path45.relative(process.cwd(), result2.output || result2.record)}`);
20712
+ log(` ${result2.safe ? "release-ready" : "not release-ready"}: ${path46.relative(process.cwd(), result2.output || result2.record)}`);
20483
20713
  for (const reason of result2.reasons) log(` ${reason}`);
20484
20714
  }
20485
20715
  if (!result2.safe) process.exitCode = 1;
@@ -20525,7 +20755,7 @@ async function runRefine(args) {
20525
20755
  log(` ${item.state.padEnd(16)} ${item.key} ${item.reason}`);
20526
20756
  if (item.state === "needs-approval") {
20527
20757
  log(
20528
- ` pixelkiln refine approve --from ${path45.relative(process.cwd(), item.record)} --reviewer "Your Name"`
20758
+ ` pixelkiln refine approve --from ${path46.relative(process.cwd(), item.record)} --reviewer "Your Name"`
20529
20759
  );
20530
20760
  }
20531
20761
  }
@@ -20536,28 +20766,189 @@ async function runRefine(args) {
20536
20766
  }
20537
20767
 
20538
20768
  // src/cli/commands/skeleton.ts
20539
- import path46 from "path";
20540
- import { readFile as readFile30, writeFile as writeFile15 } from "fs/promises";
20769
+ import path47 from "path";
20770
+ import { existsSync as existsSync33 } from "fs";
20771
+ import { readFile as readFile31, writeFile as writeFile16 } from "fs/promises";
20772
+
20773
+ // src/skeleton-preview.ts
20774
+ var RIGHT = [255, 140, 40, 255];
20775
+ var LEFT = [60, 150, 255, 255];
20776
+ var CENTER = [240, 240, 240, 255];
20777
+ var GROUND = [28, 28, 34, 255];
20778
+ var OUTLINE = [0, 0, 0, 255];
20779
+ var GUTTER = 4;
20780
+ var MIN_CELL = 192;
20781
+ function renderSkeletonSheet(set, options = {}) {
20782
+ const poses = [set.firstFrameKeypoints, ...set.frames];
20783
+ const source = options.source;
20784
+ const scale = source ? Math.max(1, Math.ceil(MIN_CELL / Math.max(source.width, source.height))) : 1;
20785
+ const cellW = source ? source.width * scale : options.cellSize ?? MIN_CELL;
20786
+ const cellH = source ? source.height * scale : options.cellSize ?? MIN_CELL;
20787
+ const columns = Math.min(options.columns ?? 8, poses.length);
20788
+ const rows = Math.ceil(poses.length / columns);
20789
+ const width = columns * cellW + (columns + 1) * GUTTER;
20790
+ const height = rows * cellH + (rows + 1) * GUTTER;
20791
+ const canvas = new Canvas(width, height);
20792
+ canvas.fill(0, 0, width, height, [12, 12, 16, 255]);
20793
+ poses.forEach((pose, index) => {
20794
+ const left = GUTTER + index % columns * (cellW + GUTTER);
20795
+ const top = GUTTER + Math.floor(index / columns) * (cellH + GUTTER);
20796
+ canvas.fill(left, top, cellW, cellH, GROUND);
20797
+ if (source) canvas.drawImage(source, left, top, scale, index === 0 ? 1 : 0.45);
20798
+ drawPose(canvas, pose, left, top, cellW, cellH, Math.max(1, Math.round(Math.min(cellW, cellH) / 128)));
20799
+ });
20800
+ return encodeRgbaPng(width, height, canvas.data);
20801
+ }
20802
+ function drawPose(canvas, pose, left, top, w, h, stroke) {
20803
+ const at = new Map(pose.map((joint) => [joint.label, joint]));
20804
+ const point = (joint) => [left + joint.x * (w - 1), top + joint.y * (h - 1)];
20805
+ for (const outline of [true, false]) {
20806
+ for (const [from, to] of SKELETON_BONES) {
20807
+ const a = at.get(from);
20808
+ const b = at.get(to);
20809
+ if (!a || !b) continue;
20810
+ const [x0, y0] = point(a);
20811
+ const [x1, y1] = point(b);
20812
+ canvas.line(x0, y0, x1, y1, outline ? OUTLINE : colorOf(to), outline ? stroke + 2 : stroke);
20813
+ }
20814
+ for (const joint of pose) {
20815
+ const [x, y] = point(joint);
20816
+ canvas.dot(x, y, outline ? stroke + 3 : stroke + 1, outline ? OUTLINE : colorOf(joint.label));
20817
+ }
20818
+ }
20819
+ }
20820
+ function colorOf(label) {
20821
+ if (label.startsWith("RIGHT")) return RIGHT;
20822
+ if (label.startsWith("LEFT")) return LEFT;
20823
+ return CENTER;
20824
+ }
20825
+ var Canvas = class {
20826
+ constructor(width, height) {
20827
+ this.width = width;
20828
+ this.height = height;
20829
+ this.data = Buffer.alloc(width * height * 4);
20830
+ }
20831
+ width;
20832
+ height;
20833
+ data;
20834
+ set(x, y, [r, g, b, a]) {
20835
+ const px = Math.round(x);
20836
+ const py = Math.round(y);
20837
+ if (px < 0 || py < 0 || px >= this.width || py >= this.height) return;
20838
+ const i = (py * this.width + px) * 4;
20839
+ this.data[i] = r;
20840
+ this.data[i + 1] = g;
20841
+ this.data[i + 2] = b;
20842
+ this.data[i + 3] = a;
20843
+ }
20844
+ fill(x, y, w, h, color) {
20845
+ for (let dy = 0; dy < h; dy++) for (let dx = 0; dx < w; dx++) this.set(x + dx, y + dy, color);
20846
+ }
20847
+ /** Nearest-neighbour upscale, composited over what is there, at `strength`. */
20848
+ drawImage(image, left, top, scale, strength) {
20849
+ for (let y = 0; y < image.height; y++) {
20850
+ for (let x = 0; x < image.width; x++) {
20851
+ const i = (y * image.width + x) * 4;
20852
+ const alpha = image.pixels[i + 3] / 255 * strength;
20853
+ if (alpha === 0) continue;
20854
+ const src = [image.pixels[i], image.pixels[i + 1], image.pixels[i + 2]];
20855
+ for (let dy = 0; dy < scale; dy++) {
20856
+ for (let dx = 0; dx < scale; dx++) {
20857
+ const o = ((top + y * scale + dy) * this.width + left + x * scale + dx) * 4;
20858
+ for (let c = 0; c < 3; c++) this.data[o + c] = Math.round(src[c] * alpha + this.data[o + c] * (1 - alpha));
20859
+ }
20860
+ }
20861
+ }
20862
+ }
20863
+ }
20864
+ line(x0, y0, x1, y1, color, stroke) {
20865
+ const steps = Math.max(1, Math.ceil(Math.max(Math.abs(x1 - x0), Math.abs(y1 - y0))));
20866
+ for (let step = 0; step <= steps; step++) {
20867
+ const t = step / steps;
20868
+ this.dot(x0 + (x1 - x0) * t, y0 + (y1 - y0) * t, stroke, color);
20869
+ }
20870
+ }
20871
+ /** A filled square `size` wide centred on (x, y): crisp at pixel-art scale. */
20872
+ dot(x, y, size, color) {
20873
+ const half = Math.floor(size / 2);
20874
+ for (let dy = -half; dy < size - half; dy++) for (let dx = -half; dx < size - half; dx++) this.set(x + dx, y + dy, color);
20875
+ }
20876
+ };
20877
+
20878
+ // src/cli/commands/skeleton.ts
20541
20879
  async function runEstimateSkeleton(args) {
20542
20880
  const image = args.target;
20543
20881
  if (!image) throw new UsageError("estimate-skeleton needs an image path: pixelkiln estimate-skeleton <image> [--out keypoints.json]");
20544
- loadEnvFiles(path46.dirname(path46.resolve(args.manifest)));
20882
+ if (args.out && existsSync33(path47.resolve(args.out)) && !args.force) {
20883
+ throw new UsageError(`${args.out} already exists; pass --force to replace it`);
20884
+ }
20885
+ loadEnvFiles(path47.dirname(path47.resolve(args.manifest)));
20545
20886
  loadEnvFiles(process.cwd());
20546
- const bytes = await readFile30(path46.resolve(image));
20887
+ const bytes = await readFile31(path47.resolve(image));
20547
20888
  const metadata = imageMetadata(bytes);
20548
20889
  if (!metadata) throw new UsageError(`${image} is not a readable PNG or JPEG`);
20890
+ const sizeProblem = estimateSkeletonSizeProblem(metadata.width, metadata.height);
20891
+ if (sizeProblem) throw new UsageError(`${image} is ${sizeProblem}`);
20549
20892
  const client2 = clientFromEnv();
20550
20893
  const res = await client2.estimateSkeleton({
20551
20894
  image: { base64: bytes.toString("base64"), format: metadata.format }
20552
20895
  });
20553
- const json = JSON.stringify(res.keypoints, null, 2);
20896
+ const frames = args.frames ?? DEFAULT_SCAFFOLD_FRAMES;
20897
+ const json = JSON.stringify(scaffoldSkeletonSet(res.keypoints, frames), null, 2);
20554
20898
  if (args.out) {
20555
- await writeFile15(path46.resolve(args.out), json + "\n");
20556
- log(` wrote ${res.keypoints.length} keypoints to ${args.out}`);
20899
+ await writeFile16(path47.resolve(args.out), json + "\n");
20900
+ log(` wrote ${args.out}: the estimated pose and ${frames} frames to edit`);
20901
+ log(` preview it with: pixelkiln skeleton-preview ${args.out} --from ${image}`);
20557
20902
  } else {
20558
20903
  log(json);
20559
20904
  }
20560
20905
  }
20906
+ async function runSkeletonPreview(args) {
20907
+ const target = args.target;
20908
+ let keypointsFile;
20909
+ let sourceFile;
20910
+ let label;
20911
+ if (target.endsWith(".json")) {
20912
+ keypointsFile = path47.resolve(target);
20913
+ sourceFile = args.from ? path47.resolve(args.from) : void 0;
20914
+ label = target;
20915
+ } else {
20916
+ const loaded = await loadManifest(args.manifest);
20917
+ const specs = (await resolveSpecs(loaded, { styles: args.styles, assets: [target] })).filter((spec2) => spec2.assetId === target);
20918
+ const spec = specs.find((candidate) => candidate.revision?.mode === "animate-skeleton");
20919
+ if (!spec) {
20920
+ throw new UsageError(
20921
+ specs.length ? `${target} is not an animate-skeleton revision; pass a keypoints file instead` : `No asset "${target}" in ${args.manifest}`
20922
+ );
20923
+ }
20924
+ keypointsFile = spec.revision.keypointsFile;
20925
+ sourceFile = args.from ? path47.resolve(args.from) : spec.revision.sourceFile;
20926
+ label = `${spec.styleId}/${spec.assetId}`;
20927
+ }
20928
+ if (!existsSync33(keypointsFile)) throw new UsageError(`Keypoints file does not exist yet: ${keypointsFile}`);
20929
+ let json;
20930
+ try {
20931
+ json = JSON.parse(await readFile31(keypointsFile, "utf8"));
20932
+ } catch (error) {
20933
+ throw new UsageError(`${keypointsFile} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
20934
+ }
20935
+ const set = parseSkeletonSet(json, keypointsFile);
20936
+ let source;
20937
+ if (sourceFile) {
20938
+ if (!existsSync33(sourceFile)) {
20939
+ log(` source image does not exist yet, drawing on a plain ground: ${sourceFile}`);
20940
+ } else {
20941
+ const bytes = await readFile31(sourceFile);
20942
+ if (imageMetadata(bytes)?.format !== "png") throw new UsageError(`${sourceFile} must be a PNG to preview over`);
20943
+ source = decodePng(bytes);
20944
+ }
20945
+ }
20946
+ const out = path47.resolve(args.out ?? keypointsFile.replace(/\.json$/, "") + ".preview.png");
20947
+ await writeFile16(out, renderSkeletonSheet(set, { source }));
20948
+ log(` ${label}: ${set.frames.length} frames after the starting pose`);
20949
+ const shown = path47.relative(process.cwd(), out);
20950
+ log(` wrote ${shown.startsWith("..") ? out : shown}`);
20951
+ }
20561
20952
 
20562
20953
  // src/cli/commands/tools.ts
20563
20954
  async function runTools(args) {
@@ -20601,15 +20992,15 @@ async function runTools(args) {
20601
20992
  }
20602
20993
 
20603
20994
  // src/cli/commands/workspace.ts
20604
- import path47 from "path";
20605
- import { existsSync as existsSync32 } from "fs";
20995
+ import path48 from "path";
20996
+ import { existsSync as existsSync34 } from "fs";
20606
20997
  async function runWorkspace(args) {
20607
- const workspacePath = path47.resolve(args.workspace ?? "pixelkiln.workspace.json");
20608
- const dir = path47.dirname(workspacePath);
20998
+ const workspacePath = path48.resolve(args.workspace ?? "pixelkiln.workspace.json");
20999
+ const dir = path48.dirname(workspacePath);
20609
21000
  if (args.subcommand === "add") {
20610
- const manifestPath = path47.resolve(args.target);
21001
+ const manifestPath = path48.resolve(args.target);
20611
21002
  const loadedTarget = await loadManifest(manifestPath);
20612
- const lockPath = args.explicitLock ? path47.resolve(args.explicitLock) : path47.join(path47.dirname(manifestPath), "pixelkiln.lock.json");
21003
+ const lockPath = args.explicitLock ? path48.resolve(args.explicitLock) : path48.join(path48.dirname(manifestPath), "pixelkiln.lock.json");
20613
21004
  const ws = await loadWorkspace(workspacePath);
20614
21005
  const id = args.name ?? loadedTarget.manifest.name;
20615
21006
  if (ws.projects.some((p) => p.id === id)) {
@@ -20629,10 +21020,10 @@ async function runWorkspace(args) {
20629
21020
  ...args.account ? { account: args.account } : {}
20630
21021
  };
20631
21022
  await saveWorkspace(workspacePath, { version: 1, projects: [...ws.projects, project] });
20632
- log(` registered "${id}" in ${path47.relative(process.cwd(), workspacePath)}`);
21023
+ log(` registered "${id}" in ${path48.relative(process.cwd(), workspacePath)}`);
20633
21024
  log(` manifest: ${project.manifest}`);
20634
21025
  log(` lock: ${project.lock}`);
20635
- if (!existsSync32(lockPath)) {
21026
+ if (!existsSync34(lockPath)) {
20636
21027
  log(
20637
21028
  ` warning: no lockfile there yet; this project contributes no claims until one is generated`
20638
21029
  );
@@ -20641,7 +21032,7 @@ async function runWorkspace(args) {
20641
21032
  }
20642
21033
  if (args.subcommand === "remove") {
20643
21034
  const ws = await loadWorkspace(workspacePath);
20644
- const resolvedTarget = path47.resolve(args.target);
21035
+ const resolvedTarget = path48.resolve(args.target);
20645
21036
  const match = ws.projects.find(
20646
21037
  (p) => p.id === args.target || resolveProject(dir, p).manifestPath === resolvedTarget
20647
21038
  );
@@ -20652,11 +21043,11 @@ async function runWorkspace(args) {
20652
21043
  version: 1,
20653
21044
  projects: ws.projects.filter((p) => p !== match)
20654
21045
  });
20655
- log(` removed "${match.id}" from ${path47.relative(process.cwd(), workspacePath)}`);
21046
+ log(` removed "${match.id}" from ${path48.relative(process.cwd(), workspacePath)}`);
20656
21047
  return;
20657
21048
  }
20658
21049
  if (args.subcommand === "list") {
20659
- if (!existsSync32(workspacePath)) {
21050
+ if (!existsSync34(workspacePath)) {
20660
21051
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
20661
21052
  }
20662
21053
  const ws = await loadWorkspace(workspacePath);
@@ -20664,9 +21055,9 @@ async function runWorkspace(args) {
20664
21055
  if (args.json) {
20665
21056
  log(JSON.stringify({ version: 1, workspace: workspacePath, projects: ws.projects, diagnostics }, null, 2));
20666
21057
  } else if (!ws.projects.length) {
20667
- log(` no projects registered in ${path47.relative(process.cwd(), workspacePath)}`);
21058
+ log(` no projects registered in ${path48.relative(process.cwd(), workspacePath)}`);
20668
21059
  } else {
20669
- log(` ${ws.projects.length} project(s) in ${path47.relative(process.cwd(), workspacePath)}:`);
21060
+ log(` ${ws.projects.length} project(s) in ${path48.relative(process.cwd(), workspacePath)}:`);
20670
21061
  for (const p of ws.projects) {
20671
21062
  log(` ${p.id.padEnd(24)} ${p.manifest.padEnd(40)} (${p.provider}${p.account ? `, ${p.account}` : ""})`);
20672
21063
  }
@@ -20676,7 +21067,7 @@ async function runWorkspace(args) {
20676
21067
  return;
20677
21068
  }
20678
21069
  if (args.subcommand === "status") {
20679
- if (!existsSync32(workspacePath)) {
21070
+ if (!existsSync34(workspacePath)) {
20680
21071
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
20681
21072
  }
20682
21073
  const ws = await loadWorkspace(workspacePath);
@@ -20684,7 +21075,7 @@ async function runWorkspace(args) {
20684
21075
  if (args.json) {
20685
21076
  log(JSON.stringify({ ...report, workspace: workspacePath }, null, 2));
20686
21077
  } else {
20687
- log(` workspace: ${path47.relative(process.cwd(), workspacePath)}`);
21078
+ log(` workspace: ${path48.relative(process.cwd(), workspacePath)}`);
20688
21079
  for (const p of report.projects) {
20689
21080
  if (p.error) {
20690
21081
  log(`
@@ -20738,8 +21129,8 @@ async function runHelp() {
20738
21129
  async function runVersion() {
20739
21130
  for (const up of ["../package.json", "../../package.json"]) {
20740
21131
  const url = new URL(up, import.meta.url);
20741
- if (!existsSync33(url)) continue;
20742
- const pkg = JSON.parse(await readFile31(url, "utf8"));
21132
+ if (!existsSync35(url)) continue;
21133
+ const pkg = JSON.parse(await readFile32(url, "utf8"));
20743
21134
  if (pkg.name === "pixelkiln") {
20744
21135
  log(`${pkg.name} ${pkg.version}`);
20745
21136
  return;
@@ -20784,6 +21175,7 @@ var RUNNERS = {
20784
21175
  recipe: runRecipe,
20785
21176
  workspace: runWorkspace,
20786
21177
  "estimate-skeleton": runEstimateSkeleton,
21178
+ "skeleton-preview": runSkeletonPreview,
20787
21179
  unzoom: runUnzoom,
20788
21180
  font: runFont
20789
21181
  };