pixelkiln 0.69.0 → 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 existsSync34 } 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";
@@ -662,10 +662,10 @@ import { createReadStream } from "fs";
662
662
  function sha256(data) {
663
663
  return createHash("sha256").update(data).digest("hex");
664
664
  }
665
- async function sha256File(path48) {
665
+ async function sha256File(path49) {
666
666
  return new Promise((resolve, reject) => {
667
667
  const hash = createHash("sha256");
668
- const stream = createReadStream(path48);
668
+ const stream = createReadStream(path49);
669
669
  stream.on("data", (chunk2) => hash.update(chunk2));
670
670
  stream.on("error", reject);
671
671
  stream.on("end", () => resolve(hash.digest("hex")));
@@ -5837,6 +5837,10 @@ function parseSkeletonSet(json, file) {
5837
5837
  }
5838
5838
  return result.data;
5839
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
+ }
5840
5844
  var DEFAULT_SCAFFOLD_FRAMES = 4;
5841
5845
  function scaffoldSkeletonSet(keypoints, frames = DEFAULT_SCAFFOLD_FRAMES) {
5842
5846
  const copy = () => keypoints.map((joint) => ({ ...joint }));
@@ -6156,9 +6160,9 @@ var PixelLabClient = class {
6156
6160
  apiKey;
6157
6161
  http;
6158
6162
  /** Retries transport failures, 429, and 5xx; see http.ts for the policy. */
6159
- async request(path48, init) {
6163
+ async request(path49, init) {
6160
6164
  const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
6161
- const res = await this.http(`${BASE}${path48}`, {
6165
+ const res = await this.http(`${BASE}${path49}`, {
6162
6166
  ...init,
6163
6167
  headers: {
6164
6168
  Authorization: auth,
@@ -6168,13 +6172,13 @@ var PixelLabClient = class {
6168
6172
  });
6169
6173
  const text = await res.text();
6170
6174
  if (!res.ok) {
6171
- 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);
6172
6176
  }
6173
6177
  if (!text) return {};
6174
6178
  try {
6175
6179
  return JSON.parse(text);
6176
6180
  } catch {
6177
- throw new Error(`${init?.method ?? "GET"} ${path48} returned invalid JSON`);
6181
+ throw new Error(`${init?.method ?? "GET"} ${path49} returned invalid JSON`);
6178
6182
  }
6179
6183
  }
6180
6184
  async balance() {
@@ -6566,10 +6570,10 @@ var PixelLabClient = class {
6566
6570
  const palette = args.paletteSwatchBase64 ? { color_image: { type: "base64", base64: args.paletteSwatchBase64, format: "png" }, force_colors: true } : {};
6567
6571
  const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
6568
6572
  const south = args.reference?.south;
6569
- let path48;
6573
+ let path49;
6570
6574
  let body;
6571
6575
  if (args.mode === "v3") {
6572
- path48 = "/create-character-v3";
6576
+ path49 = "/create-character-v3";
6573
6577
  body = {
6574
6578
  description: args.description,
6575
6579
  image_size: imageSize,
@@ -6583,7 +6587,7 @@ var PixelLabClient = class {
6583
6587
  ...args.enhancePrompt ? { enhance_prompt: true } : {}
6584
6588
  };
6585
6589
  } else if (args.mode === "pro-flash") {
6586
- path48 = "/create-character-pro-flash";
6590
+ path49 = "/create-character-pro-flash";
6587
6591
  const traits = args.styleTraits;
6588
6592
  body = {
6589
6593
  description: args.description,
@@ -6607,7 +6611,7 @@ var PixelLabClient = class {
6607
6611
  } : {}
6608
6612
  };
6609
6613
  } else if (args.mode === "pro") {
6610
- path48 = "/create-character-pro";
6614
+ path49 = "/create-character-pro";
6611
6615
  body = {
6612
6616
  description: args.description,
6613
6617
  image_size: imageSize,
@@ -6621,7 +6625,7 @@ var PixelLabClient = class {
6621
6625
  ...!south && args.styleCharacterId ? { style_character_id: args.styleCharacterId } : {}
6622
6626
  };
6623
6627
  } else {
6624
- 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";
6625
6629
  body = {
6626
6630
  description: args.description,
6627
6631
  image_size: imageSize,
@@ -6638,8 +6642,8 @@ var PixelLabClient = class {
6638
6642
  ...palette
6639
6643
  };
6640
6644
  }
6641
- const raw = await this.request(path48, { method: "POST", body: JSON.stringify(body) });
6642
- 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));
6643
6647
  }
6644
6648
  /** A text edit of an existing character, applied to every direction and stored as a sibling. */
6645
6649
  async createCharacterState(args) {
@@ -9486,9 +9490,9 @@ var RetroDiffusionClient = class {
9486
9490
  if (!response.ok) throw new ProviderError("retrodiffusion", `Retro Diffusion download failed (${response.status})`, { status: response.status });
9487
9491
  return Buffer.from(await response.arrayBuffer());
9488
9492
  }
9489
- async call(path48, init = {}) {
9493
+ async call(path49, init = {}) {
9490
9494
  if (!this.token) throw new Error("RD_API_KEY is not set");
9491
- const response = await this.request(`${this.baseUrl}${path48}`, {
9495
+ const response = await this.request(`${this.baseUrl}${path49}`, {
9492
9496
  ...init,
9493
9497
  headers: {
9494
9498
  "Content-Type": "application/json",
@@ -11793,10 +11797,10 @@ function isSha256Hash(value) {
11793
11797
  function parseCache(value) {
11794
11798
  return HashCacheSchema.parse(value);
11795
11799
  }
11796
- async function loadCache(path48) {
11797
- if (!existsSync10(path48)) return { version: 1, hashes: {} };
11800
+ async function loadCache(path49) {
11801
+ if (!existsSync10(path49)) return { version: 1, hashes: {} };
11798
11802
  try {
11799
- const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile8(path48, "utf8")));
11803
+ const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile8(path49, "utf8")));
11800
11804
  if (!parsed.success) return { version: 1, hashes: {} };
11801
11805
  return {
11802
11806
  version: 1,
@@ -11808,18 +11812,18 @@ async function loadCache(path48) {
11808
11812
  return { version: 1, hashes: {} };
11809
11813
  }
11810
11814
  }
11811
- async function saveCache(path48, cache) {
11815
+ async function saveCache(path49, cache) {
11812
11816
  const sorted = {};
11813
11817
  for (const key of Object.keys(cache.hashes).sort()) {
11814
11818
  const hash = cache.hashes[key];
11815
11819
  if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
11816
11820
  sorted[key] = hash;
11817
11821
  }
11818
- await mkdir5(pathModule.dirname(pathModule.resolve(path48)), { recursive: true });
11819
- 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`;
11820
11824
  try {
11821
11825
  await writeFile5(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
11822
- await rename5(tmp, path48);
11826
+ await rename5(tmp, path49);
11823
11827
  } finally {
11824
11828
  await rm5(tmp, { force: true });
11825
11829
  }
@@ -13823,15 +13827,27 @@ var NewAssetSchema = z6.object({
13823
13827
  /**
13824
13828
  * A controlled regeneration of another asset in the same manifest,
13825
13829
  * instead of a fresh generation. `inpaint` needs a mask upload the
13826
- * gallery does not offer yet, so only `image-to-image` can be created
13827
- * 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.
13828
13833
  */
13829
- revision: z6.object({
13830
- mode: z6.literal("image-to-image"),
13831
- /** Asset id of the parent this revises; must already exist. */
13832
- from: z6.string().min(1),
13833
- strength: z6.number().min(0).max(1).optional()
13834
- }).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(),
13835
13851
  /** A pose or outfit of an existing `character`/`objectPro` base or state. */
13836
13852
  state: NewStateSchema.optional(),
13837
13853
  /** A loop of an existing `character`/`objectPro` base or state. */
@@ -14356,52 +14372,165 @@ async function runEdit(args) {
14356
14372
  }
14357
14373
 
14358
14374
  // src/cli/commands/gallery.ts
14359
- import path32 from "path";
14360
- import { existsSync as existsSync23 } from "fs";
14375
+ import path33 from "path";
14376
+ import { existsSync as existsSync24 } from "fs";
14361
14377
 
14362
14378
  // src/gallery/snapshot.ts
14363
14379
  import { createHash as createHash3 } from "crypto";
14364
- import { existsSync as existsSync19 } from "fs";
14365
- import { readFile as readFile17, stat as stat2 } from "fs/promises";
14366
- 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";
14367
14383
 
14368
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";
14369
14391
  import path26 from "path";
14370
14392
  import { z as z8 } from "zod";
14371
- var Base64 = z8.string().regex(/^[A-Za-z0-9+/]*={0,2}$/).max(Math.ceil(MAX_HAND_EDIT_BYTES / 3) * 4);
14372
- var HandEditRequestSchema = z8.discriminatedUnion("action", [
14373
- z8.object({
14374
- action: z8.literal("start-edit"),
14375
- project: z8.string().min(1).optional(),
14376
- assetId: z8.string().min(1),
14377
- styleId: z8.string().min(1),
14378
- 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}$/),
14379
14508
  /** Also launch the author's editor on the file. */
14380
- open: z8.boolean().optional()
14509
+ open: z9.boolean().optional()
14381
14510
  }).strict(),
14382
- z8.object({
14383
- action: z8.literal("detach-edit"),
14384
- project: z8.string().min(1).optional(),
14385
- assetId: z8.string().min(1),
14386
- styleId: z8.string().min(1),
14387
- 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}$/)
14388
14517
  }).strict(),
14389
14518
  /** From the in-browser editor: the page holds the bytes, the server validates and writes. */
14390
- z8.object({
14391
- action: z8.literal("save-edit"),
14392
- project: z8.string().min(1).optional(),
14393
- assetId: z8.string().min(1),
14394
- styleId: z8.string().min(1),
14395
- 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}$/),
14396
14525
  /** The flattened image; for a frame set, `frames` instead. */
14397
14526
  png: Base64.min(1).optional(),
14398
14527
  /** Every frame of a set, tagged with the role it was opened under (null for one added in the editor). */
14399
- 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(),
14400
14529
  /** The editor's layered project file, kept beside the edit. */
14401
14530
  pxo: Base64.optional(),
14402
14531
  /** Editor identity from the bridge's ready message. */
14403
- editor: z8.string().min(1).max(200),
14404
- protocol: z8.number().int().positive()
14532
+ editor: z9.string().min(1).max(200),
14533
+ protocol: z9.number().int().positive()
14405
14534
  }).strict()
14406
14535
  ]);
14407
14536
  function createGalleryEditHandler(opts) {
@@ -14442,6 +14571,23 @@ function createGalleryEditHandler(opts) {
14442
14571
  }
14443
14572
  return opts.reload();
14444
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
+ }
14445
14591
  const parsed = ManifestEditSchema.safeParse(body);
14446
14592
  if (!parsed.success) {
14447
14593
  throw new ManifestEditError(
@@ -14456,7 +14602,7 @@ function createGalleryEditHandler(opts) {
14456
14602
  }
14457
14603
  const result = await applyManifestEdit(manifestPath, parsed.data);
14458
14604
  if (result.changed) {
14459
- const relative = path26.relative(process.cwd(), result.manifestPath);
14605
+ const relative = path27.relative(process.cwd(), result.manifestPath);
14460
14606
  const shown = !relative || relative.startsWith("..") ? result.manifestPath : relative;
14461
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;
14462
14608
  log2(` manifest edited: ${parsed.data.action === "add-asset" ? "added" : "changed"} ${subject} in ${shown}`);
@@ -14466,8 +14612,8 @@ function createGalleryEditHandler(opts) {
14466
14612
  }
14467
14613
 
14468
14614
  // src/pipeline/history.ts
14469
- import { existsSync as existsSync18 } from "fs";
14470
- import path27 from "path";
14615
+ import { existsSync as existsSync19 } from "fs";
14616
+ import path28 from "path";
14471
14617
  var HISTORY_ENV = "PIXELKILN_HISTORY";
14472
14618
  function historyLimit(manifest, env = process.env) {
14473
14619
  if (manifest?.history !== void 0) return manifest.history;
@@ -14554,10 +14700,10 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
14554
14700
  }
14555
14701
  for (let i = 0; i < entry.outputs.length; i++) {
14556
14702
  const file = currentEntryOutputPath(entry, spec, i);
14557
- if (!existsSync18(file)) continue;
14703
+ if (!existsSync19(file)) continue;
14558
14704
  if (await sha256File(file) !== entry.outputs[i].sha256 && !opts.force) {
14559
14705
  throw new OverwriteRefusedError(
14560
- `${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`,
14561
14707
  [file]
14562
14708
  );
14563
14709
  }
@@ -14610,7 +14756,7 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
14610
14756
  // src/gallery/snapshot.ts
14611
14757
  var PROJECT_FILE_TYPE = "application/octet-stream";
14612
14758
  function galleryMediaId(absolutePath) {
14613
- 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);
14614
14760
  }
14615
14761
  function galleryMediaRoute(id) {
14616
14762
  return `/media/${id}`;
@@ -14629,9 +14775,9 @@ function describeHistory(media, entry, cacheDir) {
14629
14775
  return (entry.history ?? []).map((generation, i) => {
14630
14776
  const outputs = generation.outputs.map((output) => {
14631
14777
  const mediaType = output.mediaType ?? MediaType.PNG;
14632
- const file = path28.join(cacheDir, cacheFileName(output.sha256, mediaType));
14778
+ const file = path29.join(cacheDir, cacheFileName(output.sha256, mediaType));
14633
14779
  let url = null;
14634
- if (existsSync19(file)) {
14780
+ if (existsSync20(file)) {
14635
14781
  const id = galleryMediaId(file);
14636
14782
  media.set(id, { path: file, contentType: mediaType });
14637
14783
  url = `${galleryMediaRoute(id)}?v=${output.sha256.slice(0, 24)}`;
@@ -14663,7 +14809,7 @@ function describeHistory(media, entry, cacheDir) {
14663
14809
  var MAX_COMPARED_BYTES = 8 * 1024 * 1024;
14664
14810
  async function samePixels(a, b) {
14665
14811
  try {
14666
- const [left, right] = await Promise.all([readFile17(a), readFile17(b)]);
14812
+ const [left, right] = await Promise.all([readFile18(a), readFile18(b)]);
14667
14813
  if (left.length > MAX_COMPARED_BYTES || right.length > MAX_COMPARED_BYTES) return false;
14668
14814
  const x = decodePng(left);
14669
14815
  const y = decodePng(right);
@@ -14761,10 +14907,10 @@ async function describeOutput(media, root, absolutePath, recorded = {}) {
14761
14907
  async function describeQuality(media, root, inspection) {
14762
14908
  const outputPaths = inspection.outputs ?? [inspection.output];
14763
14909
  const outputs = await Promise.all(
14764
- outputPaths.map((file) => describeOutput(media, root, path28.resolve(root, file)))
14910
+ outputPaths.map((file) => describeOutput(media, root, path29.resolve(root, file)))
14765
14911
  );
14766
- const recordPath = path28.resolve(root, inspection.record);
14767
- const recordExists = existsSync19(recordPath);
14912
+ const recordPath = path29.resolve(root, inspection.record);
14913
+ const recordExists = existsSync20(recordPath);
14768
14914
  let options = null;
14769
14915
  let check = null;
14770
14916
  if (recordExists) {
@@ -14783,7 +14929,7 @@ async function describeQuality(media, root, inspection) {
14783
14929
  return {
14784
14930
  state: inspection.state,
14785
14931
  reason: inspection.reason,
14786
- output: portableOutputPath(path28.resolve(root, inspection.output), root),
14932
+ output: portableOutputPath(path29.resolve(root, inspection.output), root),
14787
14933
  record: portableOutputPath(recordPath, root),
14788
14934
  recordExists,
14789
14935
  outputs,
@@ -14821,9 +14967,9 @@ async function buildGallerySnapshot(opts) {
14821
14967
  const media = /* @__PURE__ */ new Map();
14822
14968
  const plan = await buildPlan(specs, lock);
14823
14969
  const items = [];
14824
- const manifestText = await readFile17(loaded.path, "utf8");
14970
+ const manifestText = await readFile18(loaded.path, "utf8");
14825
14971
  const manifestSha256 = sha256(manifestText);
14826
- const cacheDir = path28.resolve(path28.dirname(opts.lockPath), ".pixelkiln", "cache");
14972
+ const cacheDir = path29.resolve(path29.dirname(opts.lockPath), ".pixelkiln", "cache");
14827
14973
  let rawStyles = {};
14828
14974
  try {
14829
14975
  const raw = JSON.parse(manifestText);
@@ -14847,12 +14993,12 @@ async function buildGallerySnapshot(opts) {
14847
14993
  )
14848
14994
  );
14849
14995
  if (spec.source) {
14850
- const editPath = path28.resolve(root, spec.source);
14996
+ const editPath = path29.resolve(root, spec.source);
14851
14997
  const memberCount = sourceIsStem(spec.source, entry, root) ? entry.outputs.length : 1;
14852
14998
  const editShas = await Promise.all(
14853
14999
  Array.from({ length: memberCount }, async (_, index) => {
14854
15000
  const file = sourceOutputPath(spec.source, entry, index, root);
14855
- return { file, sha256: existsSync19(file) ? await sha256File(file) : null };
15001
+ return { file, sha256: existsSync20(file) ? await sha256File(file) : null };
14856
15002
  })
14857
15003
  );
14858
15004
  edits = await Promise.all(editShas.map(({ file, sha256: editSha }) => describeOutput(media, root, file, { sha256: editSha })));
@@ -14860,7 +15006,7 @@ async function buildGallerySnapshot(opts) {
14860
15006
  const companion = await readHandEditCompanion(editPath);
14861
15007
  if (companion) {
14862
15008
  const projectPath = handEditProjectPath(editPath);
14863
- const project = companion.project && existsSync19(projectPath) ? await fileInfo(projectPath) : null;
15009
+ const project = companion.project && existsSync20(projectPath) ? await fileInfo(projectPath) : null;
14864
15010
  let projectUrl = null;
14865
15011
  if (project) {
14866
15012
  const id = galleryMediaId(projectPath);
@@ -14896,8 +15042,8 @@ async function buildGallerySnapshot(opts) {
14896
15042
  editStatus = edits.some((member) => !member.exists) ? "missing" : editChanged.every((changed) => !changed) ? "same" : regenerated ? "regenerated-since" : "edited";
14897
15043
  }
14898
15044
  } else if (spec.source) {
14899
- outputs = [await describeOutput(media, root, path28.resolve(root, spec.source))];
14900
- } else if (!entry && existsSync19(spec.outFile)) {
15045
+ outputs = [await describeOutput(media, root, path29.resolve(root, spec.source))];
15046
+ } else if (!entry && existsSync20(spec.outFile)) {
14901
15047
  outputs = [await describeOutput(media, root, spec.outFile)];
14902
15048
  } else {
14903
15049
  outputs = [];
@@ -14938,6 +15084,10 @@ async function buildGallerySnapshot(opts) {
14938
15084
  revision: entry?.revision ?? null,
14939
15085
  revisionParentKey: spec.revision ? lockKey(spec.styleId, spec.revision.sourceAssetId) : entry?.revision ? lockKey(spec.styleId, entry.revision.sourceAssetId) : null,
14940
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,
14941
15091
  character: describeCharacter(spec, entry),
14942
15092
  outputs,
14943
15093
  quality,
@@ -15002,6 +15152,7 @@ async function buildGallerySnapshot(opts) {
15002
15152
  revision: entry.revision,
15003
15153
  revisionParentKey: entry.revision ? lockKey(entry.styleId, entry.revision.sourceAssetId) : null,
15004
15154
  mirrorOfKey: entry.mirror ? lockKey(entry.styleId, entry.mirror.sourceAssetId) : null,
15155
+ skeleton: null,
15005
15156
  character: describeCharacter(void 0, entry),
15006
15157
  outputs,
15007
15158
  quality: null,
@@ -15092,7 +15243,7 @@ async function buildGallerySnapshot(opts) {
15092
15243
  id: loaded.manifest.name,
15093
15244
  name: loaded.manifest.name,
15094
15245
  manifest: loaded.path,
15095
- lock: path28.resolve(opts.lockPath),
15246
+ lock: path29.resolve(opts.lockPath),
15096
15247
  root,
15097
15248
  provider: loaded.manifest.provider,
15098
15249
  account: null,
@@ -15118,7 +15269,7 @@ async function buildGallerySnapshot(opts) {
15118
15269
  return { snapshot, media };
15119
15270
  }
15120
15271
  async function buildWorkspaceGallerySnapshot(opts) {
15121
- const dir = path28.dirname(path28.resolve(opts.workspacePath));
15272
+ const dir = path29.dirname(path29.resolve(opts.workspacePath));
15122
15273
  const media = /* @__PURE__ */ new Map();
15123
15274
  const projects = [];
15124
15275
  const items = [];
@@ -15161,11 +15312,11 @@ async function buildWorkspaceGallerySnapshot(opts) {
15161
15312
  id: project.id,
15162
15313
  name: loaded.manifest.name,
15163
15314
  manifest: manifestPath,
15164
- lock: path28.resolve(lockPath),
15315
+ lock: path29.resolve(lockPath),
15165
15316
  root: loaded.root,
15166
15317
  provider: loaded.manifest.provider,
15167
15318
  account: project.account ?? null,
15168
- manifestSha256: manifestSha256 ?? sha256(await readFile17(manifestPath, "utf8")),
15319
+ manifestSha256: manifestSha256 ?? sha256(await readFile18(manifestPath, "utf8")),
15169
15320
  historyLimit: historyLimit(loaded.manifest),
15170
15321
  entries: Object.keys(lock.entries).length,
15171
15322
  items: projectItems.length,
@@ -15177,8 +15328,8 @@ async function buildWorkspaceGallerySnapshot(opts) {
15177
15328
  id: project.id,
15178
15329
  name: project.id,
15179
15330
  manifest: manifestPath,
15180
- lock: path28.resolve(lockPath),
15181
- root: path28.dirname(manifestPath),
15331
+ lock: path29.resolve(lockPath),
15332
+ root: path29.dirname(manifestPath),
15182
15333
  provider: project.provider,
15183
15334
  account: project.account ?? null,
15184
15335
  manifestSha256: null,
@@ -15205,7 +15356,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
15205
15356
  version: 1,
15206
15357
  generatedAt: (opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
15207
15358
  project: null,
15208
- workspace: { path: path28.resolve(opts.workspacePath), dir, projects },
15359
+ workspace: { path: path29.resolve(opts.workspacePath), dir, projects },
15209
15360
  filter,
15210
15361
  totals: {
15211
15362
  entries: projects.reduce((sum, project) => sum + project.entries, 0),
@@ -15224,7 +15375,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
15224
15375
  // src/gallery/server.ts
15225
15376
  import { randomBytes } from "crypto";
15226
15377
  import { createServer as createServer2 } from "http";
15227
- import { readFile as readFile18 } from "fs/promises";
15378
+ import { readFile as readFile19 } from "fs/promises";
15228
15379
 
15229
15380
  // src/gallery/page.ts
15230
15381
  import { readFileSync as readFileSync4 } from "fs";
@@ -15334,7 +15485,7 @@ async function readJsonBody(req) {
15334
15485
  async function serveGallery(opts) {
15335
15486
  const log2 = opts.onProgress ?? (() => {
15336
15487
  });
15337
- 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;
15338
15489
  let media = /* @__PURE__ */ new Map();
15339
15490
  let loading = null;
15340
15491
  const reviewAssets = /* @__PURE__ */ new Map();
@@ -15409,6 +15560,17 @@ async function serveGallery(opts) {
15409
15560
  }
15410
15561
  return;
15411
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
+ }
15412
15574
  if (req.method === "POST" && url2.pathname === "/api/editor/install") {
15413
15575
  if (!opts.editor || !session) return fail(405, "the in-browser editor is off for this gallery");
15414
15576
  try {
@@ -15465,7 +15627,7 @@ async function serveGallery(opts) {
15465
15627
  const asset = reviewAssets.get(jobId)?.get(url2.pathname);
15466
15628
  if (!asset) return fail(404, "no such review asset");
15467
15629
  try {
15468
- const bytes = await readFile18(asset.path);
15630
+ const bytes = await readFile19(asset.path);
15469
15631
  res.writeHead(200, {
15470
15632
  "Content-Type": asset.contentType,
15471
15633
  "Cache-Control": "no-store",
@@ -15541,7 +15703,7 @@ async function serveGallery(opts) {
15541
15703
  if (!asset) return fail(404, "no such gallery media");
15542
15704
  let bytes;
15543
15705
  try {
15544
- bytes = await readFile18(asset.path);
15706
+ bytes = await readFile19(asset.path);
15545
15707
  } catch {
15546
15708
  return fail(404, "gallery media is no longer on disk; refresh the page");
15547
15709
  }
@@ -15585,7 +15747,7 @@ async function serveGallery(opts) {
15585
15747
 
15586
15748
  // src/gallery/generate.ts
15587
15749
  import { randomBytes as randomBytes2 } from "crypto";
15588
- import { z as z9 } from "zod";
15750
+ import { z as z10 } from "zod";
15589
15751
 
15590
15752
  // src/pipeline/poll.ts
15591
15753
  var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -15996,13 +16158,13 @@ function ownedReferenceImageId(spec, lock, providerId) {
15996
16158
  }
15997
16159
 
15998
16160
  // src/pick/server.ts
15999
- import { existsSync as existsSync21 } from "fs";
16000
- import { readFile as readFile20 } from "fs/promises";
16161
+ import { existsSync as existsSync22 } from "fs";
16162
+ import { readFile as readFile21 } from "fs/promises";
16001
16163
 
16002
16164
  // src/pipeline/init.ts
16003
- import { readdir, readFile as readFile19, writeFile as writeFile12 } from "fs/promises";
16004
- import { existsSync as existsSync20 } from "fs";
16005
- 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";
16006
16168
  function pngSize(buf) {
16007
16169
  if (buf.length < 24) return null;
16008
16170
  const isPng = buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834;
@@ -16016,7 +16178,7 @@ function slugify(value) {
16016
16178
  async function walk(dir, exclude) {
16017
16179
  const out = [];
16018
16180
  for (const entry of await readdir(dir, { withFileTypes: true })) {
16019
- const full = path29.join(dir, entry.name);
16181
+ const full = path30.join(dir, entry.name);
16020
16182
  if (exclude.some((x) => full.includes(x))) continue;
16021
16183
  if (entry.isDirectory()) out.push(...await walk(full, exclude));
16022
16184
  else if (entry.name.toLowerCase().endsWith(".png")) out.push(full);
@@ -16030,14 +16192,14 @@ async function scanAssets(root, opts = {}) {
16030
16192
  const skipped = [];
16031
16193
  const seen = /* @__PURE__ */ new Map();
16032
16194
  for (const file of files.sort()) {
16033
- const size = pngSize(await readFile19(file));
16195
+ const size = pngSize(await readFile20(file));
16034
16196
  if (!size) {
16035
16197
  skipped.push(`${file} (not a readable PNG)`);
16036
16198
  continue;
16037
16199
  }
16038
- const rel = path29.relative(root, file);
16039
- const category = path29.dirname(rel) === "." ? "" : path29.dirname(rel);
16040
- 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));
16041
16203
  if (seen.has(id)) {
16042
16204
  const n = seen.get(id) + 1;
16043
16205
  seen.set(id, n);
@@ -16095,10 +16257,10 @@ function mode(values) {
16095
16257
  return [...counts.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? 64;
16096
16258
  }
16097
16259
  async function writeManifestFile(target, manifest) {
16098
- if (existsSync20(target)) {
16260
+ if (existsSync21(target)) {
16099
16261
  throw new Error(`${target} already exists; refusing to overwrite an existing manifest.`);
16100
16262
  }
16101
- await writeFile12(target, JSON.stringify(manifest, null, 2) + "\n");
16263
+ await writeFile13(target, JSON.stringify(manifest, null, 2) + "\n");
16102
16264
  }
16103
16265
 
16104
16266
  // src/pick/sheet.ts
@@ -16511,13 +16673,13 @@ async function currentArt(entry, spec) {
16511
16673
  const previous = primaryOutput({ ...entry, outputs: entry.supersededOutputs });
16512
16674
  if (!previous) return null;
16513
16675
  const file = resolveOutputPath(previous.path, spec.root);
16514
- if (!existsSync21(file)) return null;
16676
+ if (!existsSync22(file)) return null;
16515
16677
  const contentType = previous.mediaType ?? mediaTypeFromExtension(file);
16516
16678
  if (!contentType) return null;
16517
16679
  let width = entry.width;
16518
16680
  let height = entry.height;
16519
16681
  if (contentType === "image/png") {
16520
- const size = pngSize(await readFile20(file));
16682
+ const size = pngSize(await readFile21(file));
16521
16683
  if (size) ({ width, height } = size);
16522
16684
  }
16523
16685
  return { path: file, contentType, width, height };
@@ -16685,26 +16847,26 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
16685
16847
  }
16686
16848
 
16687
16849
  // src/gallery/generate.ts
16688
- var GenerateRequestSchema = z9.object({
16689
- project: z9.string().min(1).optional(),
16690
- 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),
16691
16853
  /**
16692
16854
  * Regenerate work that is up to date, as `gen --force` does, and let the
16693
16855
  * fetch replace a locally modified or untracked file. With `restore`, put
16694
16856
  * the recorded bytes back over a modified file.
16695
16857
  */
16696
- force: z9.boolean().optional(),
16858
+ force: z10.boolean().optional(),
16697
16859
  /** Advance in-flight, review, or selected work without submitting anything. */
16698
- resume: z9.boolean().optional(),
16860
+ resume: z10.boolean().optional(),
16699
16861
  /** Re-download downloaded outputs and replace files whose object changed upstream. */
16700
- refresh: z9.boolean().optional(),
16862
+ refresh: z10.boolean().optional(),
16701
16863
  /** Put recorded outputs back on disk from the cache or the provider, without generating. */
16702
- restore: z9.boolean().optional(),
16864
+ restore: z10.boolean().optional(),
16703
16865
  /**
16704
16866
  * Bring a replaced generation back: 1-based position in the record's
16705
16867
  * history (newest first) or an output hash prefix. One key at a time.
16706
16868
  */
16707
- revert: z9.string().min(1).max(64).optional()
16869
+ revert: z10.string().min(1).max(64).optional()
16708
16870
  }).strict().refine((request) => [request.resume, request.refresh, request.restore, request.revert !== void 0].filter(Boolean).length <= 1, {
16709
16871
  message: "resume, refresh, restore, and revert are exclusive"
16710
16872
  }).refine((request) => request.revert === void 0 || request.keys.length === 1, { message: "revert takes one key" });
@@ -17014,15 +17176,15 @@ function createGenerateHandlers(opts) {
17014
17176
  }
17015
17177
 
17016
17178
  // src/gallery/editor.ts
17017
- import { readFile as readFile22 } from "fs/promises";
17018
- import path31 from "path";
17179
+ import { readFile as readFile23 } from "fs/promises";
17180
+ import path32 from "path";
17019
17181
 
17020
17182
  // src/editor/install.ts
17021
17183
  import { createHash as createHash4 } from "crypto";
17022
- import { existsSync as existsSync22 } from "fs";
17023
- 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";
17024
17186
  import os2 from "os";
17025
- import path30 from "path";
17187
+ import path31 from "path";
17026
17188
 
17027
17189
  // tools/pixelorama-bridge/pin.json
17028
17190
  var pin_default = {
@@ -17086,15 +17248,15 @@ var EditorInstallError = class extends Error {
17086
17248
  };
17087
17249
  function editorToolsRoot() {
17088
17250
  const override = process.env.PIXELKILN_TOOLS_DIR?.trim();
17089
- if (override) return path30.resolve(override);
17090
- 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");
17091
17253
  if (process.platform === "win32") {
17092
- 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");
17093
17255
  }
17094
- 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");
17095
17257
  }
17096
17258
  function editorDir(pin = EDITOR_PIN, root = editorToolsRoot()) {
17097
- return path30.join(root, "pixelorama", pin.release ?? "unpublished");
17259
+ return path31.join(root, "pixelorama", pin.release ?? "unpublished");
17098
17260
  }
17099
17261
  function editorBaseUrl(pin = EDITOR_PIN) {
17100
17262
  const override = process.env.PIXELKILN_EDITOR_URL?.trim();
@@ -17104,7 +17266,7 @@ function editorBaseUrl(pin = EDITOR_PIN) {
17104
17266
  }
17105
17267
  async function sha256Of(file) {
17106
17268
  try {
17107
- return createHash4("sha256").update(await readFile21(file)).digest("hex");
17269
+ return createHash4("sha256").update(await readFile22(file)).digest("hex");
17108
17270
  } catch {
17109
17271
  return null;
17110
17272
  }
@@ -17117,8 +17279,8 @@ async function editorStatus(opts = {}) {
17117
17279
  let totalBytes = 0;
17118
17280
  for (const [name, expected] of Object.entries(pin.files)) {
17119
17281
  totalBytes += expected.bytes;
17120
- const file = path30.join(dir, name);
17121
- 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;
17122
17284
  if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
17123
17285
  installedBytes += expected.bytes;
17124
17286
  } else {
@@ -17147,7 +17309,7 @@ async function installEditor(opts = {}) {
17147
17309
  );
17148
17310
  }
17149
17311
  if (!baseUrl) throw new EditorInstallError("no download location for the editor build");
17150
- await mkdir10(dir, { recursive: true });
17312
+ await mkdir11(dir, { recursive: true });
17151
17313
  const downloaded = [];
17152
17314
  const skipped = Object.keys(pin.files).filter((name) => !before.missing.includes(name));
17153
17315
  let fetchedBytes = before.installedBytes;
@@ -17175,13 +17337,13 @@ async function installEditor(opts = {}) {
17175
17337
  if (digest2 !== expected.sha256) {
17176
17338
  throw new EditorInstallError(`${name}: sha256 ${digest2.slice(0, 12)}\u2026 does not match the pinned ${expected.sha256.slice(0, 12)}\u2026`, name);
17177
17339
  }
17178
- const final = path30.join(dir, name);
17340
+ const final = path31.join(dir, name);
17179
17341
  const tmp = `${final}.${process.pid}.part`;
17180
17342
  try {
17181
- await writeFile13(tmp, bytes);
17343
+ await writeFile14(tmp, bytes);
17182
17344
  await rename9(tmp, final);
17183
17345
  } finally {
17184
- await rm9(tmp, { force: true });
17346
+ await rm10(tmp, { force: true });
17185
17347
  }
17186
17348
  fetchedBytes += bytes.length;
17187
17349
  downloaded.push(name);
@@ -17221,7 +17383,7 @@ var EDITOR_CONTENT_TYPES = {
17221
17383
  ".json": "application/json; charset=utf-8"
17222
17384
  };
17223
17385
  function editorContentType(name) {
17224
- return EDITOR_CONTENT_TYPES[path30.extname(name).toLowerCase()] ?? null;
17386
+ return EDITOR_CONTENT_TYPES[path31.extname(name).toLowerCase()] ?? null;
17225
17387
  }
17226
17388
 
17227
17389
  // src/gallery/editor.ts
@@ -17297,7 +17459,7 @@ function createGalleryEditorHandlers(opts = {}) {
17297
17459
  if (!status.installed) return null;
17298
17460
  let bytes;
17299
17461
  try {
17300
- bytes = await readFile22(path31.join(dir, name));
17462
+ bytes = await readFile23(path32.join(dir, name));
17301
17463
  } catch {
17302
17464
  verified = null;
17303
17465
  return null;
@@ -17337,13 +17499,13 @@ async function serveUntilStopped(initial, reload, args, access2) {
17337
17499
  };
17338
17500
  const loadProject = async (project) => {
17339
17501
  const ctx = await access2.loadProject(project);
17340
- const dir = path32.dirname(ctx.loaded.path);
17502
+ const dir = path33.dirname(ctx.loaded.path);
17341
17503
  const declared = readEnvFiles(dir);
17342
17504
  const credentialNames = new Set(availableProviders().flatMap((id) => providerCredentialEnvs(providerFactory(id))));
17343
17505
  for (const [name, value] of Object.entries(declared)) {
17344
17506
  if (credentialNames.has(name) && name in process.env && process.env[name] !== value) {
17345
17507
  throw new Error(
17346
- `${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.`
17347
17509
  );
17348
17510
  }
17349
17511
  }
@@ -17371,6 +17533,8 @@ async function serveUntilStopped(initial, reload, args, access2) {
17371
17533
  ),
17372
17534
  ...args.edit ? { edit: createGalleryEditHandler({ manifestFor: access2.manifestFor, loadProject: access2.loadProject, reload, onProgress: log }) } : {},
17373
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 }) } : {},
17374
17538
  // The editor exists to write hand edits back, so it follows the write gate.
17375
17539
  ...args.edit && !args.noEditor ? { editor: createGalleryEditorHandlers({ onProgress: log }) } : {}
17376
17540
  });
@@ -17388,8 +17552,8 @@ async function serveUntilStopped(initial, reload, args, access2) {
17388
17552
  }
17389
17553
  async function runGallery(args) {
17390
17554
  if (args.workspace) {
17391
- const workspacePath = path32.resolve(args.workspace);
17392
- if (!existsSync23(workspacePath)) {
17555
+ const workspacePath = path33.resolve(args.workspace);
17556
+ if (!existsSync24(workspacePath)) {
17393
17557
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
17394
17558
  }
17395
17559
  const filter2 = { styles: args.styles, assets: args.assets };
@@ -17409,7 +17573,7 @@ async function runGallery(args) {
17409
17573
  const registered = async (projectId) => {
17410
17574
  const project = (await loadWorkspace(workspacePath)).projects.find((candidate) => candidate.id === projectId);
17411
17575
  if (!projectId || !project) throw new Error(`unknown workspace project "${projectId ?? ""}"`);
17412
- return resolveProject(path32.dirname(workspacePath), project);
17576
+ return resolveProject(path33.dirname(workspacePath), project);
17413
17577
  };
17414
17578
  await serveUntilStopped(initial, build2, args, {
17415
17579
  manifestFor: async (projectId) => (await registered(projectId)).manifestPath,
@@ -17434,7 +17598,7 @@ async function runGallery(args) {
17434
17598
  return buildGallerySnapshot({ ...fresh, filter });
17435
17599
  };
17436
17600
  await serveUntilStopped(await build(), reload, args, {
17437
- manifestFor: () => path32.resolve(args.manifest),
17601
+ manifestFor: () => path33.resolve(args.manifest),
17438
17602
  loadProject: () => openProject(args.manifest, { lockPath: args.lock, env: false })
17439
17603
  });
17440
17604
  }
@@ -17743,18 +17907,18 @@ async function runRestoreGeneration(args) {
17743
17907
  }
17744
17908
 
17745
17909
  // src/cli/commands/housekeeping.ts
17746
- import { existsSync as existsSync25 } from "fs";
17910
+ import { existsSync as existsSync26 } from "fs";
17747
17911
 
17748
17912
  // src/pipeline/cache-health.ts
17749
- import { existsSync as existsSync24 } from "fs";
17750
- import { readFile as readFile23, readdir as readdir2, rm as rm10 } from "fs/promises";
17751
- 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";
17752
17916
  async function inspectCaches(lock, lockPath, options = {}) {
17753
- if (options.prune && !existsSync24(lockPath)) {
17754
- 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)}`);
17755
17919
  }
17756
- const contentDir = path33.resolve(path33.dirname(lockPath), ".pixelkiln", "cache");
17757
- const remotePath = path33.resolve(cachePathFor(lockPath));
17920
+ const contentDir = path34.resolve(path34.dirname(lockPath), ".pixelkiln", "cache");
17921
+ const remotePath = path34.resolve(cachePathFor(lockPath));
17758
17922
  const referenced = referencedHashes(lock);
17759
17923
  let content = await inspectContentCache(contentDir, referenced);
17760
17924
  let remoteHashes = await inspectRemoteHashCache(remotePath);
@@ -17766,7 +17930,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
17766
17930
  ]);
17767
17931
  for (const name of names) {
17768
17932
  try {
17769
- await rm10(path33.join(contentDir, name), { force: true });
17933
+ await rm11(path34.join(contentDir, name), { force: true });
17770
17934
  removed.contentFiles++;
17771
17935
  } catch {
17772
17936
  }
@@ -17775,7 +17939,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
17775
17939
  await saveCache(remotePath, { version: 1, hashes: {} });
17776
17940
  removed.resetRemoteHashCache = true;
17777
17941
  } else if (remoteHashes.invalidIds.length) {
17778
- const cache = parseCache(JSON.parse(await readFile23(remotePath, "utf8")));
17942
+ const cache = parseCache(JSON.parse(await readFile24(remotePath, "utf8")));
17779
17943
  for (const id of remoteHashes.invalidIds) delete cache.hashes[id];
17780
17944
  removed.remoteHashEntries = remoteHashes.invalidIds.length;
17781
17945
  await saveCache(remotePath, cache);
@@ -17793,7 +17957,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
17793
17957
  async function inspectContentCache(contentDir, referenced) {
17794
17958
  const report = {
17795
17959
  path: contentDir,
17796
- exists: existsSync24(contentDir),
17960
+ exists: existsSync25(contentDir),
17797
17961
  files: 0,
17798
17962
  bytes: 0,
17799
17963
  valid: 0,
@@ -17814,12 +17978,12 @@ async function inspectContentCache(contentDir, referenced) {
17814
17978
  continue;
17815
17979
  }
17816
17980
  report.files++;
17817
- const file = path33.join(contentDir, entry.name);
17981
+ const file = path34.join(contentDir, entry.name);
17818
17982
  const mediaType = mediaTypeFromExtension(entry.name);
17819
17983
  const expected = mediaType ? entry.name.slice(0, -4) : "";
17820
17984
  let bytes;
17821
17985
  try {
17822
- bytes = await readFile23(file);
17986
+ bytes = await readFile24(file);
17823
17987
  report.bytes += bytes.length;
17824
17988
  } catch (err) {
17825
17989
  report.invalid.push({
@@ -17857,7 +18021,7 @@ async function inspectContentCache(contentDir, referenced) {
17857
18021
  async function inspectRemoteHashCache(remotePath) {
17858
18022
  const report = {
17859
18023
  path: remotePath,
17860
- exists: existsSync24(remotePath),
18024
+ exists: existsSync25(remotePath),
17861
18025
  entries: 0,
17862
18026
  valid: 0,
17863
18027
  invalidIds: [],
@@ -17866,7 +18030,7 @@ async function inspectRemoteHashCache(remotePath) {
17866
18030
  if (!report.exists) return report;
17867
18031
  let cache;
17868
18032
  try {
17869
- cache = parseCache(JSON.parse(await readFile23(remotePath, "utf8")));
18033
+ cache = parseCache(JSON.parse(await readFile24(remotePath, "utf8")));
17870
18034
  } catch (err) {
17871
18035
  report.error = err instanceof Error ? err.message : String(err);
17872
18036
  return report;
@@ -17930,7 +18094,7 @@ async function runAccept(args) {
17930
18094
  let intact = true;
17931
18095
  for (const output of entry.outputs) {
17932
18096
  const file = resolveOutputPath(output.path, item.spec.root);
17933
- if (!existsSync25(file) || await sha256File(file) !== output.sha256) {
18097
+ if (!existsSync26(file) || await sha256File(file) !== output.sha256) {
17934
18098
  intact = false;
17935
18099
  break;
17936
18100
  }
@@ -17985,42 +18149,42 @@ This drops their provenance from the lockfile. The art on disk is`);
17985
18149
  }
17986
18150
 
17987
18151
  // src/cli/commands/init.ts
17988
- import path34 from "path";
17989
- import { existsSync as existsSync26 } from "fs";
18152
+ import path35 from "path";
18153
+ import { existsSync as existsSync27 } from "fs";
17990
18154
  async function runInit(args) {
17991
18155
  if (!args.from) throw new Error("init needs --from <dir> pointing at your existing PNGs.");
17992
- const root = path34.resolve(args.from);
17993
- 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}`);
17994
18158
  const generator = args.generator ?? "map";
17995
18159
  if (generator !== "1dir" && generator !== "map") {
17996
18160
  throw new Error(`--generator must be "1dir" or "map", got "${args.generator}".`);
17997
18161
  }
17998
- const target = path34.resolve(args.out ?? "pixelkiln.manifest.json");
18162
+ const target = path35.resolve(args.out ?? "pixelkiln.manifest.json");
17999
18163
  const { assets, skipped } = await scanAssets(root, { exclude: args.exclude });
18000
18164
  if (!assets.length) throw new Error(`No PNGs found under ${root}`);
18001
18165
  const manifest = buildManifest(
18002
- args.name ?? path34.basename(path34.dirname(target)),
18166
+ args.name ?? path35.basename(path35.dirname(target)),
18003
18167
  args.styles[0] ?? "base",
18004
18168
  generator,
18005
- path34.relative(path34.dirname(target), root) || ".",
18169
+ path35.relative(path35.dirname(target), root) || ".",
18006
18170
  assets
18007
18171
  );
18008
18172
  await writeManifestFile(target, manifest);
18009
- 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)}`);
18010
18174
  if (skipped.length) log(` skipped ${skipped.length} unreadable file(s)`);
18011
- log(` wrote ${path34.relative(process.cwd(), target)}`);
18175
+ log(` wrote ${path35.relative(process.cwd(), target)}`);
18012
18176
  log(`
18013
18177
  Prompts are intentionally empty. To recover the real ones from your`);
18014
18178
  log(` PixelLab account instead of inventing them:`);
18015
18179
  log(`
18016
- pixelkiln adopt --manifest ${path34.relative(process.cwd(), target)} --write-prompts
18180
+ pixelkiln adopt --manifest ${path35.relative(process.cwd(), target)} --write-prompts
18017
18181
  `);
18018
18182
  }
18019
18183
 
18020
18184
  // src/pipeline/doctor.ts
18021
- import { constants, existsSync as existsSync27 } from "fs";
18185
+ import { constants, existsSync as existsSync28 } from "fs";
18022
18186
  import { access, stat as stat4 } from "fs/promises";
18023
- import path35 from "path";
18187
+ import path36 from "path";
18024
18188
  async function doctor(loaded, specs, lock, lockPath, opts = {}) {
18025
18189
  const checks = [];
18026
18190
  const add = (id, level, message6) => checks.push({ id, level, message: message6 });
@@ -18031,8 +18195,8 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
18031
18195
  );
18032
18196
  add("lock", "ok", `${Object.keys(lock.entries).length} valid v2 lock entr(ies)`);
18033
18197
  const dirs = [...new Set(specs.flatMap((spec) => [
18034
- path35.dirname(spec.outFile),
18035
- ...spec.quality ? [path35.dirname(spec.quality.outFile)] : []
18198
+ path36.dirname(spec.outFile),
18199
+ ...spec.quality ? [path36.dirname(spec.quality.outFile)] : []
18036
18200
  ]))];
18037
18201
  const unwritable = [];
18038
18202
  for (const dir of dirs) {
@@ -18055,7 +18219,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
18055
18219
  for (let index = 0; index < entry.outputs.length; index++) {
18056
18220
  const output = entry.outputs[index];
18057
18221
  const spec = specByKey.get(key);
18058
- 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));
18059
18223
  const owner = outputOwners.get(absolute);
18060
18224
  if (owner && owner !== key) duplicateOutputs.push(`${output.path} (${owner}, ${key})`);
18061
18225
  outputOwners.set(absolute, key);
@@ -18070,7 +18234,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
18070
18234
  } else {
18071
18235
  add("lock-outputs", "ok", `${outputOwners.size} recorded output path(s) are uniquely owned`);
18072
18236
  }
18073
- const cacheDir = path35.join(path35.dirname(lockPath), ".pixelkiln", "cache");
18237
+ const cacheDir = path36.join(path36.dirname(lockPath), ".pixelkiln", "cache");
18074
18238
  const stranded = [];
18075
18239
  const invalidState = [];
18076
18240
  const staleMapUrls = [];
@@ -18086,11 +18250,11 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
18086
18250
  const hasSource = Boolean(entry.sourceUrl || entry.sourceUrls?.length);
18087
18251
  let hasCache = false;
18088
18252
  for (const output of entry.outputs) {
18089
- const cached = path35.join(
18253
+ const cached = path36.join(
18090
18254
  cacheDir,
18091
18255
  cacheFileName(output.sha256, output.mediaType ?? MediaType.PNG)
18092
18256
  );
18093
- if (existsSync27(cached) && await sha256File(cached) === output.sha256) {
18257
+ if (existsSync28(cached) && await sha256File(cached) === output.sha256) {
18094
18258
  hasCache = true;
18095
18259
  break;
18096
18260
  }
@@ -18186,16 +18350,16 @@ async function nearestExistingDirectory(start) {
18186
18350
  if ((await stat4(current)).isDirectory()) return current;
18187
18351
  } catch {
18188
18352
  }
18189
- const parent = path35.dirname(current);
18353
+ const parent = path36.dirname(current);
18190
18354
  if (parent === current) return current;
18191
18355
  current = parent;
18192
18356
  }
18193
18357
  }
18194
18358
 
18195
18359
  // src/pipeline/audit.ts
18196
- import { readFile as readFile24 } from "fs/promises";
18197
- import { existsSync as existsSync28 } from "fs";
18198
- import path36 from "path";
18360
+ import { readFile as readFile25 } from "fs/promises";
18361
+ import { existsSync as existsSync29 } from "fs";
18362
+ import path37 from "path";
18199
18363
  function paletteDistance(asset, reference) {
18200
18364
  if (!asset.length || !reference.length) return 0;
18201
18365
  let total = 0;
@@ -18231,12 +18395,12 @@ async function auditStyle(loaded, specs, styleId, lock) {
18231
18395
  const unreadable = [];
18232
18396
  for (const spec of mine) {
18233
18397
  for (const output of resolveSpecOutputs(spec, lock, loaded.root)) {
18234
- if (!existsSync28(output.absolutePath)) {
18398
+ if (!existsSync29(output.absolutePath)) {
18235
18399
  missing.push(output.id);
18236
18400
  continue;
18237
18401
  }
18238
18402
  try {
18239
- const png = decodePng(await readFile24(output.absolutePath));
18403
+ const png = decodePng(await readFile25(output.absolutePath));
18240
18404
  const palette = extractPalette(png, 12);
18241
18405
  assets.push({
18242
18406
  assetId: spec.assetId,
@@ -18260,10 +18424,10 @@ async function auditStyle(loaded, specs, styleId, lock) {
18260
18424
  let referenceFromStyleImages = false;
18261
18425
  const refPalettes = [];
18262
18426
  for (const img of style.styleImages) {
18263
- const abs = path36.resolve(loaded.root, img.path);
18264
- if (!existsSync28(abs)) continue;
18427
+ const abs = path37.resolve(loaded.root, img.path);
18428
+ if (!existsSync29(abs)) continue;
18265
18429
  try {
18266
- refPalettes.push(extractPalette(decodePng(await readFile24(abs)), 12));
18430
+ refPalettes.push(extractPalette(decodePng(await readFile25(abs)), 12));
18267
18431
  } catch {
18268
18432
  }
18269
18433
  }
@@ -18491,24 +18655,24 @@ async function runAudit(args) {
18491
18655
  }
18492
18656
 
18493
18657
  // src/cli/commands/pack.ts
18494
- import path38 from "path";
18495
- import { readFile as readFile25 } from "fs/promises";
18658
+ import path39 from "path";
18659
+ import { readFile as readFile26 } from "fs/promises";
18496
18660
 
18497
18661
  // src/pipeline/pack.ts
18498
18662
  import { readFileSync as readFileSync5 } from "fs";
18499
- import path37 from "path";
18663
+ import path38 from "path";
18500
18664
  var DEFAULT_FRAME_SET_FPS = 12;
18501
18665
  function resolvePackInputs(raw, inputsFilePath) {
18502
18666
  if (!Array.isArray(raw) || !raw.length) {
18503
18667
  throw new Error("--inputs must be a non-empty JSON array of { id, path }");
18504
18668
  }
18505
- const baseDir = path37.dirname(path37.resolve(inputsFilePath));
18669
+ const baseDir = path38.dirname(path38.resolve(inputsFilePath));
18506
18670
  return raw.map((entry, i) => {
18507
18671
  const e = entry;
18508
18672
  if (typeof e.id !== "string" || typeof e.path !== "string") {
18509
18673
  throw new Error(`--inputs[${i}] needs string "id" and "path"`);
18510
18674
  }
18511
- return { id: e.id, path: path37.resolve(baseDir, e.path) };
18675
+ return { id: e.id, path: path38.resolve(baseDir, e.path) };
18512
18676
  });
18513
18677
  }
18514
18678
  function packSprites(inputs, options = {}) {
@@ -18592,7 +18756,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
18592
18756
  }
18593
18757
  const inputs2 = Object.entries(options.sourceOverrides).map(([id, source]) => ({
18594
18758
  id,
18595
- path: path37.resolve(source)
18759
+ path: path38.resolve(source)
18596
18760
  }));
18597
18761
  if (!inputs2.length) throw new Error(`No approved quality outputs for style "${styleId}".`);
18598
18762
  const packed2 = packSprites(inputs2, options);
@@ -18616,7 +18780,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
18616
18780
  let outputs = resolveEntryOutputs(entry, id, manifestDir);
18617
18781
  const source = sources[id];
18618
18782
  if (source && outputs.length <= 1) {
18619
- inputs.push({ id, path: path37.resolve(manifestDir, source) });
18783
+ inputs.push({ id, path: path38.resolve(manifestDir, source) });
18620
18784
  continue;
18621
18785
  }
18622
18786
  if (source && sourceIsStem(source, entry, manifestDir)) {
@@ -18650,7 +18814,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
18650
18814
  }
18651
18815
  }
18652
18816
  for (const [id, source] of Object.entries(sources)) {
18653
- 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) });
18654
18818
  }
18655
18819
  if (!inputs.length) {
18656
18820
  throw new Error(`No readable sprites for style "${styleId}"; ${noOutput.length} skipped.`);
@@ -18758,7 +18922,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
18758
18922
  const source = sources[id];
18759
18923
  const entry = lock.entries[`${styleId}/${id}`];
18760
18924
  if (source && !(entry && sourceIsStem(source, entry, manifestDir))) {
18761
- placements.push({ id, path: path37.resolve(manifestDir, source), cell });
18925
+ placements.push({ id, path: path38.resolve(manifestDir, source), cell });
18762
18926
  continue;
18763
18927
  }
18764
18928
  if (source && entry) {
@@ -18791,7 +18955,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
18791
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}`
18792
18956
  );
18793
18957
  }
18794
- 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;
18795
18959
  const mounted = mountSprites(placements, {
18796
18960
  cellWidth: mount.cellWidth,
18797
18961
  cellHeight: mount.cellHeight,
@@ -18801,7 +18965,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
18801
18965
  ...mounted,
18802
18966
  sources: mount.base ? [{
18803
18967
  id: "$base",
18804
- path: path37.resolve(manifestDir, mount.base),
18968
+ path: path38.resolve(manifestDir, mount.base),
18805
18969
  sha256: sha256(basePng),
18806
18970
  included: true
18807
18971
  }, ...mounted.sources] : mounted.sources,
@@ -19205,12 +19369,12 @@ async function runPack(args) {
19205
19369
  if (args.primaryOnly || args.outputRoles.length) {
19206
19370
  throw new Error("--primary-only and --output-role require manifest-driven pack");
19207
19371
  }
19208
- const raw = JSON.parse(await readFile25(path38.resolve(args.inputs), "utf8"));
19372
+ const raw = JSON.parse(await readFile26(path39.resolve(args.inputs), "utf8"));
19209
19373
  const inputs = resolvePackInputs(raw, args.inputs);
19210
19374
  const { png, atlas, skipped, sources } = packSprites(inputs, { columns: args.columns });
19211
19375
  const format = sheetFormat(args);
19212
- const base = path38.resolve(args.out.replace(/\.(?:png|json|tres)$/i, ""));
19213
- 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`) });
19214
19378
  const outputs = [
19215
19379
  { path: `${base}.png`, data: png },
19216
19380
  { path: `${base}${extension}`, data: document }
@@ -19223,7 +19387,7 @@ async function runPack(args) {
19223
19387
  log(
19224
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})`
19225
19389
  );
19226
- log(` ${path38.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
19390
+ log(` ${path39.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
19227
19391
  for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
19228
19392
  return;
19229
19393
  }
@@ -19231,7 +19395,7 @@ async function runPack(args) {
19231
19395
  if (args.primaryOnly && args.outputRoles.length) {
19232
19396
  throw new Error("pack accepts either --primary-only or --output-role, not both");
19233
19397
  }
19234
- const manifestDir = path38.dirname(path38.resolve(args.manifest));
19398
+ const manifestDir = path39.dirname(path39.resolve(args.manifest));
19235
19399
  const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
19236
19400
  for (const styleId of styleIds) {
19237
19401
  const style = loaded.manifest.styles[styleId];
@@ -19246,9 +19410,9 @@ async function runPack(args) {
19246
19410
  sourceOverrides: qualitySources,
19247
19411
  sources: manifestSources(loaded.manifest, styleId)
19248
19412
  });
19249
- 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`);
19250
19414
  const format = sheetFormat(args);
19251
- const { extension, document } = renderSheetDocument(format, atlas, { imageName: path38.basename(`${base}.png`) });
19415
+ const { extension, document } = renderSheetDocument(format, atlas, { imageName: path39.basename(`${base}.png`) });
19252
19416
  const outputs = [
19253
19417
  { path: `${base}.png`, data: png },
19254
19418
  { path: `${base}${extension}`, data: document }
@@ -19280,13 +19444,13 @@ async function runPack(args) {
19280
19444
  log(
19281
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})`
19282
19446
  );
19283
- log(` ${path38.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
19447
+ log(` ${path39.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
19284
19448
  for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
19285
19449
  }
19286
19450
  }
19287
19451
  async function runMount(args) {
19288
19452
  const { loaded, lock } = await openProject2(args);
19289
- const manifestDir = path38.dirname(path38.resolve(args.manifest));
19453
+ const manifestDir = path39.dirname(path39.resolve(args.manifest));
19290
19454
  const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
19291
19455
  for (const styleId of styleIds) {
19292
19456
  const style = loaded.manifest.styles[styleId];
@@ -19325,9 +19489,9 @@ async function runMount(args) {
19325
19489
  sources,
19326
19490
  outputRoles
19327
19491
  );
19328
- const out = path38.resolve(manifestDir, style.mount.out);
19492
+ const out = path39.resolve(manifestDir, style.mount.out);
19329
19493
  const format = sheetFormat(args);
19330
- const { extension, document } = renderSheetDocument(format, atlas, { imageName: path38.basename(out) });
19494
+ const { extension, document } = renderSheetDocument(format, atlas, { imageName: path39.basename(out) });
19331
19495
  const metadata = out.replace(/\.png$/, "") + extension;
19332
19496
  const companion = out.replace(/\.png$/, "") + ".pixelkiln.json";
19333
19497
  const outputs = [
@@ -19347,7 +19511,7 @@ async function runMount(args) {
19347
19511
  await provenanceFile("$lock", args.lock),
19348
19512
  ...qualityRecords,
19349
19513
  ...artifactSources.filter(
19350
- (source) => source.id !== "$base" || path38.resolve(source.path) !== out
19514
+ (source) => source.id !== "$base" || path39.resolve(source.path) !== out
19351
19515
  )
19352
19516
  ],
19353
19517
  options: {
@@ -19361,14 +19525,14 @@ async function runMount(args) {
19361
19525
  log(
19362
19526
  ` ${styleId}: ${atlas.frames.length} cell(s) into ${atlas.sheet.width}x${atlas.sheet.height}` + (overBase ? ` over ${style.mount.base}` : " (new sheet)")
19363
19527
  );
19364
- log(` ${path38.relative(process.cwd(), out)} + atlas/provenance JSON`);
19528
+ log(` ${path39.relative(process.cwd(), out)} + atlas/provenance JSON`);
19365
19529
  for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
19366
19530
  }
19367
19531
  }
19368
19532
  async function runExport(args) {
19369
19533
  const { loaded, specs, lock } = await openProject2(args);
19370
19534
  const format = args.format ?? "generic";
19371
- const manifestDir = path38.dirname(path38.resolve(args.manifest));
19535
+ const manifestDir = path39.dirname(path39.resolve(args.manifest));
19372
19536
  const selected = specs.filter((spec) => {
19373
19537
  if (spec.generator !== "tiles" && spec.generator !== "terrain") return false;
19374
19538
  if (args.styles.length && !args.styles.includes(spec.styleId)) return false;
@@ -19384,12 +19548,12 @@ async function runExport(args) {
19384
19548
  for (const spec of selected) {
19385
19549
  const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
19386
19550
  const style = loaded.manifest.styles[spec.styleId];
19387
- const defaultBase = path38.resolve(manifestDir, style.outDir, `${spec.assetId}-tileset`);
19388
- 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;
19389
19553
  const result = exportTileset(entry, spec, {
19390
19554
  format,
19391
19555
  manifestDir,
19392
- imageName: path38.basename(`${base}.png`),
19556
+ imageName: path39.basename(`${base}.png`),
19393
19557
  columns: args.columns
19394
19558
  });
19395
19559
  const outputs = [
@@ -19407,7 +19571,7 @@ async function runExport(args) {
19407
19571
  asset: spec.assetId,
19408
19572
  columns: args.columns ?? null,
19409
19573
  format,
19410
- image: path38.basename(`${base}.png`),
19574
+ image: path39.basename(`${base}.png`),
19411
19575
  providerRules: result.generic.providerRules,
19412
19576
  style: spec.styleId,
19413
19577
  tileType: spec.tileType ?? null
@@ -19417,7 +19581,7 @@ async function runExport(args) {
19417
19581
  ` ${spec.styleId}/${spec.assetId}: ${result.generic.tiles.length} tile(s), ${result.generic.sheet.width}x${result.generic.sheet.height} (${format})`
19418
19582
  );
19419
19583
  log(
19420
- ` ${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`
19421
19585
  );
19422
19586
  }
19423
19587
  }
@@ -19430,23 +19594,23 @@ function sheetFormat(args) {
19430
19594
  }
19431
19595
 
19432
19596
  // src/cli/commands/pixellab-utils.ts
19433
- import path40 from "path";
19597
+ import path41 from "path";
19434
19598
 
19435
19599
  // src/pixellab-utils.ts
19436
- import { existsSync as existsSync29 } from "fs";
19437
- import { mkdir as mkdir11, readFile as readFile26, writeFile as writeFile14 } from "fs/promises";
19438
- 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";
19439
19603
  var UNZOOM_MIN_SIDE = 256;
19440
19604
  var UNZOOM_MAX_AREA = 2048 * 2048;
19441
19605
  function defaultUnzoomOut(input) {
19442
- const ext = path39.extname(input);
19606
+ const ext = path40.extname(input);
19443
19607
  return `${ext ? input.slice(0, -ext.length) : input}.unzoomed.png`;
19444
19608
  }
19445
19609
  async function unzoomFile(client2, input, opts = {}) {
19446
- const out = path39.resolve(opts.out ?? defaultUnzoomOut(input));
19447
- if (path39.resolve(input) === out) throw new Error("unzoom will not overwrite its own input; pass a different --out");
19448
- if (existsSync29(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
19449
- 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);
19450
19614
  const metadata = imageMetadata(bytes);
19451
19615
  if (!metadata) throw new Error(`${input} is not a readable PNG or JPEG`);
19452
19616
  const { width, height, format } = metadata;
@@ -19459,22 +19623,22 @@ async function unzoomFile(client2, input, opts = {}) {
19459
19623
  throw new Error(`${input} is ${width}x${height}; PixelLab's unzoom takes at most 2048x2048 worth of pixels`);
19460
19624
  }
19461
19625
  const res = await client2.unzoom({ image: { base64: bytes.toString("base64"), format }, quantize: opts.quantize });
19462
- await mkdir11(path39.dirname(out), { recursive: true });
19463
- await writeFile14(out, res.png);
19626
+ await mkdir12(path40.dirname(out), { recursive: true });
19627
+ await writeFile15(out, res.png);
19464
19628
  return { out, original: res.originalSize, unzoomed: res.unzoomedSize, zoomFactor: res.zoomFactor, usage: res.usage };
19465
19629
  }
19466
19630
  var FONT_WEIGHTS = ["Bold", "Regular"];
19467
19631
  var FONT_GLYPH_SIZES = [8, 16, 32, 64];
19468
19632
  var FONT_COST_GENERATIONS = 25;
19469
19633
  function fontOutputPaths(out) {
19470
- const base = path39.resolve(out.replace(/\.(ttf|png)$/i, ""));
19634
+ const base = path40.resolve(out.replace(/\.(ttf|png)$/i, ""));
19471
19635
  return { ttf: `${base}.ttf`, atlas: `${base}.png` };
19472
19636
  }
19473
19637
  async function generateFont(client2, opts) {
19474
19638
  const { ttf, atlas } = fontOutputPaths(opts.out);
19475
19639
  if (!opts.force) {
19476
19640
  for (const file of [ttf, atlas]) {
19477
- 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`);
19478
19642
  }
19479
19643
  }
19480
19644
  const sleep3 = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
@@ -19501,9 +19665,9 @@ async function generateFont(client2, opts) {
19501
19665
  client2.download(job.download_ttf_url),
19502
19666
  client2.download(job.download_atlas_url)
19503
19667
  ]);
19504
- await mkdir11(path39.dirname(ttf), { recursive: true });
19505
- await writeFile14(ttf, ttfBytes);
19506
- await writeFile14(atlas, atlasBytes);
19668
+ await mkdir12(path40.dirname(ttf), { recursive: true });
19669
+ await writeFile15(ttf, ttfBytes);
19670
+ await writeFile15(atlas, atlasBytes);
19507
19671
  return { jobId, ttf, atlas, glyphPx: job.glyph_px ?? null, usage: job.usage };
19508
19672
  }
19509
19673
  if (Date.now() - started > timeout) {
@@ -19517,7 +19681,7 @@ async function generateFont(client2, opts) {
19517
19681
 
19518
19682
  // src/cli/commands/pixellab-utils.ts
19519
19683
  function loadKeys(args) {
19520
- loadEnvFiles(path40.dirname(path40.resolve(args.manifest)));
19684
+ loadEnvFiles(path41.dirname(path41.resolve(args.manifest)));
19521
19685
  loadEnvFiles(process.cwd());
19522
19686
  }
19523
19687
  async function runUnzoom(args) {
@@ -19583,19 +19747,19 @@ async function runFont(args) {
19583
19747
  }
19584
19748
 
19585
19749
  // src/cli/commands/quality.ts
19586
- import path42 from "path";
19587
- import { readFile as readFile28 } from "fs/promises";
19750
+ import path43 from "path";
19751
+ import { readFile as readFile29 } from "fs/promises";
19588
19752
 
19589
19753
  // src/pipeline/quality-regression.ts
19590
- import path41 from "path";
19591
- import { existsSync as existsSync30 } from "fs";
19592
- import { readFile as readFile27 } from "fs/promises";
19593
- 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";
19594
19758
  var SHA256_RE = /^[0-9a-f]{64}$/;
19595
19759
  var HEX_RE = /^#[0-9a-f]{6}$/;
19596
19760
  var MAX_REDMEAN_DISTANCE = 765;
19597
- var QualityPathSchema = z10.string().min(1).refine(
19598
- (value) => !path41.posix.isAbsolute(value) && !value.includes("\\"),
19761
+ var QualityPathSchema = z11.string().min(1).refine(
19762
+ (value) => !path42.posix.isAbsolute(value) && !value.includes("\\"),
19599
19763
  "expected a portable relative path"
19600
19764
  );
19601
19765
  var DEFAULT_QUALITY_TOLERANCES = {
@@ -19608,53 +19772,53 @@ var DEFAULT_QUALITY_TOLERANCES = {
19608
19772
  maxEdgeContrastDrop: 0.03,
19609
19773
  maxIsolatedPixelIncrease: 5e-3
19610
19774
  };
19611
- var QualityToleranceSchema = z10.object({
19612
- requireExactHash: z10.boolean(),
19613
- maxColorCountIncrease: z10.number().int().min(0),
19614
- maxNewColors: z10.number().int().min(0),
19615
- maxTransparencyDelta: z10.number().min(0).max(1),
19616
- maxPartialAlphaIncrease: z10.number().min(0).max(1),
19617
- maxEdgeDensityDelta: z10.number().min(0).max(1),
19618
- maxEdgeContrastDrop: z10.number().min(0).max(1),
19619
- 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)
19620
19784
  }).strict();
19621
- var ImageQualityMetricsSchema = z10.object({
19622
- sha256: z10.string().regex(SHA256_RE),
19623
- width: z10.number().int().min(1),
19624
- height: z10.number().int().min(1),
19625
- colorCount: z10.number().int().min(0),
19626
- colors: z10.array(z10.string().regex(HEX_RE)),
19627
- transparency: z10.number().min(0).max(1),
19628
- partialAlpha: z10.number().min(0).max(1),
19629
- edgeDensity: z10.number().min(0).max(1),
19630
- meanEdgeContrast: z10.number().min(0).max(1),
19631
- 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)
19632
19796
  }).strict();
19633
- var QualityBaselineCaseSchema = z10.object({
19634
- 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/_-]*$/),
19635
19799
  file: QualityPathSchema,
19636
- record: z10.object({
19800
+ record: z11.object({
19637
19801
  path: QualityPathSchema,
19638
- sha256: z10.string().regex(SHA256_RE)
19802
+ sha256: z11.string().regex(SHA256_RE)
19639
19803
  }).strict().optional(),
19640
19804
  expected: ImageQualityMetricsSchema,
19641
19805
  tolerances: QualityToleranceSchema
19642
19806
  }).strict();
19643
- var QualityBaselineSchema = z10.object({
19644
- $schema: z10.string().url().optional(),
19645
- format: z10.literal("pixelkiln-quality-baseline"),
19646
- schemaVersion: z10.literal(1),
19647
- 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)
19648
19812
  }).strict().superRefine((baseline, context) => {
19649
19813
  const ids = baseline.cases.map((entry) => entry.id);
19650
19814
  if (new Set(ids).size !== ids.length) {
19651
- 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" });
19652
19816
  }
19653
19817
  });
19654
- var QualityInputSchema = z10.object({
19655
- id: z10.string().regex(/^[a-z0-9][a-z0-9/_-]*$/),
19656
- path: z10.string().min(1),
19657
- 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(),
19658
19822
  tolerances: QualityToleranceSchema.partial().optional()
19659
19823
  }).strict();
19660
19824
  function message4(error) {
@@ -19742,10 +19906,10 @@ function measureDecodedImageQuality(png, digest2) {
19742
19906
  };
19743
19907
  }
19744
19908
  async function measureImageQuality(file) {
19745
- const absolute = path41.resolve(file);
19909
+ const absolute = path42.resolve(file);
19746
19910
  let bytes;
19747
19911
  try {
19748
- bytes = await readFile27(absolute);
19912
+ bytes = await readFile28(absolute);
19749
19913
  } catch (error) {
19750
19914
  throw new Error(`Cannot read quality image ${absolute}: ${message4(error)}`, { cause: error });
19751
19915
  }
@@ -19756,7 +19920,7 @@ async function measureImageQuality(file) {
19756
19920
  }
19757
19921
  }
19758
19922
  function resolveQualityInputs(raw, inputsFilePath) {
19759
- const parsed = z10.array(QualityInputSchema).min(1).safeParse(raw);
19923
+ const parsed = z11.array(QualityInputSchema).min(1).safeParse(raw);
19760
19924
  if (!parsed.success) {
19761
19925
  throw new Error(
19762
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")
@@ -19764,11 +19928,11 @@ function resolveQualityInputs(raw, inputsFilePath) {
19764
19928
  }
19765
19929
  const ids = parsed.data.map((entry) => entry.id);
19766
19930
  if (new Set(ids).size !== ids.length) throw new Error("--inputs contains a duplicate quality case id.");
19767
- const root = path41.dirname(path41.resolve(inputsFilePath));
19931
+ const root = path42.dirname(path42.resolve(inputsFilePath));
19768
19932
  return parsed.data.map((entry) => ({
19769
19933
  id: entry.id,
19770
- file: path41.resolve(root, entry.path),
19771
- ...entry.record ? { record: path41.resolve(root, entry.record) } : {},
19934
+ file: path42.resolve(root, entry.path),
19935
+ ...entry.record ? { record: path42.resolve(root, entry.record) } : {},
19772
19936
  tolerances: QualityToleranceSchema.parse({
19773
19937
  ...DEFAULT_QUALITY_TOLERANCES,
19774
19938
  ...entry.tolerances
@@ -19776,7 +19940,7 @@ function resolveQualityInputs(raw, inputsFilePath) {
19776
19940
  }));
19777
19941
  }
19778
19942
  function portableRelative2(from, to) {
19779
- return path41.relative(from, path41.resolve(to)).split(path41.sep).join("/") || ".";
19943
+ return path42.relative(from, path42.resolve(to)).split(path42.sep).join("/") || ".";
19780
19944
  }
19781
19945
  async function verifyRecordForImage(recordPath, imagePath) {
19782
19946
  const verification = await verifyArtifactBundle(recordPath);
@@ -19791,20 +19955,20 @@ async function verifyRecordForImage(recordPath, imagePath) {
19791
19955
  if (record.kind !== "refine" || options === null || typeof options !== "object" || options.schema !== "pixelkiln-quality") {
19792
19956
  throw new Error(`Quality record ${recordPath} is not a PixelKiln refinement record.`);
19793
19957
  }
19794
- const outputs = record.outputs.map((output) => path41.resolve(path41.dirname(recordPath), output.path));
19795
- 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))) {
19796
19960
  throw new Error(`Quality record ${recordPath} does not own ${imagePath}.`);
19797
19961
  }
19798
19962
  }
19799
19963
  async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
19800
19964
  if (!inputs.length) throw new Error("Cannot snapshot an empty quality baseline.");
19801
- const absolute = path41.resolve(baselinePath);
19965
+ const absolute = path42.resolve(baselinePath);
19802
19966
  for (const input of inputs) {
19803
- 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) {
19804
19968
  throw new Error(`Quality baseline output would overwrite an input: ${absolute}.`);
19805
19969
  }
19806
19970
  }
19807
- const root = path41.dirname(absolute);
19971
+ const root = path42.dirname(absolute);
19808
19972
  const cases = [];
19809
19973
  for (const input of [...inputs].sort((a, b) => a.id.localeCompare(b.id))) {
19810
19974
  const expected = await measureImageQuality(input.file);
@@ -19831,8 +19995,8 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
19831
19995
  cases
19832
19996
  });
19833
19997
  const data = Buffer.from(JSON.stringify(baseline, null, 2) + "\n");
19834
- if (existsSync30(absolute) && !options.force) {
19835
- const current = await readFile27(absolute);
19998
+ if (existsSync31(absolute) && !options.force) {
19999
+ const current = await readFile28(absolute);
19836
20000
  if (!current.equals(data)) {
19837
20001
  throw new OverwriteRefusedError(
19838
20002
  `Quality baseline already exists with different content: ${absolute}. Pass --force to replace it.`,
@@ -19844,10 +20008,10 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
19844
20008
  return { baseline, path: absolute, changed: result.changed.length > 0 };
19845
20009
  }
19846
20010
  async function readQualityBaseline(baselinePath) {
19847
- const absolute = path41.resolve(baselinePath);
20011
+ const absolute = path42.resolve(baselinePath);
19848
20012
  let raw;
19849
20013
  try {
19850
- raw = JSON.parse(await readFile27(absolute, "utf8"));
20014
+ raw = JSON.parse(await readFile28(absolute, "utf8"));
19851
20015
  } catch (error) {
19852
20016
  throw new Error(`Could not read quality baseline ${absolute}: ${message4(error)}`, { cause: error });
19853
20017
  }
@@ -19917,12 +20081,12 @@ function regressionViolations(expected, actual, tolerances) {
19917
20081
  return { violations, warnings };
19918
20082
  }
19919
20083
  async function checkQualityBaseline(baselinePath) {
19920
- const absolute = path41.resolve(baselinePath);
20084
+ const absolute = path42.resolve(baselinePath);
19921
20085
  const baseline = await readQualityBaseline(absolute);
19922
- const root = path41.dirname(absolute);
20086
+ const root = path42.dirname(absolute);
19923
20087
  const cases = [];
19924
20088
  for (const entry of baseline.cases) {
19925
- const file = path41.resolve(root, entry.file);
20089
+ const file = path42.resolve(root, entry.file);
19926
20090
  let actual = null;
19927
20091
  const violations = [];
19928
20092
  const warnings = [];
@@ -19935,7 +20099,7 @@ async function checkQualityBaseline(baselinePath) {
19935
20099
  violations.push(message4(error));
19936
20100
  }
19937
20101
  if (entry.record) {
19938
- const recordPath = path41.resolve(root, entry.record.path);
20102
+ const recordPath = path42.resolve(root, entry.record.path);
19939
20103
  try {
19940
20104
  const recordHash = await sha256File(recordPath);
19941
20105
  if (recordHash !== entry.record.sha256) violations.push("quality record hash changed");
@@ -19977,15 +20141,15 @@ async function runQuality(args) {
19977
20141
  if (!args.out) throw new Error("quality snapshot needs --out <pixelkiln.quality.json>.");
19978
20142
  let raw;
19979
20143
  try {
19980
- raw = JSON.parse(await readFile28(path42.resolve(args.inputs), "utf8"));
20144
+ raw = JSON.parse(await readFile29(path43.resolve(args.inputs), "utf8"));
19981
20145
  } catch (error) {
19982
20146
  throw new Error(
19983
- `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)}`,
19984
20148
  { cause: error }
19985
20149
  );
19986
20150
  }
19987
20151
  const inputs = resolveQualityInputs(raw, args.inputs);
19988
- if (path42.resolve(args.inputs) === path42.resolve(args.out)) {
20152
+ if (path43.resolve(args.inputs) === path43.resolve(args.out)) {
19989
20153
  throw new Error("quality snapshot --out must not overwrite its --inputs file.");
19990
20154
  }
19991
20155
  const result = await snapshotQualityBaseline(inputs, args.out, { force: args.force });
@@ -19998,7 +20162,7 @@ async function runQuality(args) {
19998
20162
  }, null, 2));
19999
20163
  } else {
20000
20164
  log(
20001
- ` ${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))`
20002
20166
  );
20003
20167
  log(` Review the tolerances, commit the baseline, then run quality check in CI.`);
20004
20168
  }
@@ -20024,54 +20188,54 @@ async function runQuality(args) {
20024
20188
  }
20025
20189
 
20026
20190
  // src/cli/commands/recipe.ts
20027
- import path44 from "path";
20191
+ import path45 from "path";
20028
20192
 
20029
20193
  // src/recipes.ts
20030
- import path43 from "path";
20031
- import { existsSync as existsSync31 } from "fs";
20032
- 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";
20033
20197
  import { fileURLToPath } from "url";
20034
- import { z as z11 } from "zod";
20198
+ import { z as z12 } from "zod";
20035
20199
  var SHA256_RE2 = /^[0-9a-f]{64}$/;
20036
20200
  var RECIPE_ID_RE = /^[a-z0-9][a-z0-9-]*\/[a-z0-9][a-z0-9-]*$/;
20037
20201
  var VERSION_RE = /^\d+\.\d+\.\d+$/;
20038
20202
  var RECIPE_FILE = "pixelkiln.recipe.json";
20039
20203
  var RECIPE_DIR_TOKEN = "{{recipeDir}}";
20040
- var PortablePathSchema = z11.string().min(1).refine((value) => {
20041
- 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;
20042
20206
  const parts = value.split("/");
20043
20207
  return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
20044
20208
  }, "expected a portable relative path without '.', '..', or backslashes");
20045
- var RecipeBindingSchema = z11.object({
20046
- nodeId: z11.string().min(1),
20047
- input: z11.string().min(1)
20209
+ var RecipeBindingSchema = z12.object({
20210
+ nodeId: z12.string().min(1),
20211
+ input: z12.string().min(1)
20048
20212
  }).strict();
20049
- var RecipeSchema = z11.object({
20050
- $schema: z11.string().url().optional(),
20051
- format: z11.literal("pixelkiln-recipe"),
20052
- schemaVersion: z11.literal(1),
20053
- id: z11.string().regex(RECIPE_ID_RE, "expected provider/name"),
20054
- version: z11.string().regex(VERSION_RE, "expected x.y.z"),
20055
- provider: z11.string().regex(/^[a-z0-9][a-z0-9-]*$/),
20056
- summary: z11.string().min(1).max(240),
20057
- 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({
20058
20222
  path: PortablePathSchema,
20059
- role: z11.enum(["workflow", "reference"]),
20060
- sha256: z11.string().regex(SHA256_RE2)
20223
+ role: z12.enum(["workflow", "reference"]),
20224
+ sha256: z12.string().regex(SHA256_RE2)
20061
20225
  }).strict()).default([]),
20062
- models: z11.array(z11.object({
20226
+ models: z12.array(z12.object({
20063
20227
  path: PortablePathSchema,
20064
- sha256: z11.string().regex(SHA256_RE2),
20065
- source: z11.string().url(),
20066
- license: z11.string().min(1)
20228
+ sha256: z12.string().regex(SHA256_RE2),
20229
+ source: z12.string().url(),
20230
+ license: z12.string().min(1)
20067
20231
  }).strict()).default([]),
20068
- styleId: z11.string().regex(/^[a-z0-9][a-z0-9-]*$/),
20232
+ styleId: z12.string().regex(/^[a-z0-9][a-z0-9-]*$/),
20069
20233
  style: StyleSchema,
20070
- workflow: z11.object({
20234
+ workflow: z12.object({
20071
20235
  path: PortablePathSchema,
20072
- outputNodeId: z11.string().min(1),
20073
- numImages: z11.number().int().min(1).max(16),
20074
- bindings: z11.object({
20236
+ outputNodeId: z12.string().min(1),
20237
+ numImages: z12.number().int().min(1).max(16),
20238
+ bindings: z12.object({
20075
20239
  prompt: RecipeBindingSchema,
20076
20240
  width: RecipeBindingSchema.optional(),
20077
20241
  height: RecipeBindingSchema.optional(),
@@ -20082,67 +20246,67 @@ var RecipeSchema = z11.object({
20082
20246
  strength: RecipeBindingSchema.optional()
20083
20247
  }).strict()
20084
20248
  }).strict().optional(),
20085
- quality: z11.object({
20086
- stage: z11.enum(["composition-source", "production-candidate"]),
20087
- workingCanvas: z11.object({
20088
- width: z11.number().int().min(16),
20089
- 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)
20090
20254
  }).strict(),
20091
- recommendedNativeSize: z11.object({
20092
- min: z11.number().int().min(1),
20093
- max: z11.number().int().min(1)
20255
+ recommendedNativeSize: z12.object({
20256
+ min: z12.number().int().min(1),
20257
+ max: z12.number().int().min(1)
20094
20258
  }).strict(),
20095
- paletteColors: z11.object({
20096
- min: z11.number().int().min(1),
20097
- max: z11.number().int().min(1)
20259
+ paletteColors: z12.object({
20260
+ min: z12.number().int().min(1),
20261
+ max: z12.number().int().min(1)
20098
20262
  }).strict(),
20099
- background: z11.enum(["opaque", "transparent", "full-bleed"]),
20100
- checks: z11.array(z11.enum([
20263
+ background: z12.enum(["opaque", "transparent", "full-bleed"]),
20264
+ checks: z12.array(z12.enum([
20101
20265
  "prompt-coverage",
20102
20266
  "native-grid",
20103
20267
  "final-palette",
20104
20268
  "alpha-edge",
20105
20269
  "human-1x"
20106
20270
  ])).min(1),
20107
- notes: z11.array(z11.string().min(1)).default([])
20271
+ notes: z12.array(z12.string().min(1)).default([])
20108
20272
  }).strict(),
20109
- integrity: z11.object({
20110
- algorithm: z11.literal("sha256"),
20111
- digest: z11.string().regex(SHA256_RE2)
20273
+ integrity: z12.object({
20274
+ algorithm: z12.literal("sha256"),
20275
+ digest: z12.string().regex(SHA256_RE2)
20112
20276
  }).strict()
20113
20277
  }).strict().superRefine((recipe, context) => {
20114
20278
  if (recipe.style.provider && recipe.style.provider !== recipe.provider) {
20115
20279
  context.addIssue({
20116
- code: z11.ZodIssueCode.custom,
20280
+ code: z12.ZodIssueCode.custom,
20117
20281
  path: ["style", "provider"],
20118
20282
  message: "must match the recipe provider"
20119
20283
  });
20120
20284
  }
20121
20285
  const filePaths = recipe.files.map((file) => file.path);
20122
20286
  if (new Set(filePaths).size !== filePaths.length) {
20123
- 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" });
20124
20288
  }
20125
20289
  const modelPaths = recipe.models.map((model) => model.path);
20126
20290
  if (new Set(modelPaths).size !== modelPaths.length) {
20127
- 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" });
20128
20292
  }
20129
20293
  if (filePaths.includes(RECIPE_FILE)) {
20130
20294
  context.addIssue({
20131
- code: z11.ZodIssueCode.custom,
20295
+ code: z12.ZodIssueCode.custom,
20132
20296
  path: ["files"],
20133
20297
  message: `${RECIPE_FILE} cannot hash itself`
20134
20298
  });
20135
20299
  }
20136
20300
  if (recipe.quality.recommendedNativeSize.min > recipe.quality.recommendedNativeSize.max) {
20137
20301
  context.addIssue({
20138
- code: z11.ZodIssueCode.custom,
20302
+ code: z12.ZodIssueCode.custom,
20139
20303
  path: ["quality", "recommendedNativeSize"],
20140
20304
  message: "min cannot exceed max"
20141
20305
  });
20142
20306
  }
20143
20307
  if (recipe.quality.paletteColors.min > recipe.quality.paletteColors.max) {
20144
20308
  context.addIssue({
20145
- code: z11.ZodIssueCode.custom,
20309
+ code: z12.ZodIssueCode.custom,
20146
20310
  path: ["quality", "paletteColors"],
20147
20311
  message: "min cannot exceed max"
20148
20312
  });
@@ -20151,7 +20315,7 @@ var RecipeSchema = z11.object({
20151
20315
  (file) => file.path === recipe.workflow.path && file.role === "workflow"
20152
20316
  )) {
20153
20317
  context.addIssue({
20154
- code: z11.ZodIssueCode.custom,
20318
+ code: z12.ZodIssueCode.custom,
20155
20319
  path: ["workflow", "path"],
20156
20320
  message: "must name a file whose role is workflow"
20157
20321
  });
@@ -20159,7 +20323,7 @@ var RecipeSchema = z11.object({
20159
20323
  if (recipe.provider === "comfyui") {
20160
20324
  if (!recipe.workflow) {
20161
20325
  context.addIssue({
20162
- code: z11.ZodIssueCode.custom,
20326
+ code: z12.ZodIssueCode.custom,
20163
20327
  path: ["workflow"],
20164
20328
  message: "ComfyUI recipes require workflow metadata"
20165
20329
  });
@@ -20168,7 +20332,7 @@ var RecipeSchema = z11.object({
20168
20332
  const options = recipe.style.providerOptions.comfyui;
20169
20333
  if (!options || typeof options !== "object" || Array.isArray(options)) {
20170
20334
  context.addIssue({
20171
- code: z11.ZodIssueCode.custom,
20335
+ code: z12.ZodIssueCode.custom,
20172
20336
  path: ["style", "providerOptions", "comfyui"],
20173
20337
  message: "ComfyUI recipes require ComfyUI provider options"
20174
20338
  });
@@ -20182,7 +20346,7 @@ var RecipeSchema = z11.object({
20182
20346
  };
20183
20347
  if (JSON.stringify(canonical3(options)) !== JSON.stringify(canonical3(expected))) {
20184
20348
  context.addIssue({
20185
- code: z11.ZodIssueCode.custom,
20349
+ code: z12.ZodIssueCode.custom,
20186
20350
  path: ["style", "providerOptions", "comfyui"],
20187
20351
  message: "must match workflow metadata and use {{recipeDir}} for workflowFile"
20188
20352
  });
@@ -20214,10 +20378,10 @@ function message5(error) {
20214
20378
  return error instanceof Error ? error.message : String(error);
20215
20379
  }
20216
20380
  async function readRecipeFile(recipePath, bundled) {
20217
- const absolute = path43.resolve(recipePath);
20381
+ const absolute = path44.resolve(recipePath);
20218
20382
  let raw;
20219
20383
  try {
20220
- raw = JSON.parse(await readFile29(absolute, "utf8"));
20384
+ raw = JSON.parse(await readFile30(absolute, "utf8"));
20221
20385
  } catch (error) {
20222
20386
  throw new Error(`Could not read recipe ${absolute}: ${message5(error)}`, { cause: error });
20223
20387
  }
@@ -20228,13 +20392,13 @@ async function readRecipeFile(recipePath, bundled) {
20228
20392
  ` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
20229
20393
  );
20230
20394
  }
20231
- 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 };
20232
20396
  }
20233
20397
  async function walkRecipeFiles(root) {
20234
- if (!existsSync31(root)) return [];
20398
+ if (!existsSync32(root)) return [];
20235
20399
  const out = [];
20236
20400
  for (const entry of await readdir3(root, { withFileTypes: true })) {
20237
- const resolved = path43.join(root, entry.name);
20401
+ const resolved = path44.join(root, entry.name);
20238
20402
  if (entry.isDirectory()) out.push(...await walkRecipeFiles(resolved));
20239
20403
  else if (entry.isFile() && entry.name === RECIPE_FILE) out.push(resolved);
20240
20404
  }
@@ -20245,7 +20409,7 @@ function bundledRecipeRoot() {
20245
20409
  }
20246
20410
  async function listBundledRecipes(root = bundledRecipeRoot()) {
20247
20411
  const recipes = await Promise.all(
20248
- (await walkRecipeFiles(path43.resolve(root))).map((file) => readRecipeFile(file, true))
20412
+ (await walkRecipeFiles(path44.resolve(root))).map((file) => readRecipeFile(file, true))
20249
20413
  );
20250
20414
  return recipes.sort(
20251
20415
  (a, b) => a.recipe.id.localeCompare(b.recipe.id) || compareVersions(b.recipe.version, a.recipe.version)
@@ -20268,9 +20432,9 @@ function parseSelector(selector) {
20268
20432
  return { id: match[1], version: match[2] };
20269
20433
  }
20270
20434
  async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
20271
- const local = path43.resolve(target);
20272
- if (existsSync31(local)) {
20273
- 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);
20274
20438
  return readRecipeFile(recipePath, false);
20275
20439
  }
20276
20440
  if (target.startsWith(".") || target.startsWith("/") || target.includes("\\")) {
@@ -20284,8 +20448,8 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
20284
20448
  return matches[0];
20285
20449
  }
20286
20450
  async function verifyFile(root, file) {
20287
- const absolute = path43.resolve(root, ...file.path.split("/"));
20288
- if (!existsSync31(absolute)) {
20451
+ const absolute = path44.resolve(root, ...file.path.split("/"));
20452
+ if (!existsSync32(absolute)) {
20289
20453
  return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
20290
20454
  }
20291
20455
  const actualSha256 = await sha256File(absolute);
@@ -20300,7 +20464,7 @@ async function verifyRecipe(target, options = {}) {
20300
20464
  const loaded = await resolveRecipe(target, options.bundledRoot);
20301
20465
  const actualIntegrity = recipeDigest(loaded.authored);
20302
20466
  const files = await Promise.all(loaded.recipe.files.map((file) => verifyFile(loaded.dir, file)));
20303
- const modelRoot = options.modelRoot ? path43.resolve(options.modelRoot) : null;
20467
+ const modelRoot = options.modelRoot ? path44.resolve(options.modelRoot) : null;
20304
20468
  const models = modelRoot ? await Promise.all(loaded.recipe.models.map(async (model) => ({
20305
20469
  ...await verifyFile(modelRoot, model),
20306
20470
  source: model.source,
@@ -20334,7 +20498,7 @@ async function verifyRecipe(target, options = {}) {
20334
20498
  };
20335
20499
  }
20336
20500
  function renderStyle(style, destination, cwd) {
20337
- const portableDir = path43.relative(cwd, destination).split(path43.sep).join("/") || ".";
20501
+ const portableDir = path44.relative(cwd, destination).split(path44.sep).join("/") || ".";
20338
20502
  const replace = (value) => {
20339
20503
  if (typeof value === "string") return value.replaceAll(RECIPE_DIR_TOKEN, portableDir);
20340
20504
  if (Array.isArray(value)) return value.map(replace);
@@ -20346,25 +20510,25 @@ function renderStyle(style, destination, cwd) {
20346
20510
  return replace(style);
20347
20511
  }
20348
20512
  async function installRecipe(target, options = {}) {
20349
- const cwd = path43.resolve(options.cwd ?? process.cwd());
20513
+ const cwd = path44.resolve(options.cwd ?? process.cwd());
20350
20514
  const loaded = await resolveRecipe(target, options.bundledRoot);
20351
20515
  const verification = await verifyRecipe(loaded.path);
20352
20516
  if (!verification.ok) {
20353
20517
  throw new Error(`Recipe ${loaded.recipe.id}@${loaded.recipe.version} failed integrity verification.`);
20354
20518
  }
20355
- 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);
20356
20520
  const sources = [
20357
20521
  { path: RECIPE_FILE, source: loaded.path },
20358
- ...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("/")) }))
20359
20523
  ];
20360
20524
  const files = await Promise.all(sources.map(async (file) => ({
20361
- path: path43.join(destination, ...file.path.split("/")),
20362
- data: await readFile29(file.source)
20525
+ path: path44.join(destination, ...file.path.split("/")),
20526
+ data: await readFile30(file.source)
20363
20527
  })));
20364
20528
  if (!options.force) {
20365
20529
  for (const file of files) {
20366
- if (!existsSync31(file.path)) continue;
20367
- const current = await readFile29(file.path);
20530
+ if (!existsSync32(file.path)) continue;
20531
+ const current = await readFile30(file.path);
20368
20532
  if (!current.equals(file.data)) {
20369
20533
  throw new OverwriteRefusedError(
20370
20534
  `Recipe destination has local changes: ${file.path}. Choose another --out or pass --force to replace declared recipe files.`,
@@ -20458,14 +20622,14 @@ async function runRecipe(args) {
20458
20622
  }, null, 2));
20459
20623
  } else {
20460
20624
  log(` installed ${result.recipe.id}@${result.recipe.version}`);
20461
- log(` ${path44.relative(process.cwd(), result.destination) || "."}`);
20625
+ log(` ${path45.relative(process.cwd(), result.destination) || "."}`);
20462
20626
  log(`
20463
20627
  Add this entry under your manifest's styles object:`);
20464
20628
  log(JSON.stringify({ [result.styleId]: result.style }, null, 2));
20465
20629
  if (result.recipe.models.length) {
20466
20630
  log(`
20467
20631
  Models are not downloaded automatically. Verify them with:`);
20468
- 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>`);
20469
20633
  }
20470
20634
  }
20471
20635
  return;
@@ -20473,7 +20637,7 @@ async function runRecipe(args) {
20473
20637
  }
20474
20638
 
20475
20639
  // src/cli/commands/refine.ts
20476
- import path45 from "path";
20640
+ import path46 from "path";
20477
20641
  async function runRefine(args) {
20478
20642
  if (args.from || args.subcommand === "approve") {
20479
20643
  if (!args.from) {
@@ -20503,12 +20667,12 @@ async function runRefine(args) {
20503
20667
  ` recovered ${result3.detection.columns}x${result3.detection.rows} native grid (${result3.detection.confidence}: ${result3.detection.consensus})`
20504
20668
  );
20505
20669
  log(` enforced ${result3.palette.length}-color palette without dithering`);
20506
- log(` wrote ${path45.relative(process.cwd(), result3.output)}`);
20507
- 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)}`);
20508
20672
  log(`
20509
20673
  Automated checks passed. Human 1\xD7 review is still required:`);
20510
20674
  log(
20511
- ` 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"`
20512
20676
  );
20513
20677
  }
20514
20678
  return;
@@ -20536,7 +20700,7 @@ async function runRefine(args) {
20536
20700
  if (args.json) log(JSON.stringify(result3, null, 2));
20537
20701
  else {
20538
20702
  log(
20539
- 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()}`
20540
20704
  );
20541
20705
  }
20542
20706
  return;
@@ -20545,7 +20709,7 @@ async function runRefine(args) {
20545
20709
  if (args.json) {
20546
20710
  log(JSON.stringify(result2, null, 2));
20547
20711
  } else {
20548
- 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)}`);
20549
20713
  for (const reason of result2.reasons) log(` ${reason}`);
20550
20714
  }
20551
20715
  if (!result2.safe) process.exitCode = 1;
@@ -20591,7 +20755,7 @@ async function runRefine(args) {
20591
20755
  log(` ${item.state.padEnd(16)} ${item.key} ${item.reason}`);
20592
20756
  if (item.state === "needs-approval") {
20593
20757
  log(
20594
- ` 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"`
20595
20759
  );
20596
20760
  }
20597
20761
  }
@@ -20602,9 +20766,9 @@ async function runRefine(args) {
20602
20766
  }
20603
20767
 
20604
20768
  // src/cli/commands/skeleton.ts
20605
- import path46 from "path";
20606
- import { existsSync as existsSync32 } from "fs";
20607
- 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";
20608
20772
 
20609
20773
  // src/skeleton-preview.ts
20610
20774
  var RIGHT = [255, 140, 40, 255];
@@ -20712,23 +20876,19 @@ var Canvas = class {
20712
20876
  };
20713
20877
 
20714
20878
  // src/cli/commands/skeleton.ts
20715
- var ESTIMATE_SKELETON_SIZES = [16, 32, 64, 128, 256];
20716
20879
  async function runEstimateSkeleton(args) {
20717
20880
  const image = args.target;
20718
20881
  if (!image) throw new UsageError("estimate-skeleton needs an image path: pixelkiln estimate-skeleton <image> [--out keypoints.json]");
20719
- if (args.out && existsSync32(path46.resolve(args.out)) && !args.force) {
20882
+ if (args.out && existsSync33(path47.resolve(args.out)) && !args.force) {
20720
20883
  throw new UsageError(`${args.out} already exists; pass --force to replace it`);
20721
20884
  }
20722
- loadEnvFiles(path46.dirname(path46.resolve(args.manifest)));
20885
+ loadEnvFiles(path47.dirname(path47.resolve(args.manifest)));
20723
20886
  loadEnvFiles(process.cwd());
20724
- const bytes = await readFile30(path46.resolve(image));
20887
+ const bytes = await readFile31(path47.resolve(image));
20725
20888
  const metadata = imageMetadata(bytes);
20726
20889
  if (!metadata) throw new UsageError(`${image} is not a readable PNG or JPEG`);
20727
- if (metadata.width !== metadata.height || !ESTIMATE_SKELETON_SIZES.includes(metadata.width)) {
20728
- throw new UsageError(
20729
- `${image} is ${metadata.width}x${metadata.height}; estimate-skeleton takes a square image of ${ESTIMATE_SKELETON_SIZES.join(", ")} pixels`
20730
- );
20731
- }
20890
+ const sizeProblem = estimateSkeletonSizeProblem(metadata.width, metadata.height);
20891
+ if (sizeProblem) throw new UsageError(`${image} is ${sizeProblem}`);
20732
20892
  const client2 = clientFromEnv();
20733
20893
  const res = await client2.estimateSkeleton({
20734
20894
  image: { base64: bytes.toString("base64"), format: metadata.format }
@@ -20736,7 +20896,7 @@ async function runEstimateSkeleton(args) {
20736
20896
  const frames = args.frames ?? DEFAULT_SCAFFOLD_FRAMES;
20737
20897
  const json = JSON.stringify(scaffoldSkeletonSet(res.keypoints, frames), null, 2);
20738
20898
  if (args.out) {
20739
- await writeFile15(path46.resolve(args.out), json + "\n");
20899
+ await writeFile16(path47.resolve(args.out), json + "\n");
20740
20900
  log(` wrote ${args.out}: the estimated pose and ${frames} frames to edit`);
20741
20901
  log(` preview it with: pixelkiln skeleton-preview ${args.out} --from ${image}`);
20742
20902
  } else {
@@ -20749,8 +20909,8 @@ async function runSkeletonPreview(args) {
20749
20909
  let sourceFile;
20750
20910
  let label;
20751
20911
  if (target.endsWith(".json")) {
20752
- keypointsFile = path46.resolve(target);
20753
- sourceFile = args.from ? path46.resolve(args.from) : void 0;
20912
+ keypointsFile = path47.resolve(target);
20913
+ sourceFile = args.from ? path47.resolve(args.from) : void 0;
20754
20914
  label = target;
20755
20915
  } else {
20756
20916
  const loaded = await loadManifest(args.manifest);
@@ -20762,31 +20922,31 @@ async function runSkeletonPreview(args) {
20762
20922
  );
20763
20923
  }
20764
20924
  keypointsFile = spec.revision.keypointsFile;
20765
- sourceFile = args.from ? path46.resolve(args.from) : spec.revision.sourceFile;
20925
+ sourceFile = args.from ? path47.resolve(args.from) : spec.revision.sourceFile;
20766
20926
  label = `${spec.styleId}/${spec.assetId}`;
20767
20927
  }
20768
- if (!existsSync32(keypointsFile)) throw new UsageError(`Keypoints file does not exist yet: ${keypointsFile}`);
20928
+ if (!existsSync33(keypointsFile)) throw new UsageError(`Keypoints file does not exist yet: ${keypointsFile}`);
20769
20929
  let json;
20770
20930
  try {
20771
- json = JSON.parse(await readFile30(keypointsFile, "utf8"));
20931
+ json = JSON.parse(await readFile31(keypointsFile, "utf8"));
20772
20932
  } catch (error) {
20773
20933
  throw new UsageError(`${keypointsFile} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
20774
20934
  }
20775
20935
  const set = parseSkeletonSet(json, keypointsFile);
20776
20936
  let source;
20777
20937
  if (sourceFile) {
20778
- if (!existsSync32(sourceFile)) {
20938
+ if (!existsSync33(sourceFile)) {
20779
20939
  log(` source image does not exist yet, drawing on a plain ground: ${sourceFile}`);
20780
20940
  } else {
20781
- const bytes = await readFile30(sourceFile);
20941
+ const bytes = await readFile31(sourceFile);
20782
20942
  if (imageMetadata(bytes)?.format !== "png") throw new UsageError(`${sourceFile} must be a PNG to preview over`);
20783
20943
  source = decodePng(bytes);
20784
20944
  }
20785
20945
  }
20786
- const out = path46.resolve(args.out ?? keypointsFile.replace(/\.json$/, "") + ".preview.png");
20787
- await writeFile15(out, renderSkeletonSheet(set, { source }));
20946
+ const out = path47.resolve(args.out ?? keypointsFile.replace(/\.json$/, "") + ".preview.png");
20947
+ await writeFile16(out, renderSkeletonSheet(set, { source }));
20788
20948
  log(` ${label}: ${set.frames.length} frames after the starting pose`);
20789
- const shown = path46.relative(process.cwd(), out);
20949
+ const shown = path47.relative(process.cwd(), out);
20790
20950
  log(` wrote ${shown.startsWith("..") ? out : shown}`);
20791
20951
  }
20792
20952
 
@@ -20832,15 +20992,15 @@ async function runTools(args) {
20832
20992
  }
20833
20993
 
20834
20994
  // src/cli/commands/workspace.ts
20835
- import path47 from "path";
20836
- import { existsSync as existsSync33 } from "fs";
20995
+ import path48 from "path";
20996
+ import { existsSync as existsSync34 } from "fs";
20837
20997
  async function runWorkspace(args) {
20838
- const workspacePath = path47.resolve(args.workspace ?? "pixelkiln.workspace.json");
20839
- const dir = path47.dirname(workspacePath);
20998
+ const workspacePath = path48.resolve(args.workspace ?? "pixelkiln.workspace.json");
20999
+ const dir = path48.dirname(workspacePath);
20840
21000
  if (args.subcommand === "add") {
20841
- const manifestPath = path47.resolve(args.target);
21001
+ const manifestPath = path48.resolve(args.target);
20842
21002
  const loadedTarget = await loadManifest(manifestPath);
20843
- 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");
20844
21004
  const ws = await loadWorkspace(workspacePath);
20845
21005
  const id = args.name ?? loadedTarget.manifest.name;
20846
21006
  if (ws.projects.some((p) => p.id === id)) {
@@ -20860,10 +21020,10 @@ async function runWorkspace(args) {
20860
21020
  ...args.account ? { account: args.account } : {}
20861
21021
  };
20862
21022
  await saveWorkspace(workspacePath, { version: 1, projects: [...ws.projects, project] });
20863
- log(` registered "${id}" in ${path47.relative(process.cwd(), workspacePath)}`);
21023
+ log(` registered "${id}" in ${path48.relative(process.cwd(), workspacePath)}`);
20864
21024
  log(` manifest: ${project.manifest}`);
20865
21025
  log(` lock: ${project.lock}`);
20866
- if (!existsSync33(lockPath)) {
21026
+ if (!existsSync34(lockPath)) {
20867
21027
  log(
20868
21028
  ` warning: no lockfile there yet; this project contributes no claims until one is generated`
20869
21029
  );
@@ -20872,7 +21032,7 @@ async function runWorkspace(args) {
20872
21032
  }
20873
21033
  if (args.subcommand === "remove") {
20874
21034
  const ws = await loadWorkspace(workspacePath);
20875
- const resolvedTarget = path47.resolve(args.target);
21035
+ const resolvedTarget = path48.resolve(args.target);
20876
21036
  const match = ws.projects.find(
20877
21037
  (p) => p.id === args.target || resolveProject(dir, p).manifestPath === resolvedTarget
20878
21038
  );
@@ -20883,11 +21043,11 @@ async function runWorkspace(args) {
20883
21043
  version: 1,
20884
21044
  projects: ws.projects.filter((p) => p !== match)
20885
21045
  });
20886
- log(` removed "${match.id}" from ${path47.relative(process.cwd(), workspacePath)}`);
21046
+ log(` removed "${match.id}" from ${path48.relative(process.cwd(), workspacePath)}`);
20887
21047
  return;
20888
21048
  }
20889
21049
  if (args.subcommand === "list") {
20890
- if (!existsSync33(workspacePath)) {
21050
+ if (!existsSync34(workspacePath)) {
20891
21051
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
20892
21052
  }
20893
21053
  const ws = await loadWorkspace(workspacePath);
@@ -20895,9 +21055,9 @@ async function runWorkspace(args) {
20895
21055
  if (args.json) {
20896
21056
  log(JSON.stringify({ version: 1, workspace: workspacePath, projects: ws.projects, diagnostics }, null, 2));
20897
21057
  } else if (!ws.projects.length) {
20898
- log(` no projects registered in ${path47.relative(process.cwd(), workspacePath)}`);
21058
+ log(` no projects registered in ${path48.relative(process.cwd(), workspacePath)}`);
20899
21059
  } else {
20900
- 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)}:`);
20901
21061
  for (const p of ws.projects) {
20902
21062
  log(` ${p.id.padEnd(24)} ${p.manifest.padEnd(40)} (${p.provider}${p.account ? `, ${p.account}` : ""})`);
20903
21063
  }
@@ -20907,7 +21067,7 @@ async function runWorkspace(args) {
20907
21067
  return;
20908
21068
  }
20909
21069
  if (args.subcommand === "status") {
20910
- if (!existsSync33(workspacePath)) {
21070
+ if (!existsSync34(workspacePath)) {
20911
21071
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
20912
21072
  }
20913
21073
  const ws = await loadWorkspace(workspacePath);
@@ -20915,7 +21075,7 @@ async function runWorkspace(args) {
20915
21075
  if (args.json) {
20916
21076
  log(JSON.stringify({ ...report, workspace: workspacePath }, null, 2));
20917
21077
  } else {
20918
- log(` workspace: ${path47.relative(process.cwd(), workspacePath)}`);
21078
+ log(` workspace: ${path48.relative(process.cwd(), workspacePath)}`);
20919
21079
  for (const p of report.projects) {
20920
21080
  if (p.error) {
20921
21081
  log(`
@@ -20969,8 +21129,8 @@ async function runHelp() {
20969
21129
  async function runVersion() {
20970
21130
  for (const up of ["../package.json", "../../package.json"]) {
20971
21131
  const url = new URL(up, import.meta.url);
20972
- if (!existsSync34(url)) continue;
20973
- const pkg = JSON.parse(await readFile31(url, "utf8"));
21132
+ if (!existsSync35(url)) continue;
21133
+ const pkg = JSON.parse(await readFile32(url, "utf8"));
20974
21134
  if (pkg.name === "pixelkiln") {
20975
21135
  log(`${pkg.name} ${pkg.version}`);
20976
21136
  return;