pixelkiln 0.82.0 → 0.84.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 existsSync37 } from "fs";
71
- import { readFile as readFile33 } from "fs/promises";
70
+ import { existsSync as existsSync38 } from "fs";
71
+ import { readFile as readFile34 } from "fs/promises";
72
72
 
73
73
  // src/cli/args.ts
74
74
  import path from "path";
@@ -665,10 +665,10 @@ import { createReadStream } from "fs";
665
665
  function sha256(data) {
666
666
  return createHash("sha256").update(data).digest("hex");
667
667
  }
668
- async function sha256File(path50) {
668
+ async function sha256File(path51) {
669
669
  return new Promise((resolve, reject) => {
670
670
  const hash = createHash("sha256");
671
- const stream = createReadStream(path50);
671
+ const stream = createReadStream(path51);
672
672
  stream.on("data", (chunk2) => hash.update(chunk2));
673
673
  stream.on("error", reject);
674
674
  stream.on("end", () => resolve(hash.digest("hex")));
@@ -792,6 +792,10 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
792
792
  keypointsSha256: spec.revision.keypointsSha256,
793
793
  skeletonTemplate: spec.revision.skeletonTemplate,
794
794
  description: spec.revision.description,
795
+ removalTask: spec.revision.removalTask,
796
+ driftThreshold: spec.revision.driftThreshold,
797
+ subjectDescription: spec.revision.subjectDescription,
798
+ initialPose: spec.revision.initialPose,
795
799
  engine: spec.revision.engine
796
800
  } : void 0
797
801
  })
@@ -818,6 +822,7 @@ var RevisionModeSchema = z.enum([
818
822
  "outpaint",
819
823
  "reduce-colors",
820
824
  "correct-pixelart",
825
+ "remove-background",
821
826
  "animate",
822
827
  "animate-pixminimax",
823
828
  "animate-skeleton",
@@ -1149,7 +1154,22 @@ var RevisionSchema = z.object({
1149
1154
  * `interpolate`: required, the ending keyframe; the parent is the start.
1150
1155
  */
1151
1156
  lastFrame: z.string().min(1).optional(),
1152
- /** `animate-pixminimax` only: facing direction, used only alongside `enhancePrompt` to hold the sprite's facing. */
1157
+ /**
1158
+ * `animate`/`animate-pixminimax` only: colour de-flicker sensitivity.
1159
+ * Frames whose foreground drifts from the first frame beyond this are
1160
+ * corrected toward it; 0 corrects every frame and higher values correct
1161
+ * fewer. Omit for PixelLab's default.
1162
+ */
1163
+ driftThreshold: z.number().min(0).optional(),
1164
+ /**
1165
+ * `animate-pixminimax` only: one noun phrase for what the parent shows.
1166
+ * PixelLab reads it off the image when omitted, and can decline some
1167
+ * images, failing the job until it is sent.
1168
+ */
1169
+ subjectDescription: z.string().min(1).max(300).optional(),
1170
+ /** `animate-pixminimax` only: the pose the parent is in, read off the image by PixelLab when omitted. */
1171
+ initialPose: z.string().min(1).max(300).optional(),
1172
+ /** `animate-pixminimax` only: facing direction, one of the model's caption fields, so it also holds the sprite's facing. */
1153
1173
  direction: CharacterDirectionSchema.optional(),
1154
1174
  /** `animate`/`animate-pixminimax` only: let PixelLab expand the action into a fuller motion description first. */
1155
1175
  enhancePrompt: z.boolean().optional(),
@@ -1169,12 +1189,15 @@ var RevisionSchema = z.object({
1169
1189
  * same `mannequin`/`bear`/`cat`/`dog`/`horse`/`lion` vocabulary.
1170
1190
  */
1171
1191
  skeletonTemplate: z.string().min(1).optional(),
1192
+ /** `remove-background` only: `simple` is faster and suits flat backgrounds; `complex` handles detailed edges. PixelLab defaults to `simple`. */
1193
+ removalTask: z.enum(["simple", "complex"]).optional(),
1172
1194
  /**
1173
- * `animate-skeleton` only: what the character *looks like*, a noun
1195
+ * `animate-skeleton`: what the character *looks like*, a noun
1174
1196
  * phrase (colours, clothing, held items) — distinct from the asset's
1175
1197
  * own `prompt`, which this mode sends as the motion's short label
1176
1198
  * (PixelLab's `action`), the same `prompt`-as-motion convention
1177
- * `animate`/`animate-pixminimax` already use.
1199
+ * `animate`/`animate-pixminimax` already use. `remove-background`: a
1200
+ * short description of the foreground, which PixelLab uses as a hint.
1178
1201
  */
1179
1202
  description: z.string().min(1).optional(),
1180
1203
  /**
@@ -1291,7 +1314,44 @@ var RevisionSchema = z.object({
1291
1314
  path: ["keypointsFile"]
1292
1315
  });
1293
1316
  }
1294
- for (const field of ["keypointsFile", "skeletonTemplate", "description"]) {
1317
+ if (revision.driftThreshold !== void 0 && revision.mode !== "animate" && revision.mode !== "animate-pixminimax") {
1318
+ context.addIssue({
1319
+ code: z.ZodIssueCode.custom,
1320
+ message: "driftThreshold applies to animate/animate-pixminimax revisions only",
1321
+ path: ["driftThreshold"]
1322
+ });
1323
+ }
1324
+ for (const field of ["subjectDescription", "initialPose"]) {
1325
+ if (revision[field] !== void 0 && revision.mode !== "animate-pixminimax") {
1326
+ context.addIssue({
1327
+ code: z.ZodIssueCode.custom,
1328
+ message: `${field} applies to animate-pixminimax revisions only`,
1329
+ path: [field]
1330
+ });
1331
+ }
1332
+ }
1333
+ if (revision.removalTask !== void 0 && revision.mode !== "remove-background") {
1334
+ context.addIssue({
1335
+ code: z.ZodIssueCode.custom,
1336
+ message: "removalTask applies to remove-background revisions only",
1337
+ path: ["removalTask"]
1338
+ });
1339
+ }
1340
+ if (revision.strength !== void 0 && revision.mode === "remove-background") {
1341
+ context.addIssue({
1342
+ code: z.ZodIssueCode.custom,
1343
+ message: "remove-background revisions do not take a strength",
1344
+ path: ["strength"]
1345
+ });
1346
+ }
1347
+ if (revision.description !== void 0 && revision.mode !== "animate-skeleton" && revision.mode !== "remove-background") {
1348
+ context.addIssue({
1349
+ code: z.ZodIssueCode.custom,
1350
+ message: "description applies to animate-skeleton/remove-background revisions only",
1351
+ path: ["description"]
1352
+ });
1353
+ }
1354
+ for (const field of ["keypointsFile", "skeletonTemplate"]) {
1295
1355
  if (revision[field] !== void 0 && revision.mode !== "animate-skeleton") {
1296
1356
  context.addIssue({
1297
1357
  code: z.ZodIssueCode.custom,
@@ -1945,6 +2005,10 @@ var LockEntrySchema = z.object({
1945
2005
  keypointsSha256: z.string().regex(/^[0-9a-f]{64}$/).optional(),
1946
2006
  skeletonTemplate: z.string().min(1).optional(),
1947
2007
  description: z.string().min(1).optional(),
2008
+ removalTask: z.enum(["simple", "complex"]).optional(),
2009
+ driftThreshold: z.number().min(0).optional(),
2010
+ subjectDescription: z.string().min(1).max(300).optional(),
2011
+ initialPose: z.string().min(1).max(300).optional(),
1948
2012
  engine: z.enum(["pro-flash"]).optional()
1949
2013
  }).strict().nullable().default(null),
1950
2014
  /** For a mirror: the asset it flips and a hash over that asset's output hashes when this was made. */
@@ -5995,6 +6059,7 @@ var CorrectPixelartResponseSchema = z3.object({
5995
6059
  images: z3.array(z3.object({ base64: z3.string().min(1) }).passthrough()).min(1),
5996
6060
  usage: z3.unknown().optional()
5997
6061
  }).passthrough();
6062
+ var RemoveBackgroundResponseSchema = z3.object({ image: z3.object({ base64: z3.string().min(1) }).passthrough(), usage: z3.unknown().optional() }).passthrough();
5998
6063
  var ProFlashImageSubmitSchema = z3.object({
5999
6064
  background_job_id: z3.string().min(1),
6000
6065
  image_id: z3.string().min(1),
@@ -6186,9 +6251,9 @@ var PixelLabClient = class {
6186
6251
  apiKey;
6187
6252
  http;
6188
6253
  /** Retries transport failures, 429, and 5xx; see http.ts for the policy. */
6189
- async request(path50, init) {
6254
+ async request(path51, init) {
6190
6255
  const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
6191
- const res = await this.http(`${BASE}${path50}`, {
6256
+ const res = await this.http(`${BASE}${path51}`, {
6192
6257
  ...init,
6193
6258
  headers: {
6194
6259
  Authorization: auth,
@@ -6198,13 +6263,13 @@ var PixelLabClient = class {
6198
6263
  });
6199
6264
  const text = await res.text();
6200
6265
  if (!res.ok) {
6201
- throw new PixelLabError(`${init?.method ?? "GET"} ${path50} \u2192 ${res.status}`, res.status, text);
6266
+ throw new PixelLabError(`${init?.method ?? "GET"} ${path51} \u2192 ${res.status}`, res.status, text);
6202
6267
  }
6203
6268
  if (!text) return {};
6204
6269
  try {
6205
6270
  return JSON.parse(text);
6206
6271
  } catch {
6207
- throw new Error(`${init?.method ?? "GET"} ${path50} returned invalid JSON`);
6272
+ throw new Error(`${init?.method ?? "GET"} ${path51} returned invalid JSON`);
6208
6273
  }
6209
6274
  }
6210
6275
  async balance() {
@@ -6609,10 +6674,10 @@ var PixelLabClient = class {
6609
6674
  const palette = args.paletteSwatchBase64 ? { color_image: { type: "base64", base64: args.paletteSwatchBase64, format: "png" }, force_colors: true } : {};
6610
6675
  const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
6611
6676
  const south = args.reference?.south;
6612
- let path50;
6677
+ let path51;
6613
6678
  let body;
6614
6679
  if (args.mode === "v3") {
6615
- path50 = "/create-character-v3";
6680
+ path51 = "/create-character-v3";
6616
6681
  body = {
6617
6682
  description: args.description,
6618
6683
  image_size: imageSize,
@@ -6626,7 +6691,7 @@ var PixelLabClient = class {
6626
6691
  ...args.enhancePrompt ? { enhance_prompt: true } : {}
6627
6692
  };
6628
6693
  } else if (args.mode === "pro-flash") {
6629
- path50 = "/create-character-pro-flash";
6694
+ path51 = "/create-character-pro-flash";
6630
6695
  const traits = args.styleTraits;
6631
6696
  body = {
6632
6697
  description: args.description,
@@ -6650,7 +6715,7 @@ var PixelLabClient = class {
6650
6715
  } : {}
6651
6716
  };
6652
6717
  } else if (args.mode === "pro") {
6653
- path50 = "/create-character-pro";
6718
+ path51 = "/create-character-pro";
6654
6719
  body = {
6655
6720
  description: args.description,
6656
6721
  image_size: imageSize,
@@ -6664,7 +6729,7 @@ var PixelLabClient = class {
6664
6729
  ...!south && args.styleCharacterId ? { style_character_id: args.styleCharacterId } : {}
6665
6730
  };
6666
6731
  } else {
6667
- path50 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
6732
+ path51 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
6668
6733
  body = {
6669
6734
  description: args.description,
6670
6735
  image_size: imageSize,
@@ -6681,8 +6746,8 @@ var PixelLabClient = class {
6681
6746
  ...palette
6682
6747
  };
6683
6748
  }
6684
- const raw = await this.request(path50, { method: "POST", body: JSON.stringify(body) });
6685
- return validateResponse(CharacterSubmitSchema, raw, path50.slice(1));
6749
+ const raw = await this.request(path51, { method: "POST", body: JSON.stringify(body) });
6750
+ return validateResponse(CharacterSubmitSchema, raw, path51.slice(1));
6686
6751
  }
6687
6752
  /** A text edit of an existing character, applied to every direction and stored as a sibling. */
6688
6753
  async createCharacterState(args) {
@@ -7059,6 +7124,27 @@ var PixelLabClient = class {
7059
7124
  const pngs = cleanupResults(res.images, images.length, "correct-pixelart");
7060
7125
  return { png: pngs[0], pngs, usage: res.usage };
7061
7126
  }
7127
+ /**
7128
+ * `/remove-background`: a transparent PNG from one pixel-art image,
7129
+ * synchronously, like the rest of the Cleanup tier. The API takes at most
7130
+ * 400x400 worth of area, one image per call. `simple` is faster and suits
7131
+ * flat backgrounds; `complex` is slower and handles detailed edges. The
7132
+ * schema's response example is dollar-denominated (`usd: 0.01`); the
7133
+ * subscription cost measured 1 generation (docs/ENDPOINTS.md), and the
7134
+ * default `simple` task is the one measured.
7135
+ */
7136
+ async removeBackground(args) {
7137
+ const body = { image: args.image, image_size: { width: args.width, height: args.height } };
7138
+ if (args.task) body.background_removal_task = args.task === "complex" ? "remove_complex_background" : "remove_simple_background";
7139
+ if (args.text) body.text = args.text;
7140
+ if (args.seed != null) body.seed = args.seed;
7141
+ const res = validateResponse(
7142
+ RemoveBackgroundResponseSchema,
7143
+ await this.request("/remove-background", { method: "POST", body: JSON.stringify(body) }),
7144
+ "remove-background"
7145
+ );
7146
+ return { png: Buffer.from(res.image.base64, "base64"), usage: res.usage };
7147
+ }
7062
7148
  /**
7063
7149
  * `/unzoom`: recover native-resolution pixel art from an upscaled image (a
7064
7150
  * 32x32 sprite saved at 512x512). Synchronous like the Cleanup tier.
@@ -7167,6 +7253,7 @@ var PixelLabClient = class {
7167
7253
  */
7168
7254
  async animateWithTextV3(args) {
7169
7255
  const body = { first_frame: args.firstFrame, action: args.action };
7256
+ if (args.driftThreshold != null) body.drift_threshold = args.driftThreshold;
7170
7257
  if (args.lastFrame) body.last_frame = args.lastFrame;
7171
7258
  if (args.frameCount != null) body.frame_count = args.frameCount;
7172
7259
  if (args.seed != null) body.seed = args.seed;
@@ -7186,6 +7273,10 @@ var PixelLabClient = class {
7186
7273
  */
7187
7274
  async animatePixminimax(args) {
7188
7275
  const body = { first_frame: args.firstFrame, description: args.description };
7276
+ if (args.view) body.view = args.view;
7277
+ if (args.subjectDescription) body.subject_description = args.subjectDescription;
7278
+ if (args.initialPose) body.initial_pose = args.initialPose;
7279
+ if (args.driftThreshold != null) body.drift_threshold = args.driftThreshold;
7189
7280
  if (args.lastFrame) body.last_frame = args.lastFrame;
7190
7281
  if (args.frameCount != null) body.frame_count = args.frameCount;
7191
7282
  if (args.seed != null) body.seed = args.seed;
@@ -7221,6 +7312,7 @@ var PixelLabClient = class {
7221
7312
  keypoints: args.keypoints,
7222
7313
  direction: args.direction
7223
7314
  };
7315
+ if (args.view) body.view = args.view;
7224
7316
  if (args.templateId) body.template_id = args.templateId;
7225
7317
  if (args.action) body.action = args.action;
7226
7318
  if (args.description) body.description = args.description;
@@ -7532,7 +7624,7 @@ var PixelLabProvider = class _PixelLabProvider {
7532
7624
  * outpaint path yet.
7533
7625
  */
7534
7626
  supportsRevision(mode2) {
7535
- return mode2 === "inpaint" || mode2 === "image-to-image" || mode2 === "reduce-colors" || mode2 === "correct-pixelart" || mode2 === "animate" || mode2 === "animate-pixminimax" || mode2 === "animate-skeleton" || mode2 === "interpolate" || mode2 === "edit-animation";
7627
+ return mode2 === "inpaint" || mode2 === "image-to-image" || mode2 === "reduce-colors" || mode2 === "correct-pixelart" || mode2 === "remove-background" || mode2 === "animate" || mode2 === "animate-pixminimax" || mode2 === "animate-skeleton" || mode2 === "interpolate" || mode2 === "edit-animation";
7536
7628
  }
7537
7629
  /** PixelLab's own constraints: submissions must be >2s apart, and
7538
7630
  * background jobs in flight are capped by subscription tier (Tier 1=8,
@@ -7560,6 +7652,9 @@ var PixelLabProvider = class _PixelLabProvider {
7560
7652
  if (spec.batch?.role === "member") {
7561
7653
  return { unit: "generations", amount: 0, candidates: 1 };
7562
7654
  }
7655
+ if (spec.revision?.mode === "remove-background") {
7656
+ return { unit: "generations", amount: 1, candidates: 1 };
7657
+ }
7563
7658
  if (spec.revision?.mode === "reduce-colors" || spec.revision?.mode === "correct-pixelart") {
7564
7659
  return { unit: "generations", amount: 0.1, candidates: 1 };
7565
7660
  }
@@ -7648,6 +7743,12 @@ var PixelLabProvider = class _PixelLabProvider {
7648
7743
  throw new Error(`PixelLab correct-pixelart source is ${width}x${height}; the API takes at most 1024 pixels per side`);
7649
7744
  }
7650
7745
  }
7746
+ if (spec.revision?.mode === "remove-background") {
7747
+ const { sourceWidth: width, sourceHeight: height } = spec.revision;
7748
+ if (width != null && height != null && (width > 400 || height > 400)) {
7749
+ throw new Error(`PixelLab remove-background source is ${width}x${height}; the API takes at most 400x400`);
7750
+ }
7751
+ }
7651
7752
  if (spec.revision?.mode === "animate-skeleton") {
7652
7753
  const { sourceWidth: width, sourceHeight: height, frames, skeletonTemplate } = spec.revision;
7653
7754
  if (width != null && height != null && (width > 256 || height > 256)) {
@@ -7668,6 +7769,9 @@ var PixelLabProvider = class _PixelLabProvider {
7668
7769
  if (mode2 === "animate" && frames != null && frames > 16) {
7669
7770
  throw new Error(`PixelLab animate takes 4 to 16 frames; ${frames} is too many (use animate-pixminimax for up to 40)`);
7670
7771
  }
7772
+ if (mode2 === "animate-pixminimax" && frames != null && frames % 4 !== 0) {
7773
+ throw new Error(`PixelLab animate-pixminimax takes a multiple of 4 frames (4 to 40); ${frames} is not one`);
7774
+ }
7671
7775
  if (lastFrameWidth != null && lastFrameHeight != null && width != null && height != null && (lastFrameWidth !== width || lastFrameHeight !== height)) {
7672
7776
  throw new Error(
7673
7777
  `PixelLab ${mode2} lastFrame is ${lastFrameWidth}x${lastFrameHeight}; source is ${width}x${height} \u2014 they must match`
@@ -8759,6 +8863,18 @@ var PixelLabProvider = class _PixelLabProvider {
8759
8863
  const res2 = await this.client.correctPixelart({ images, strength: revision.strength });
8760
8864
  return this.cacheCleanupResult(res2.pngs, res2.usage, roles, revision.sourceFps);
8761
8865
  }
8866
+ if (revision.mode === "remove-background") {
8867
+ if (width == null || height == null) throw new Error(`${spec.styleId}/${spec.assetId}: remove-background needs the source's size`);
8868
+ const res2 = await this.client.removeBackground({
8869
+ image,
8870
+ width,
8871
+ height,
8872
+ task: revision.removalTask,
8873
+ text: revision.description,
8874
+ seed: spec.seed
8875
+ });
8876
+ return this.cacheCleanupResult([res2.png], res2.usage, roles, revision.sourceFps);
8877
+ }
8762
8878
  if (revision.mode === "interpolate") {
8763
8879
  const lastFrame = this.readLastFrame(spec);
8764
8880
  if (!lastFrame) throw new Error(`${spec.styleId}/${spec.assetId}: interpolate needs lastFrame, the ending keyframe`);
@@ -8794,7 +8910,8 @@ var PixelLabProvider = class _PixelLabProvider {
8794
8910
  frameCount: revision.frames,
8795
8911
  seed: spec.seed,
8796
8912
  noBackground: spec.noBackground,
8797
- enhancePrompt: revision.enhancePrompt
8913
+ enhancePrompt: revision.enhancePrompt,
8914
+ driftThreshold: revision.driftThreshold
8798
8915
  });
8799
8916
  return { jobId: res3.background_job_id };
8800
8917
  }
@@ -8806,7 +8923,11 @@ var PixelLabProvider = class _PixelLabProvider {
8806
8923
  seed: spec.seed,
8807
8924
  noBackground: spec.noBackground,
8808
8925
  enhancePrompt: revision.enhancePrompt,
8809
- direction: revision.direction
8926
+ direction: revision.direction,
8927
+ ...spec.view === "side" || spec.view === "low top-down" || spec.view === "high top-down" ? { view: spec.view } : {},
8928
+ subjectDescription: revision.subjectDescription,
8929
+ initialPose: revision.initialPose,
8930
+ driftThreshold: revision.driftThreshold
8810
8931
  });
8811
8932
  return { jobId: res2.background_job_id };
8812
8933
  }
@@ -8823,6 +8944,9 @@ var PixelLabProvider = class _PixelLabProvider {
8823
8944
  firstFrameKeypoints: revision.skeleton.firstFrameKeypoints,
8824
8945
  keypoints: revision.skeleton.frames,
8825
8946
  direction: revision.direction,
8947
+ // The style's camera, when it is one this endpoint knows; its enum is
8948
+ // narrower than a style's free-string `view`.
8949
+ ...spec.view === "side" || spec.view === "low top-down" || spec.view === "high top-down" ? { view: spec.view } : {},
8826
8950
  templateId: revision.skeletonTemplate,
8827
8951
  action: spec.prompt,
8828
8952
  description: revision.description,
@@ -8893,7 +9017,7 @@ var PixelLabProvider = class _PixelLabProvider {
8893
9017
  }
8894
9018
  async poll(jobId, generator, context) {
8895
9019
  const revisionMode = context?.spec?.revision?.mode;
8896
- if (revisionMode === "reduce-colors" || revisionMode === "correct-pixelart") {
9020
+ if (revisionMode === "reduce-colors" || revisionMode === "correct-pixelart" || revisionMode === "remove-background") {
8897
9021
  return this.pollCachedRevision(jobId, context);
8898
9022
  }
8899
9023
  if (revisionMode === "animate" || revisionMode === "animate-pixminimax" || revisionMode === "animate-skeleton" || revisionMode === "interpolate" || revisionMode === "edit-animation") {
@@ -9601,9 +9725,9 @@ var RetroDiffusionClient = class {
9601
9725
  if (!response.ok) throw new ProviderError("retrodiffusion", `Retro Diffusion download failed (${response.status})`, { status: response.status });
9602
9726
  return Buffer.from(await response.arrayBuffer());
9603
9727
  }
9604
- async call(path50, init = {}) {
9728
+ async call(path51, init = {}) {
9605
9729
  if (!this.token) throw new Error("RD_API_KEY is not set");
9606
- const response = await this.request(`${this.baseUrl}${path50}`, {
9730
+ const response = await this.request(`${this.baseUrl}${path51}`, {
9607
9731
  ...init,
9608
9732
  headers: {
9609
9733
  "Content-Type": "application/json",
@@ -11201,6 +11325,10 @@ async function resolveSpecs(loaded, filter) {
11201
11325
  } : {},
11202
11326
  ...asset.revision.skeletonTemplate ? { skeletonTemplate: asset.revision.skeletonTemplate } : {},
11203
11327
  ...asset.revision.description ? { description: asset.revision.description } : {},
11328
+ ...asset.revision.removalTask ? { removalTask: asset.revision.removalTask } : {},
11329
+ ...asset.revision.driftThreshold == null ? {} : { driftThreshold: asset.revision.driftThreshold },
11330
+ ...asset.revision.subjectDescription ? { subjectDescription: asset.revision.subjectDescription } : {},
11331
+ ...asset.revision.initialPose ? { initialPose: asset.revision.initialPose } : {},
11204
11332
  ...asset.revision.engine ? { engine: asset.revision.engine } : {}
11205
11333
  };
11206
11334
  }
@@ -11908,10 +12036,10 @@ function isSha256Hash(value) {
11908
12036
  function parseCache(value) {
11909
12037
  return HashCacheSchema.parse(value);
11910
12038
  }
11911
- async function loadCache(path50) {
11912
- if (!existsSync10(path50)) return { version: 1, hashes: {} };
12039
+ async function loadCache(path51) {
12040
+ if (!existsSync10(path51)) return { version: 1, hashes: {} };
11913
12041
  try {
11914
- const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile8(path50, "utf8")));
12042
+ const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile8(path51, "utf8")));
11915
12043
  if (!parsed.success) return { version: 1, hashes: {} };
11916
12044
  return {
11917
12045
  version: 1,
@@ -11923,18 +12051,18 @@ async function loadCache(path50) {
11923
12051
  return { version: 1, hashes: {} };
11924
12052
  }
11925
12053
  }
11926
- async function saveCache(path50, cache) {
12054
+ async function saveCache(path51, cache) {
11927
12055
  const sorted = {};
11928
12056
  for (const key of Object.keys(cache.hashes).sort()) {
11929
12057
  const hash = cache.hashes[key];
11930
12058
  if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
11931
12059
  sorted[key] = hash;
11932
12060
  }
11933
- await mkdir5(pathModule.dirname(pathModule.resolve(path50)), { recursive: true });
11934
- const tmp = `${path50}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
12061
+ await mkdir5(pathModule.dirname(pathModule.resolve(path51)), { recursive: true });
12062
+ const tmp = `${path51}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
11935
12063
  try {
11936
12064
  await writeFile5(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
11937
- await rename5(tmp, path50);
12065
+ await rename5(tmp, path51);
11938
12066
  } finally {
11939
12067
  await rm5(tmp, { force: true });
11940
12068
  }
@@ -13897,6 +14025,7 @@ import { readFile as readFile15, rename as rename7, rm as rm7, writeFile as writ
13897
14025
  import path23 from "path";
13898
14026
  import { z as z6 } from "zod";
13899
14027
  var HexSha = z6.string().regex(/^[0-9a-f]{64}$/);
14028
+ var ProjectFileSchema = z6.string().min(1).refine((file) => !path23.isAbsolute(file) && !file.split(/[\\/]/).includes(".."), "must be a path inside the project");
13900
14029
  var AssetPatchSchema = z6.object({
13901
14030
  prompt: z6.string().optional(),
13902
14031
  /** Per-style override for the style being viewed; null removes it. */
@@ -13945,10 +14074,11 @@ var NewAssetSchema = z6.object({
13945
14074
  styles: z6.array(z6.string().min(1)).optional(),
13946
14075
  /**
13947
14076
  * A controlled regeneration of another asset in the same manifest,
13948
- * instead of a fresh generation. `inpaint` needs a mask upload the
13949
- * gallery does not offer yet, so only `image-to-image` and
13950
- * `animate-skeleton` (whose keypoints file the gallery writes first) can
13951
- * be created here; hand-edit the manifest for the other modes.
14077
+ * instead of a fresh generation. Every mode but `outpaint` can be
14078
+ * created here: `inpaint` names a mask file the gallery's mask editor
14079
+ * wrote first, `interpolate` names an existing image as its ending
14080
+ * keyframe, and `animate-skeleton` names the keypoints file the gallery
14081
+ * wrote first.
13952
14082
  */
13953
14083
  revision: z6.discriminatedUnion("mode", [
13954
14084
  z6.object({
@@ -13957,6 +14087,52 @@ var NewAssetSchema = z6.object({
13957
14087
  from: z6.string().min(1),
13958
14088
  strength: z6.number().min(0).max(1).optional()
13959
14089
  }).strict(),
14090
+ z6.object({
14091
+ mode: z6.literal("reduce-colors"),
14092
+ from: z6.string().min(1),
14093
+ /** A target colour count; the gallery leaves PixelLab's default when this is omitted. */
14094
+ numColors: z6.number().int().min(2).max(256).optional(),
14095
+ dithering: RevisionDitheringSchema.optional(),
14096
+ ditheringStrength: z6.number().min(0).max(10).optional()
14097
+ }).strict(),
14098
+ z6.object({
14099
+ mode: z6.literal("inpaint"),
14100
+ from: z6.string().min(1),
14101
+ /** Black-and-white PNG the same size as the parent; white is repainted. */
14102
+ mask: ProjectFileSchema
14103
+ }).strict(),
14104
+ z6.object({
14105
+ mode: z6.literal("interpolate"),
14106
+ from: z6.string().min(1),
14107
+ /** The ending keyframe: an image already in the project, the parent's size. */
14108
+ lastFrame: ProjectFileSchema,
14109
+ fps: z6.number().int().min(1).max(60).optional()
14110
+ }).strict(),
14111
+ z6.object({ mode: z6.literal("correct-pixelart"), from: z6.string().min(1), strength: z6.number().min(0).max(1).optional() }).strict(),
14112
+ z6.object({
14113
+ mode: z6.literal("remove-background"),
14114
+ from: z6.string().min(1),
14115
+ removalTask: z6.enum(["simple", "complex"]).optional(),
14116
+ /** A short description of the foreground, sent as PixelLab's text hint. */
14117
+ description: z6.string().min(1).optional()
14118
+ }).strict(),
14119
+ z6.object({
14120
+ mode: z6.enum(["animate", "animate-pixminimax"]),
14121
+ from: z6.string().min(1),
14122
+ frames: z6.number().int().min(4).max(40).refine((n) => n % 2 === 0, "frames must be even").optional(),
14123
+ fps: z6.number().int().min(1).max(60).optional(),
14124
+ enhancePrompt: z6.boolean().optional(),
14125
+ driftThreshold: z6.number().min(0).optional(),
14126
+ /** `animate-pixminimax` only; the loader refuses these on `animate`. */
14127
+ direction: CharacterDirectionSchema.optional(),
14128
+ subjectDescription: z6.string().min(1).max(300).optional(),
14129
+ initialPose: z6.string().min(1).max(300).optional()
14130
+ }).strict(),
14131
+ z6.object({
14132
+ mode: z6.literal("edit-animation"),
14133
+ from: z6.string().min(1),
14134
+ fps: z6.number().int().min(1).max(60).optional()
14135
+ }).strict(),
13960
14136
  z6.object({
13961
14137
  mode: z6.literal("animate-skeleton"),
13962
14138
  from: z6.string().min(1),
@@ -14622,19 +14798,19 @@ async function runEdit(args) {
14622
14798
  }
14623
14799
 
14624
14800
  // src/cli/commands/gallery.ts
14625
- import path34 from "path";
14626
- import { existsSync as existsSync26 } from "fs";
14801
+ import path35 from "path";
14802
+ import { existsSync as existsSync27 } from "fs";
14627
14803
 
14628
14804
  // src/gallery/snapshot.ts
14629
14805
  import { createHash as createHash3 } from "crypto";
14630
- import { existsSync as existsSync21 } from "fs";
14631
- import { readFile as readFile19, stat as stat2 } from "fs/promises";
14632
- import path30 from "path";
14806
+ import { existsSync as existsSync22 } from "fs";
14807
+ import { readFile as readFile20, stat as stat2 } from "fs/promises";
14808
+ import path31 from "path";
14633
14809
 
14634
14810
  // src/gallery/edit.ts
14635
- import { readFile as readFile18, rename as rename9, writeFile as writeFile14 } from "fs/promises";
14636
- import path28 from "path";
14637
- import { z as z10 } from "zod";
14811
+ import { readFile as readFile19, rename as rename9, writeFile as writeFile15 } from "fs/promises";
14812
+ import path29 from "path";
14813
+ import { z as z11 } from "zod";
14638
14814
 
14639
14815
  // src/gallery/upload.ts
14640
14816
  import { existsSync as existsSync18 } from "fs";
@@ -14677,50 +14853,106 @@ async function saveProjectImage(root, request) {
14677
14853
  return relative.split(path26.sep).join("/");
14678
14854
  }
14679
14855
 
14680
- // src/gallery/skeleton.ts
14856
+ // src/gallery/image-revision.ts
14681
14857
  import { existsSync as existsSync19 } from "fs";
14682
- import { mkdir as mkdir11, readFile as readFile17, rm as rm9, writeFile as writeFile13 } from "fs/promises";
14858
+ import { readFile as readFile17, rm as rm9, writeFile as writeFile13 } from "fs/promises";
14683
14859
  import path27 from "path";
14684
14860
  import { z as z9 } from "zod";
14685
- var SkeletonEstimateRequestSchema = z9.object({
14686
- project: z9.string().min(1).optional(),
14687
- styleId: z9.string().min(1),
14688
- assetId: z9.string().min(1),
14689
- frames: z9.number().int().min(3).max(15).optional()
14690
- }).strict();
14691
- var SkeletonAnimationRequestSchema = z9.object({
14692
- action: z9.literal("create-skeleton-animation"),
14861
+ var InpaintRevisionRequestSchema = z9.object({
14862
+ action: z9.literal("create-inpaint-revision"),
14693
14863
  project: z9.string().min(1).optional(),
14694
14864
  expectedSha256: z9.string().regex(/^[0-9a-f]{64}$/),
14695
14865
  styleId: z9.string().min(1),
14696
- /** The sprite the animation poses. */
14866
+ /** The sprite being repainted. */
14697
14867
  from: z9.string().min(1),
14698
14868
  assetId: z9.string().min(1).regex(/^[^/\\]+$/, "asset ids cannot contain slashes"),
14699
- /** The motion, sent as PixelLab's `action`. */
14869
+ /** What to paint into the masked area. */
14700
14870
  prompt: z9.string().min(1),
14871
+ /** Manifest-relative `.png` destination for the mask. */
14872
+ maskPath: z9.string().min(1),
14873
+ maskBase64: z9.string().regex(/^[A-Za-z0-9+/]*={0,2}$/),
14874
+ /** Replace a file already at `maskPath`. */
14875
+ overwrite: z9.boolean().optional()
14876
+ }).strict();
14877
+ async function createInpaintRevision(manifestPath, request) {
14878
+ const root = path27.dirname(path27.resolve(manifestPath));
14879
+ if (path27.extname(request.maskPath).toLowerCase() !== ".png") {
14880
+ throw new ManifestEditError("the mask must be a .png path inside the project");
14881
+ }
14882
+ const bytes = Buffer.from(request.maskBase64, "base64");
14883
+ const metadata = imageMetadata(bytes);
14884
+ if (!metadata || metadata.format !== "png") throw new ManifestEditError("the mask is not a readable PNG");
14885
+ const file = path27.resolve(root, request.maskPath);
14886
+ const previous = existsSync19(file) ? await readFile17(file) : null;
14887
+ const mask = await saveProjectImage(root, {
14888
+ action: "upload-image",
14889
+ path: request.maskPath,
14890
+ base64: request.maskBase64,
14891
+ ...request.overwrite ? { overwrite: true } : {}
14892
+ });
14893
+ try {
14894
+ await applyManifestEdit(manifestPath, {
14895
+ action: "add-asset",
14896
+ assetId: request.assetId,
14897
+ expectedSha256: request.expectedSha256,
14898
+ asset: {
14899
+ prompt: request.prompt,
14900
+ styles: [request.styleId],
14901
+ revision: { mode: "inpaint", from: request.from, mask }
14902
+ }
14903
+ });
14904
+ } catch (error) {
14905
+ if (previous) await writeFile13(file, previous);
14906
+ else await rm9(file, { force: true });
14907
+ throw error;
14908
+ }
14909
+ return mask;
14910
+ }
14911
+
14912
+ // src/gallery/skeleton.ts
14913
+ import { existsSync as existsSync20 } from "fs";
14914
+ import { mkdir as mkdir11, readFile as readFile18, rm as rm10, writeFile as writeFile14 } from "fs/promises";
14915
+ import path28 from "path";
14916
+ import { z as z10 } from "zod";
14917
+ var SkeletonEstimateRequestSchema = z10.object({
14918
+ project: z10.string().min(1).optional(),
14919
+ styleId: z10.string().min(1),
14920
+ assetId: z10.string().min(1),
14921
+ frames: z10.number().int().min(3).max(15).optional()
14922
+ }).strict();
14923
+ var SkeletonAnimationRequestSchema = z10.object({
14924
+ action: z10.literal("create-skeleton-animation"),
14925
+ project: z10.string().min(1).optional(),
14926
+ expectedSha256: z10.string().regex(/^[0-9a-f]{64}$/),
14927
+ styleId: z10.string().min(1),
14928
+ /** The sprite the animation poses. */
14929
+ from: z10.string().min(1),
14930
+ assetId: z10.string().min(1).regex(/^[^/\\]+$/, "asset ids cannot contain slashes"),
14931
+ /** The motion, sent as PixelLab's `action`. */
14932
+ prompt: z10.string().min(1),
14701
14933
  direction: CharacterDirectionSchema,
14702
- description: z9.string().min(1).optional(),
14703
- skeletonTemplate: z9.string().min(1).optional(),
14934
+ description: z10.string().min(1).optional(),
14935
+ skeletonTemplate: z10.string().min(1).optional(),
14704
14936
  /** Manifest-relative JSON path inside the project. */
14705
- keypointsFile: z9.string().min(1),
14937
+ keypointsFile: z10.string().min(1),
14706
14938
  /** Poses to write to `keypointsFile`; omitted to use a file that already exists. */
14707
14939
  set: SkeletonSetSchema.optional(),
14708
14940
  /** Replace an existing `keypointsFile` with `set`. */
14709
- overwrite: z9.boolean().optional()
14941
+ overwrite: z10.boolean().optional()
14710
14942
  }).strict();
14711
- var SkeletonKeypointsUpdateSchema = z9.object({
14712
- action: z9.literal("update-skeleton-keypoints"),
14713
- project: z9.string().min(1).optional(),
14714
- styleId: z9.string().min(1),
14715
- assetId: z9.string().min(1),
14943
+ var SkeletonKeypointsUpdateSchema = z10.object({
14944
+ action: z10.literal("update-skeleton-keypoints"),
14945
+ project: z10.string().min(1).optional(),
14946
+ styleId: z10.string().min(1),
14947
+ assetId: z10.string().min(1),
14716
14948
  /** The file's SHA-256 when the page loaded it; anything else on disk is a concurrent edit. */
14717
- expectedSha256: z9.string().regex(/^[0-9a-f]{64}$/),
14949
+ expectedSha256: z10.string().regex(/^[0-9a-f]{64}$/),
14718
14950
  set: SkeletonSetSchema
14719
14951
  }).strict();
14720
14952
  var DEFAULT_ESTIMATE_LIMIT = 10;
14721
14953
  function skeletonSourceFile(spec) {
14722
- if (spec.source) return path27.resolve(spec.root, spec.source);
14723
- if (spec.quality && existsSync19(spec.quality.outFile)) return spec.quality.outFile;
14954
+ if (spec.source) return path28.resolve(spec.root, spec.source);
14955
+ if (spec.quality && existsSync20(spec.quality.outFile)) return spec.quality.outFile;
14724
14956
  return spec.outFile;
14725
14957
  }
14726
14958
  function createGallerySkeletonHandlers(opts) {
@@ -14741,10 +14973,10 @@ function createGallerySkeletonHandlers(opts) {
14741
14973
  const spec = ctx.specs.find((candidate) => lockKey(candidate.styleId, candidate.assetId) === key);
14742
14974
  if (!spec) throw new ManifestEditError(`"${key}" is not declared by the manifest`);
14743
14975
  const file = skeletonSourceFile(spec);
14744
- if (!existsSync19(file)) throw new ManifestEditError(`${key} has no image on disk to estimate from`);
14745
- const bytes = await readFile17(file);
14976
+ if (!existsSync20(file)) throw new ManifestEditError(`${key} has no image on disk to estimate from`);
14977
+ const bytes = await readFile18(file);
14746
14978
  const metadata = imageMetadata(bytes);
14747
- if (!metadata) throw new ManifestEditError(`${path27.basename(file)} is not a readable PNG or JPEG`);
14979
+ if (!metadata) throw new ManifestEditError(`${path28.basename(file)} is not a readable PNG or JPEG`);
14748
14980
  const problem = estimateSkeletonSizeProblem(metadata.width, metadata.height);
14749
14981
  if (problem) throw new ManifestEditError(`${key} is ${problem}`);
14750
14982
  if (used >= limit) {
@@ -14761,7 +14993,7 @@ function createGallerySkeletonHandlers(opts) {
14761
14993
  log2(` estimated a skeleton for ${key} (PixelLab estimate-skeleton, ${used}/${limit} this session${billed ? `, billed ${billed.amount} ${billed.unit}` : ""})`);
14762
14994
  return {
14763
14995
  set: scaffoldSkeletonSet(res.keypoints, request.frames ?? DEFAULT_SCAFFOLD_FRAMES),
14764
- source: path27.relative(ctx.loaded.root, file).split(path27.sep).join("/"),
14996
+ source: path28.relative(ctx.loaded.root, file).split(path28.sep).join("/"),
14765
14997
  session: status()
14766
14998
  };
14767
14999
  }
@@ -14775,21 +15007,21 @@ function billedAmount(usage) {
14775
15007
  return typeof amount === "number" && Number.isFinite(amount) ? { unit: type, amount } : null;
14776
15008
  }
14777
15009
  async function createSkeletonAnimation(manifestPath, request) {
14778
- const root = path27.dirname(path27.resolve(manifestPath));
14779
- const file = path27.resolve(root, request.keypointsFile);
14780
- const relative = path27.relative(root, file);
14781
- if (!request.keypointsFile.endsWith(".json") || relative.startsWith("..") || path27.isAbsolute(relative)) {
15010
+ const root = path28.dirname(path28.resolve(manifestPath));
15011
+ const file = path28.resolve(root, request.keypointsFile);
15012
+ const relative = path28.relative(root, file);
15013
+ if (!request.keypointsFile.endsWith(".json") || relative.startsWith("..") || path28.isAbsolute(relative)) {
14782
15014
  throw new ManifestEditError("the keypoints file must be a .json path inside the project");
14783
15015
  }
14784
- const existed = existsSync19(file);
15016
+ const existed = existsSync20(file);
14785
15017
  let previous = null;
14786
15018
  if (request.set) {
14787
15019
  if (existed && !request.overwrite) {
14788
15020
  throw new ManifestEditError(`${request.keypointsFile} already exists; choose another path, or replace it`);
14789
15021
  }
14790
- if (existed) previous = await readFile17(file);
14791
- await mkdir11(path27.dirname(file), { recursive: true });
14792
- await writeFile13(file, JSON.stringify(request.set, null, 2) + "\n");
15022
+ if (existed) previous = await readFile18(file);
15023
+ await mkdir11(path28.dirname(file), { recursive: true });
15024
+ await writeFile14(file, JSON.stringify(request.set, null, 2) + "\n");
14793
15025
  } else if (!existed) {
14794
15026
  throw new ManifestEditError(`${request.keypointsFile} does not exist; paste poses or estimate them from the sprite`);
14795
15027
  }
@@ -14804,7 +15036,7 @@ async function createSkeletonAnimation(manifestPath, request) {
14804
15036
  revision: {
14805
15037
  mode: "animate-skeleton",
14806
15038
  from: request.from,
14807
- keypointsFile: relative.split(path27.sep).join("/"),
15039
+ keypointsFile: relative.split(path28.sep).join("/"),
14808
15040
  direction: request.direction,
14809
15041
  ...request.description ? { description: request.description } : {},
14810
15042
  ...request.skeletonTemplate ? { skeletonTemplate: request.skeletonTemplate } : {}
@@ -14813,8 +15045,8 @@ async function createSkeletonAnimation(manifestPath, request) {
14813
15045
  });
14814
15046
  } catch (error) {
14815
15047
  if (request.set) {
14816
- if (previous) await writeFile13(file, previous);
14817
- else await rm9(file, { force: true });
15048
+ if (previous) await writeFile14(file, previous);
15049
+ else await rm10(file, { force: true });
14818
15050
  }
14819
15051
  throw error;
14820
15052
  }
@@ -14825,64 +15057,64 @@ async function updateSkeletonKeypoints(ctx, request) {
14825
15057
  if (!spec) throw new ManifestEditError(`"${key}" is not declared by the manifest`);
14826
15058
  const file = spec.revision?.mode === "animate-skeleton" ? spec.revision.keypointsFile : void 0;
14827
15059
  if (!file) throw new ManifestEditError(`${key} is not an animate-skeleton revision`);
14828
- const relative = path27.relative(ctx.loaded.root, file);
14829
- if (relative.startsWith("..") || path27.isAbsolute(relative)) {
15060
+ const relative = path28.relative(ctx.loaded.root, file);
15061
+ if (relative.startsWith("..") || path28.isAbsolute(relative)) {
14830
15062
  throw new ManifestEditError(`${key}'s keypoints file is outside the project; edit it by hand`);
14831
15063
  }
14832
- const current = existsSync19(file) ? sha256(await readFile17(file)) : null;
15064
+ const current = existsSync20(file) ? sha256(await readFile18(file)) : null;
14833
15065
  if (current !== request.expectedSha256) {
14834
15066
  throw Object.assign(
14835
- new Error(`${relative.split(path27.sep).join("/")} changed on disk since this page loaded it; press Refresh and edit again`),
15067
+ new Error(`${relative.split(path28.sep).join("/")} changed on disk since this page loaded it; press Refresh and edit again`),
14836
15068
  { status: 409 }
14837
15069
  );
14838
15070
  }
14839
- await writeFile13(file, JSON.stringify(request.set, null, 2) + "\n");
14840
- return relative.split(path27.sep).join("/");
15071
+ await writeFile14(file, JSON.stringify(request.set, null, 2) + "\n");
15072
+ return relative.split(path28.sep).join("/");
14841
15073
  }
14842
15074
 
14843
15075
  // src/gallery/edit.ts
14844
- var Base64 = z10.string().regex(/^[A-Za-z0-9+/]*={0,2}$/).max(Math.ceil(MAX_HAND_EDIT_BYTES / 3) * 4);
14845
- var HandEditRequestSchema = z10.discriminatedUnion("action", [
14846
- z10.object({
14847
- action: z10.literal("start-edit"),
14848
- project: z10.string().min(1).optional(),
14849
- assetId: z10.string().min(1),
14850
- styleId: z10.string().min(1),
14851
- expectedSha256: z10.string().regex(/^[0-9a-f]{64}$/),
15076
+ var Base64 = z11.string().regex(/^[A-Za-z0-9+/]*={0,2}$/).max(Math.ceil(MAX_HAND_EDIT_BYTES / 3) * 4);
15077
+ var HandEditRequestSchema = z11.discriminatedUnion("action", [
15078
+ z11.object({
15079
+ action: z11.literal("start-edit"),
15080
+ project: z11.string().min(1).optional(),
15081
+ assetId: z11.string().min(1),
15082
+ styleId: z11.string().min(1),
15083
+ expectedSha256: z11.string().regex(/^[0-9a-f]{64}$/),
14852
15084
  /** Also launch the author's editor on the file. */
14853
- open: z10.boolean().optional()
15085
+ open: z11.boolean().optional()
14854
15086
  }).strict(),
14855
- z10.object({
14856
- action: z10.literal("detach-edit"),
14857
- project: z10.string().min(1).optional(),
14858
- assetId: z10.string().min(1),
14859
- styleId: z10.string().min(1),
14860
- expectedSha256: z10.string().regex(/^[0-9a-f]{64}$/)
15087
+ z11.object({
15088
+ action: z11.literal("detach-edit"),
15089
+ project: z11.string().min(1).optional(),
15090
+ assetId: z11.string().min(1),
15091
+ styleId: z11.string().min(1),
15092
+ expectedSha256: z11.string().regex(/^[0-9a-f]{64}$/)
14861
15093
  }).strict(),
14862
15094
  /** From the in-browser editor: the page holds the bytes, the server validates and writes. */
14863
- z10.object({
14864
- action: z10.literal("save-edit"),
14865
- project: z10.string().min(1).optional(),
14866
- assetId: z10.string().min(1),
14867
- styleId: z10.string().min(1),
14868
- expectedSha256: z10.string().regex(/^[0-9a-f]{64}$/),
15095
+ z11.object({
15096
+ action: z11.literal("save-edit"),
15097
+ project: z11.string().min(1).optional(),
15098
+ assetId: z11.string().min(1),
15099
+ styleId: z11.string().min(1),
15100
+ expectedSha256: z11.string().regex(/^[0-9a-f]{64}$/),
14869
15101
  /** The flattened image; for a frame set, `frames` instead. */
14870
15102
  png: Base64.min(1).optional(),
14871
15103
  /** Every frame of a set, tagged with the role it was opened under (null for one added in the editor). */
14872
- frames: z10.array(z10.object({ role: z10.string().min(1).nullable(), png: Base64.min(1) }).strict()).min(1).max(256).optional(),
15104
+ frames: z11.array(z11.object({ role: z11.string().min(1).nullable(), png: Base64.min(1) }).strict()).min(1).max(256).optional(),
14873
15105
  /** The editor's layered project file, kept beside the edit. */
14874
15106
  pxo: Base64.optional(),
14875
15107
  /** Editor identity from the bridge's ready message. */
14876
- editor: z10.string().min(1).max(200),
14877
- protocol: z10.number().int().positive()
15108
+ editor: z11.string().min(1).max(200),
15109
+ protocol: z11.number().int().positive()
14878
15110
  }).strict()
14879
15111
  ]);
14880
15112
  var UNDO_DEPTH = 20;
14881
- var UndoRequestSchema = z10.object({
14882
- action: z10.literal("undo-edit"),
14883
- project: z10.string().min(1).optional(),
15113
+ var UndoRequestSchema = z11.object({
15114
+ action: z11.literal("undo-edit"),
15115
+ project: z11.string().min(1).optional(),
14884
15116
  /** The manifest as the page last saw it; undo refuses a manifest someone else has changed since. */
14885
- expectedSha256: z10.string().regex(/^[0-9a-f]{64}$/)
15117
+ expectedSha256: z11.string().regex(/^[0-9a-f]{64}$/)
14886
15118
  }).strict();
14887
15119
  function createGalleryEditHandler(opts) {
14888
15120
  const log2 = opts.onProgress ?? (() => {
@@ -14891,7 +15123,7 @@ function createGalleryEditHandler(opts) {
14891
15123
  const history = /* @__PURE__ */ new Map();
14892
15124
  const manifestOf = async (project) => {
14893
15125
  try {
14894
- return path28.resolve(await opts.manifestFor(typeof project === "string" ? project : void 0));
15126
+ return path29.resolve(await opts.manifestFor(typeof project === "string" ? project : void 0));
14895
15127
  } catch {
14896
15128
  return null;
14897
15129
  }
@@ -14901,7 +15133,7 @@ function createGalleryEditHandler(opts) {
14901
15133
  if (undo.success) {
14902
15134
  const manifestPath2 = await manifestOf(undo.data.project);
14903
15135
  if (!manifestPath2) throw new ManifestEditError("no manifest to undo an edit in");
14904
- const current = await readFile18(manifestPath2, "utf8");
15136
+ const current = await readFile19(manifestPath2, "utf8");
14905
15137
  if (sha256(current) !== undo.data.expectedSha256) throw new ManifestDriftError(manifestPath2);
14906
15138
  const stack = history.get(manifestPath2) ?? [];
14907
15139
  const last = stack[stack.length - 1];
@@ -14911,17 +15143,17 @@ function createGalleryEditHandler(opts) {
14911
15143
  throw Object.assign(new Error("the manifest changed outside the gallery since its last edit; undo would discard that change"), { status: 409 });
14912
15144
  }
14913
15145
  const tmp = manifestPath2 + ".pixelkiln-undo";
14914
- await writeFile14(tmp, last.before);
15146
+ await writeFile15(tmp, last.before);
14915
15147
  await rename9(tmp, manifestPath2);
14916
15148
  stack.pop();
14917
- log2(` manifest edit undone in ${path28.relative(process.cwd(), manifestPath2) || manifestPath2}`);
15149
+ log2(` manifest edit undone in ${path29.relative(process.cwd(), manifestPath2) || manifestPath2}`);
14918
15150
  return opts.reload();
14919
15151
  }
14920
15152
  const manifestPath = await manifestOf(body?.project);
14921
- const before = manifestPath ? await readFile18(manifestPath, "utf8").catch(() => null) : null;
15153
+ const before = manifestPath ? await readFile19(manifestPath, "utf8").catch(() => null) : null;
14922
15154
  const build = await apply(body);
14923
15155
  if (manifestPath && before !== null) {
14924
- const after = await readFile18(manifestPath, "utf8").catch(() => null);
15156
+ const after = await readFile19(manifestPath, "utf8").catch(() => null);
14925
15157
  if (after !== null && after !== before) {
14926
15158
  const stack = history.get(manifestPath) ?? [];
14927
15159
  stack.push({ before, afterSha: sha256(after) });
@@ -14976,7 +15208,7 @@ function createEditApplier(opts, log2) {
14976
15208
  } catch (error) {
14977
15209
  throw new ManifestEditError(error instanceof Error ? error.message : String(error));
14978
15210
  }
14979
- const written = await saveProjectImage(path28.dirname(path28.resolve(manifestPath2)), upload.data);
15211
+ const written = await saveProjectImage(path29.dirname(path29.resolve(manifestPath2)), upload.data);
14980
15212
  log2(` image added: ${written}`);
14981
15213
  return opts.reload();
14982
15214
  }
@@ -15012,6 +15244,23 @@ function createEditApplier(opts, log2) {
15012
15244
  "invalid skeleton animation: " + skeleton.error.issues.slice(0, 3).map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")
15013
15245
  );
15014
15246
  }
15247
+ const inpaint = InpaintRevisionRequestSchema.safeParse(body);
15248
+ if (inpaint.success) {
15249
+ let manifestPath2;
15250
+ try {
15251
+ manifestPath2 = await opts.manifestFor(inpaint.data.project);
15252
+ } catch (error) {
15253
+ throw new ManifestEditError(error instanceof Error ? error.message : String(error));
15254
+ }
15255
+ const mask = await createInpaintRevision(manifestPath2, inpaint.data);
15256
+ log2(` manifest edited: added inpaint revision ${inpaint.data.assetId} (mask ${mask})`);
15257
+ return opts.reload();
15258
+ }
15259
+ if (body?.action === "create-inpaint-revision") {
15260
+ throw new ManifestEditError(
15261
+ "invalid inpaint revision: " + inpaint.error.issues.slice(0, 3).map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")
15262
+ );
15263
+ }
15015
15264
  const parsed = ManifestEditSchema.safeParse(body);
15016
15265
  if (!parsed.success) {
15017
15266
  throw new ManifestEditError(
@@ -15026,7 +15275,7 @@ function createEditApplier(opts, log2) {
15026
15275
  }
15027
15276
  const result = await applyManifestEdit(manifestPath, parsed.data);
15028
15277
  if (result.changed) {
15029
- const relative = path28.relative(process.cwd(), result.manifestPath);
15278
+ const relative = path29.relative(process.cwd(), result.manifestPath);
15030
15279
  const shown = !relative || relative.startsWith("..") ? result.manifestPath : relative;
15031
15280
  log2(` manifest edited: ${describeEdit(parsed.data)} in ${shown}`);
15032
15281
  }
@@ -15077,8 +15326,8 @@ function createGalleryPriceHandler(opts) {
15077
15326
  }
15078
15327
 
15079
15328
  // src/pipeline/history.ts
15080
- import { existsSync as existsSync20 } from "fs";
15081
- import path29 from "path";
15329
+ import { existsSync as existsSync21 } from "fs";
15330
+ import path30 from "path";
15082
15331
  var HISTORY_ENV = "PIXELKILN_HISTORY";
15083
15332
  function historyLimit(manifest, env = process.env) {
15084
15333
  if (manifest?.history !== void 0) return manifest.history;
@@ -15165,10 +15414,10 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
15165
15414
  }
15166
15415
  for (let i = 0; i < entry.outputs.length; i++) {
15167
15416
  const file = currentEntryOutputPath(entry, spec, i);
15168
- if (!existsSync20(file)) continue;
15417
+ if (!existsSync21(file)) continue;
15169
15418
  if (await sha256File(file) !== entry.outputs[i].sha256 && !opts.force) {
15170
15419
  throw new OverwriteRefusedError(
15171
- `${path29.relative(process.cwd(), file)} was changed after download; keep it with pixelkiln edit, or pass --force to replace it`,
15420
+ `${path30.relative(process.cwd(), file)} was changed after download; keep it with pixelkiln edit, or pass --force to replace it`,
15172
15421
  [file]
15173
15422
  );
15174
15423
  }
@@ -15221,7 +15470,7 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
15221
15470
  // src/gallery/snapshot.ts
15222
15471
  var PROJECT_FILE_TYPE = "application/octet-stream";
15223
15472
  function galleryMediaId(absolutePath) {
15224
- return createHash3("sha256").update(path30.resolve(absolutePath)).digest("hex").slice(0, 24);
15473
+ return createHash3("sha256").update(path31.resolve(absolutePath)).digest("hex").slice(0, 24);
15225
15474
  }
15226
15475
  function galleryMediaRoute(id) {
15227
15476
  return `/media/${id}`;
@@ -15240,9 +15489,9 @@ function describeHistory(media, entry, cacheDir) {
15240
15489
  return (entry.history ?? []).map((generation, i) => {
15241
15490
  const outputs = generation.outputs.map((output) => {
15242
15491
  const mediaType = output.mediaType ?? MediaType.PNG;
15243
- const file = path30.join(cacheDir, cacheFileName(output.sha256, mediaType));
15492
+ const file = path31.join(cacheDir, cacheFileName(output.sha256, mediaType));
15244
15493
  let url = null;
15245
- if (existsSync21(file)) {
15494
+ if (existsSync22(file)) {
15246
15495
  const id = galleryMediaId(file);
15247
15496
  media.set(id, { path: file, contentType: mediaType });
15248
15497
  url = `${galleryMediaRoute(id)}?v=${output.sha256.slice(0, 24)}`;
@@ -15274,7 +15523,7 @@ function describeHistory(media, entry, cacheDir) {
15274
15523
  var MAX_COMPARED_BYTES = 8 * 1024 * 1024;
15275
15524
  async function samePixels(a, b) {
15276
15525
  try {
15277
- const [left, right] = await Promise.all([readFile19(a), readFile19(b)]);
15526
+ const [left, right] = await Promise.all([readFile20(a), readFile20(b)]);
15278
15527
  if (left.length > MAX_COMPARED_BYTES || right.length > MAX_COMPARED_BYTES) return false;
15279
15528
  const x = decodePng(left);
15280
15529
  const y = decodePng(right);
@@ -15372,10 +15621,10 @@ async function describeOutput(media, root, absolutePath, recorded = {}) {
15372
15621
  async function describeQuality(media, root, inspection) {
15373
15622
  const outputPaths = inspection.outputs ?? [inspection.output];
15374
15623
  const outputs = await Promise.all(
15375
- outputPaths.map((file) => describeOutput(media, root, path30.resolve(root, file)))
15624
+ outputPaths.map((file) => describeOutput(media, root, path31.resolve(root, file)))
15376
15625
  );
15377
- const recordPath = path30.resolve(root, inspection.record);
15378
- const recordExists = existsSync21(recordPath);
15626
+ const recordPath = path31.resolve(root, inspection.record);
15627
+ const recordExists = existsSync22(recordPath);
15379
15628
  let options = null;
15380
15629
  let check = null;
15381
15630
  if (recordExists) {
@@ -15394,7 +15643,7 @@ async function describeQuality(media, root, inspection) {
15394
15643
  return {
15395
15644
  state: inspection.state,
15396
15645
  reason: inspection.reason,
15397
- output: portableOutputPath(path30.resolve(root, inspection.output), root),
15646
+ output: portableOutputPath(path31.resolve(root, inspection.output), root),
15398
15647
  record: portableOutputPath(recordPath, root),
15399
15648
  recordExists,
15400
15649
  outputs,
@@ -15432,9 +15681,9 @@ async function buildGallerySnapshot(opts) {
15432
15681
  const media = /* @__PURE__ */ new Map();
15433
15682
  const plan = await buildPlan(specs, lock);
15434
15683
  const items = [];
15435
- const manifestText = await readFile19(loaded.path, "utf8");
15684
+ const manifestText = await readFile20(loaded.path, "utf8");
15436
15685
  const manifestSha256 = sha256(manifestText);
15437
- const cacheDir = path30.resolve(path30.dirname(opts.lockPath), ".pixelkiln", "cache");
15686
+ const cacheDir = path31.resolve(path31.dirname(opts.lockPath), ".pixelkiln", "cache");
15438
15687
  let rawStyles = {};
15439
15688
  try {
15440
15689
  const raw = JSON.parse(manifestText);
@@ -15458,12 +15707,12 @@ async function buildGallerySnapshot(opts) {
15458
15707
  )
15459
15708
  );
15460
15709
  if (spec.source) {
15461
- const editPath = path30.resolve(root, spec.source);
15710
+ const editPath = path31.resolve(root, spec.source);
15462
15711
  const memberCount = sourceIsStem(spec.source, entry, root) ? entry.outputs.length : 1;
15463
15712
  const editShas = await Promise.all(
15464
15713
  Array.from({ length: memberCount }, async (_, index) => {
15465
15714
  const file = sourceOutputPath(spec.source, entry, index, root);
15466
- return { file, sha256: existsSync21(file) ? await sha256File(file) : null };
15715
+ return { file, sha256: existsSync22(file) ? await sha256File(file) : null };
15467
15716
  })
15468
15717
  );
15469
15718
  edits = await Promise.all(editShas.map(({ file, sha256: editSha }) => describeOutput(media, root, file, { sha256: editSha })));
@@ -15471,7 +15720,7 @@ async function buildGallerySnapshot(opts) {
15471
15720
  const companion = await readHandEditCompanion(editPath);
15472
15721
  if (companion) {
15473
15722
  const projectPath = handEditProjectPath(editPath);
15474
- const project = companion.project && existsSync21(projectPath) ? await fileInfo(projectPath) : null;
15723
+ const project = companion.project && existsSync22(projectPath) ? await fileInfo(projectPath) : null;
15475
15724
  let projectUrl = null;
15476
15725
  if (project) {
15477
15726
  const id = galleryMediaId(projectPath);
@@ -15507,8 +15756,8 @@ async function buildGallerySnapshot(opts) {
15507
15756
  editStatus = edits.some((member) => !member.exists) ? "missing" : editChanged.every((changed) => !changed) ? "same" : regenerated ? "regenerated-since" : "edited";
15508
15757
  }
15509
15758
  } else if (spec.source) {
15510
- outputs = [await describeOutput(media, root, path30.resolve(root, spec.source))];
15511
- } else if (!entry && existsSync21(spec.outFile)) {
15759
+ outputs = [await describeOutput(media, root, path31.resolve(root, spec.source))];
15760
+ } else if (!entry && existsSync22(spec.outFile)) {
15512
15761
  outputs = [await describeOutput(media, root, spec.outFile)];
15513
15762
  } else {
15514
15763
  outputs = [];
@@ -15711,7 +15960,7 @@ async function buildGallerySnapshot(opts) {
15711
15960
  id: loaded.manifest.name,
15712
15961
  name: loaded.manifest.name,
15713
15962
  manifest: loaded.path,
15714
- lock: path30.resolve(opts.lockPath),
15963
+ lock: path31.resolve(opts.lockPath),
15715
15964
  root,
15716
15965
  provider: loaded.manifest.provider,
15717
15966
  account: null,
@@ -15737,7 +15986,7 @@ async function buildGallerySnapshot(opts) {
15737
15986
  return { snapshot, media };
15738
15987
  }
15739
15988
  async function buildWorkspaceGallerySnapshot(opts) {
15740
- const dir = path30.dirname(path30.resolve(opts.workspacePath));
15989
+ const dir = path31.dirname(path31.resolve(opts.workspacePath));
15741
15990
  const media = /* @__PURE__ */ new Map();
15742
15991
  const projects = [];
15743
15992
  const items = [];
@@ -15780,11 +16029,11 @@ async function buildWorkspaceGallerySnapshot(opts) {
15780
16029
  id: project.id,
15781
16030
  name: loaded.manifest.name,
15782
16031
  manifest: manifestPath,
15783
- lock: path30.resolve(lockPath),
16032
+ lock: path31.resolve(lockPath),
15784
16033
  root: loaded.root,
15785
16034
  provider: loaded.manifest.provider,
15786
16035
  account: project.account ?? null,
15787
- manifestSha256: manifestSha256 ?? sha256(await readFile19(manifestPath, "utf8")),
16036
+ manifestSha256: manifestSha256 ?? sha256(await readFile20(manifestPath, "utf8")),
15788
16037
  historyLimit: historyLimit(loaded.manifest),
15789
16038
  entries: Object.keys(lock.entries).length,
15790
16039
  items: projectItems.length,
@@ -15796,8 +16045,8 @@ async function buildWorkspaceGallerySnapshot(opts) {
15796
16045
  id: project.id,
15797
16046
  name: project.id,
15798
16047
  manifest: manifestPath,
15799
- lock: path30.resolve(lockPath),
15800
- root: path30.dirname(manifestPath),
16048
+ lock: path31.resolve(lockPath),
16049
+ root: path31.dirname(manifestPath),
15801
16050
  provider: project.provider,
15802
16051
  account: project.account ?? null,
15803
16052
  manifestSha256: null,
@@ -15824,7 +16073,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
15824
16073
  version: 1,
15825
16074
  generatedAt: (opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
15826
16075
  project: null,
15827
- workspace: { path: path30.resolve(opts.workspacePath), dir, projects },
16076
+ workspace: { path: path31.resolve(opts.workspacePath), dir, projects },
15828
16077
  filter,
15829
16078
  totals: {
15830
16079
  entries: projects.reduce((sum, project) => sum + project.entries, 0),
@@ -15843,10 +16092,10 @@ async function buildWorkspaceGallerySnapshot(opts) {
15843
16092
  // src/gallery/server.ts
15844
16093
  import { randomBytes } from "crypto";
15845
16094
  import { createServer as createServer2 } from "http";
15846
- import { readFile as readFile20 } from "fs/promises";
16095
+ import { readFile as readFile21 } from "fs/promises";
15847
16096
 
15848
16097
  // src/gallery/page.ts
15849
- import { existsSync as existsSync22, readFileSync as readFileSync4 } from "fs";
16098
+ import { existsSync as existsSync23, readFileSync as readFileSync4 } from "fs";
15850
16099
 
15851
16100
  // src/gallery/client-bundle.ts
15852
16101
  import { createRequire } from "module";
@@ -15887,7 +16136,7 @@ function clientAssets() {
15887
16136
  const prebuilt = new URL("gallery.js", CLIENT_DIR);
15888
16137
  client ??= {
15889
16138
  css: readFileSync4(new URL("gallery.css", CLIENT_DIR), "utf8"),
15890
- js: existsSync22(prebuilt) ? readFileSync4(prebuilt, "utf8") : bundleGalleryClient()
16139
+ js: existsSync23(prebuilt) ? readFileSync4(prebuilt, "utf8") : bundleGalleryClient()
15891
16140
  };
15892
16141
  return client;
15893
16142
  }
@@ -16158,7 +16407,7 @@ async function serveGallery(opts) {
16158
16407
  const asset = reviewAssets.get(jobId)?.get(url2.pathname);
16159
16408
  if (!asset) return fail(404, "no such review asset");
16160
16409
  try {
16161
- const bytes = await readFile20(asset.path);
16410
+ const bytes = await readFile21(asset.path);
16162
16411
  res.writeHead(200, {
16163
16412
  "Content-Type": asset.contentType,
16164
16413
  "Cache-Control": "no-store",
@@ -16241,7 +16490,7 @@ async function serveGallery(opts) {
16241
16490
  if (!asset) return fail(404, "no such gallery media");
16242
16491
  let bytes;
16243
16492
  try {
16244
- bytes = await readFile20(asset.path);
16493
+ bytes = await readFile21(asset.path);
16245
16494
  } catch {
16246
16495
  return fail(404, "gallery media is no longer on disk; refresh the page");
16247
16496
  }
@@ -16285,7 +16534,7 @@ async function serveGallery(opts) {
16285
16534
 
16286
16535
  // src/gallery/generate.ts
16287
16536
  import { randomBytes as randomBytes2 } from "crypto";
16288
- import { z as z11 } from "zod";
16537
+ import { z as z12 } from "zod";
16289
16538
 
16290
16539
  // src/pipeline/poll.ts
16291
16540
  var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -16569,6 +16818,10 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
16569
16818
  ...spec.revision.keypointsSha256 ? { keypointsSha256: spec.revision.keypointsSha256 } : {},
16570
16819
  ...spec.revision.skeletonTemplate ? { skeletonTemplate: spec.revision.skeletonTemplate } : {},
16571
16820
  ...spec.revision.description ? { description: spec.revision.description } : {},
16821
+ ...spec.revision.removalTask ? { removalTask: spec.revision.removalTask } : {},
16822
+ ...spec.revision.driftThreshold == null ? {} : { driftThreshold: spec.revision.driftThreshold },
16823
+ ...spec.revision.subjectDescription ? { subjectDescription: spec.revision.subjectDescription } : {},
16824
+ ...spec.revision.initialPose ? { initialPose: spec.revision.initialPose } : {},
16572
16825
  ...spec.revision.engine ? { engine: spec.revision.engine } : {}
16573
16826
  } : null,
16574
16827
  outfit: spec.character?.kind === "outfit" && outfitParentEntry ? {
@@ -16696,13 +16949,13 @@ function ownedReferenceImageId(spec, lock, providerId) {
16696
16949
  }
16697
16950
 
16698
16951
  // src/pick/server.ts
16699
- import { existsSync as existsSync24 } from "fs";
16700
- import { readFile as readFile22 } from "fs/promises";
16952
+ import { existsSync as existsSync25 } from "fs";
16953
+ import { readFile as readFile23 } from "fs/promises";
16701
16954
 
16702
16955
  // src/pipeline/init.ts
16703
- import { readdir, readFile as readFile21, writeFile as writeFile15 } from "fs/promises";
16704
- import { existsSync as existsSync23 } from "fs";
16705
- import path31 from "path";
16956
+ import { readdir, readFile as readFile22, writeFile as writeFile16 } from "fs/promises";
16957
+ import { existsSync as existsSync24 } from "fs";
16958
+ import path32 from "path";
16706
16959
  function pngSize(buf) {
16707
16960
  if (buf.length < 24) return null;
16708
16961
  const isPng = buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834;
@@ -16716,7 +16969,7 @@ function slugify(value) {
16716
16969
  async function walk(dir, exclude) {
16717
16970
  const out = [];
16718
16971
  for (const entry of await readdir(dir, { withFileTypes: true })) {
16719
- const full = path31.join(dir, entry.name);
16972
+ const full = path32.join(dir, entry.name);
16720
16973
  if (exclude.some((x) => full.includes(x))) continue;
16721
16974
  if (entry.isDirectory()) out.push(...await walk(full, exclude));
16722
16975
  else if (entry.name.toLowerCase().endsWith(".png")) out.push(full);
@@ -16730,14 +16983,14 @@ async function scanAssets(root, opts = {}) {
16730
16983
  const skipped = [];
16731
16984
  const seen = /* @__PURE__ */ new Map();
16732
16985
  for (const file of files.sort()) {
16733
- const size = pngSize(await readFile21(file));
16986
+ const size = pngSize(await readFile22(file));
16734
16987
  if (!size) {
16735
16988
  skipped.push(`${file} (not a readable PNG)`);
16736
16989
  continue;
16737
16990
  }
16738
- const rel = path31.relative(root, file);
16739
- const category = path31.dirname(rel) === "." ? "" : path31.dirname(rel);
16740
- let id = slugify(path31.basename(rel));
16991
+ const rel = path32.relative(root, file);
16992
+ const category = path32.dirname(rel) === "." ? "" : path32.dirname(rel);
16993
+ let id = slugify(path32.basename(rel));
16741
16994
  if (seen.has(id)) {
16742
16995
  const n = seen.get(id) + 1;
16743
16996
  seen.set(id, n);
@@ -16795,10 +17048,10 @@ function mode(values) {
16795
17048
  return [...counts.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? 64;
16796
17049
  }
16797
17050
  async function writeManifestFile(target, manifest) {
16798
- if (existsSync23(target)) {
17051
+ if (existsSync24(target)) {
16799
17052
  throw new Error(`${target} already exists; refusing to overwrite an existing manifest.`);
16800
17053
  }
16801
- await writeFile15(target, JSON.stringify(manifest, null, 2) + "\n");
17054
+ await writeFile16(target, JSON.stringify(manifest, null, 2) + "\n");
16802
17055
  }
16803
17056
 
16804
17057
  // src/pick/sheet.ts
@@ -17211,13 +17464,13 @@ async function currentArt(entry, spec) {
17211
17464
  const previous = primaryOutput({ ...entry, outputs: entry.supersededOutputs });
17212
17465
  if (!previous) return null;
17213
17466
  const file = resolveOutputPath(previous.path, spec.root);
17214
- if (!existsSync24(file)) return null;
17467
+ if (!existsSync25(file)) return null;
17215
17468
  const contentType = previous.mediaType ?? mediaTypeFromExtension(file);
17216
17469
  if (!contentType) return null;
17217
17470
  let width = entry.width;
17218
17471
  let height = entry.height;
17219
17472
  if (contentType === "image/png") {
17220
- const size = pngSize(await readFile22(file));
17473
+ const size = pngSize(await readFile23(file));
17221
17474
  if (size) ({ width, height } = size);
17222
17475
  }
17223
17476
  return { path: file, contentType, width, height };
@@ -17385,26 +17638,26 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
17385
17638
  }
17386
17639
 
17387
17640
  // src/gallery/generate.ts
17388
- var GenerateRequestSchema = z11.object({
17389
- project: z11.string().min(1).optional(),
17390
- keys: z11.array(z11.string().min(1)).min(1).max(500),
17641
+ var GenerateRequestSchema = z12.object({
17642
+ project: z12.string().min(1).optional(),
17643
+ keys: z12.array(z12.string().min(1)).min(1).max(500),
17391
17644
  /**
17392
17645
  * Regenerate work that is up to date, as `gen --force` does, and let the
17393
17646
  * fetch replace a locally modified or untracked file. With `restore`, put
17394
17647
  * the recorded bytes back over a modified file.
17395
17648
  */
17396
- force: z11.boolean().optional(),
17649
+ force: z12.boolean().optional(),
17397
17650
  /** Advance in-flight, review, or selected work without submitting anything. */
17398
- resume: z11.boolean().optional(),
17651
+ resume: z12.boolean().optional(),
17399
17652
  /** Re-download downloaded outputs and replace files whose object changed upstream. */
17400
- refresh: z11.boolean().optional(),
17653
+ refresh: z12.boolean().optional(),
17401
17654
  /** Put recorded outputs back on disk from the cache or the provider, without generating. */
17402
- restore: z11.boolean().optional(),
17655
+ restore: z12.boolean().optional(),
17403
17656
  /**
17404
17657
  * Bring a replaced generation back: 1-based position in the record's
17405
17658
  * history (newest first) or an output hash prefix. One key at a time.
17406
17659
  */
17407
- revert: z11.string().min(1).max(64).optional()
17660
+ revert: z12.string().min(1).max(64).optional()
17408
17661
  }).strict().refine((request) => [request.resume, request.refresh, request.restore, request.revert !== void 0].filter(Boolean).length <= 1, {
17409
17662
  message: "resume, refresh, restore, and revert are exclusive"
17410
17663
  }).refine((request) => request.revert === void 0 || request.keys.length === 1, { message: "revert takes one key" });
@@ -17750,15 +18003,15 @@ function createGenerateHandlers(opts) {
17750
18003
  }
17751
18004
 
17752
18005
  // src/gallery/editor.ts
17753
- import { readFile as readFile24 } from "fs/promises";
17754
- import path33 from "path";
18006
+ import { readFile as readFile25 } from "fs/promises";
18007
+ import path34 from "path";
17755
18008
 
17756
18009
  // src/editor/install.ts
17757
18010
  import { createHash as createHash4 } from "crypto";
17758
- import { existsSync as existsSync25 } from "fs";
17759
- import { mkdir as mkdir12, readFile as readFile23, rename as rename10, rm as rm10, stat as stat3, writeFile as writeFile16 } from "fs/promises";
18011
+ import { existsSync as existsSync26 } from "fs";
18012
+ import { mkdir as mkdir12, readFile as readFile24, rename as rename10, rm as rm11, stat as stat3, writeFile as writeFile17 } from "fs/promises";
17760
18013
  import os2 from "os";
17761
- import path32 from "path";
18014
+ import path33 from "path";
17762
18015
 
17763
18016
  // tools/pixelorama-bridge/pin.json
17764
18017
  var pin_default = {
@@ -17822,15 +18075,15 @@ var EditorInstallError = class extends Error {
17822
18075
  };
17823
18076
  function editorToolsRoot() {
17824
18077
  const override = process.env.PIXELKILN_TOOLS_DIR?.trim();
17825
- if (override) return path32.resolve(override);
17826
- if (process.platform === "darwin") return path32.join(os2.homedir(), "Library", "Caches", "pixelkiln", "tools");
18078
+ if (override) return path33.resolve(override);
18079
+ if (process.platform === "darwin") return path33.join(os2.homedir(), "Library", "Caches", "pixelkiln", "tools");
17827
18080
  if (process.platform === "win32") {
17828
- return path32.join(process.env.LOCALAPPDATA ?? path32.join(os2.homedir(), "AppData", "Local"), "pixelkiln", "tools");
18081
+ return path33.join(process.env.LOCALAPPDATA ?? path33.join(os2.homedir(), "AppData", "Local"), "pixelkiln", "tools");
17829
18082
  }
17830
- return path32.join(process.env.XDG_CACHE_HOME ?? path32.join(os2.homedir(), ".cache"), "pixelkiln", "tools");
18083
+ return path33.join(process.env.XDG_CACHE_HOME ?? path33.join(os2.homedir(), ".cache"), "pixelkiln", "tools");
17831
18084
  }
17832
18085
  function editorDir(pin = EDITOR_PIN, root = editorToolsRoot()) {
17833
- return path32.join(root, "pixelorama", pin.release ?? "unpublished");
18086
+ return path33.join(root, "pixelorama", pin.release ?? "unpublished");
17834
18087
  }
17835
18088
  function editorBaseUrl(pin = EDITOR_PIN) {
17836
18089
  const override = process.env.PIXELKILN_EDITOR_URL?.trim();
@@ -17840,7 +18093,7 @@ function editorBaseUrl(pin = EDITOR_PIN) {
17840
18093
  }
17841
18094
  async function sha256Of(file) {
17842
18095
  try {
17843
- return createHash4("sha256").update(await readFile23(file)).digest("hex");
18096
+ return createHash4("sha256").update(await readFile24(file)).digest("hex");
17844
18097
  } catch {
17845
18098
  return null;
17846
18099
  }
@@ -17853,8 +18106,8 @@ async function editorStatus(opts = {}) {
17853
18106
  let totalBytes = 0;
17854
18107
  for (const [name, expected] of Object.entries(pin.files)) {
17855
18108
  totalBytes += expected.bytes;
17856
- const file = path32.join(dir, name);
17857
- const info = existsSync25(file) ? await stat3(file).catch(() => null) : null;
18109
+ const file = path33.join(dir, name);
18110
+ const info = existsSync26(file) ? await stat3(file).catch(() => null) : null;
17858
18111
  if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
17859
18112
  installedBytes += expected.bytes;
17860
18113
  } else {
@@ -17911,13 +18164,13 @@ async function installEditor(opts = {}) {
17911
18164
  if (digest2 !== expected.sha256) {
17912
18165
  throw new EditorInstallError(`${name}: sha256 ${digest2.slice(0, 12)}\u2026 does not match the pinned ${expected.sha256.slice(0, 12)}\u2026`, name);
17913
18166
  }
17914
- const final = path32.join(dir, name);
18167
+ const final = path33.join(dir, name);
17915
18168
  const tmp = `${final}.${process.pid}.part`;
17916
18169
  try {
17917
- await writeFile16(tmp, bytes);
18170
+ await writeFile17(tmp, bytes);
17918
18171
  await rename10(tmp, final);
17919
18172
  } finally {
17920
- await rm10(tmp, { force: true });
18173
+ await rm11(tmp, { force: true });
17921
18174
  }
17922
18175
  fetchedBytes += bytes.length;
17923
18176
  downloaded.push(name);
@@ -17957,7 +18210,7 @@ var EDITOR_CONTENT_TYPES = {
17957
18210
  ".json": "application/json; charset=utf-8"
17958
18211
  };
17959
18212
  function editorContentType(name) {
17960
- return EDITOR_CONTENT_TYPES[path32.extname(name).toLowerCase()] ?? null;
18213
+ return EDITOR_CONTENT_TYPES[path33.extname(name).toLowerCase()] ?? null;
17961
18214
  }
17962
18215
 
17963
18216
  // src/gallery/editor.ts
@@ -18033,7 +18286,7 @@ function createGalleryEditorHandlers(opts = {}) {
18033
18286
  if (!status.installed) return null;
18034
18287
  let bytes;
18035
18288
  try {
18036
- bytes = await readFile24(path33.join(dir, name));
18289
+ bytes = await readFile25(path34.join(dir, name));
18037
18290
  } catch {
18038
18291
  verified = null;
18039
18292
  return null;
@@ -18073,7 +18326,7 @@ async function serveUntilStopped(initial, reload, args, access2) {
18073
18326
  };
18074
18327
  const loadProject = async (project) => {
18075
18328
  const ctx = await access2.loadProject(project);
18076
- loadProjectEnv(path34.dirname(ctx.loaded.path));
18329
+ loadProjectEnv(path35.dirname(ctx.loaded.path));
18077
18330
  return ctx;
18078
18331
  };
18079
18332
  const loadProjectEnv = (dir) => {
@@ -18082,7 +18335,7 @@ async function serveUntilStopped(initial, reload, args, access2) {
18082
18335
  for (const [name, value] of Object.entries(declared)) {
18083
18336
  if (credentialNames.has(name) && name in process.env && process.env[name] !== value) {
18084
18337
  throw new Error(
18085
- `${path34.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.`
18338
+ `${path35.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.`
18086
18339
  );
18087
18340
  }
18088
18341
  }
@@ -18090,7 +18343,7 @@ async function serveUntilStopped(initial, reload, args, access2) {
18090
18343
  };
18091
18344
  const quoters = /* @__PURE__ */ new Map();
18092
18345
  const quoteFor = async (project) => {
18093
- loadProjectEnv(path34.dirname(path34.resolve(await access2.manifestFor(project))));
18346
+ loadProjectEnv(path35.dirname(path35.resolve(await access2.manifestFor(project))));
18094
18347
  const key = process.env.PIXELLAB_API_KEY;
18095
18348
  if (!key) return null;
18096
18349
  let quoter = quoters.get(key);
@@ -18141,8 +18394,8 @@ async function serveUntilStopped(initial, reload, args, access2) {
18141
18394
  }
18142
18395
  async function runGallery(args) {
18143
18396
  if (args.workspace) {
18144
- const workspacePath = path34.resolve(args.workspace);
18145
- if (!existsSync26(workspacePath)) {
18397
+ const workspacePath = path35.resolve(args.workspace);
18398
+ if (!existsSync27(workspacePath)) {
18146
18399
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
18147
18400
  }
18148
18401
  const filter2 = { styles: args.styles, assets: args.assets };
@@ -18162,7 +18415,7 @@ async function runGallery(args) {
18162
18415
  const registered = async (projectId) => {
18163
18416
  const project = (await loadWorkspace(workspacePath)).projects.find((candidate) => candidate.id === projectId);
18164
18417
  if (!projectId || !project) throw new Error(`unknown workspace project "${projectId ?? ""}"`);
18165
- return resolveProject(path34.dirname(workspacePath), project);
18418
+ return resolveProject(path35.dirname(workspacePath), project);
18166
18419
  };
18167
18420
  await serveUntilStopped(initial, build2, args, {
18168
18421
  manifestFor: async (projectId) => (await registered(projectId)).manifestPath,
@@ -18187,7 +18440,7 @@ async function runGallery(args) {
18187
18440
  return buildGallerySnapshot({ ...fresh, filter });
18188
18441
  };
18189
18442
  await serveUntilStopped(await build(), reload, args, {
18190
- manifestFor: () => path34.resolve(args.manifest),
18443
+ manifestFor: () => path35.resolve(args.manifest),
18191
18444
  loadProject: () => openProject(args.manifest, { lockPath: args.lock, env: false })
18192
18445
  });
18193
18446
  }
@@ -18496,18 +18749,18 @@ async function runRestoreGeneration(args) {
18496
18749
  }
18497
18750
 
18498
18751
  // src/cli/commands/housekeeping.ts
18499
- import { existsSync as existsSync28 } from "fs";
18752
+ import { existsSync as existsSync29 } from "fs";
18500
18753
 
18501
18754
  // src/pipeline/cache-health.ts
18502
- import { existsSync as existsSync27 } from "fs";
18503
- import { readFile as readFile25, readdir as readdir2, rm as rm11 } from "fs/promises";
18504
- import path35 from "path";
18755
+ import { existsSync as existsSync28 } from "fs";
18756
+ import { readFile as readFile26, readdir as readdir2, rm as rm12 } from "fs/promises";
18757
+ import path36 from "path";
18505
18758
  async function inspectCaches(lock, lockPath, options = {}) {
18506
- if (options.prune && !existsSync27(lockPath)) {
18507
- throw new Error(`Refusing to prune without an existing lockfile at ${path35.resolve(lockPath)}`);
18759
+ if (options.prune && !existsSync28(lockPath)) {
18760
+ throw new Error(`Refusing to prune without an existing lockfile at ${path36.resolve(lockPath)}`);
18508
18761
  }
18509
- const contentDir = path35.resolve(path35.dirname(lockPath), ".pixelkiln", "cache");
18510
- const remotePath = path35.resolve(cachePathFor(lockPath));
18762
+ const contentDir = path36.resolve(path36.dirname(lockPath), ".pixelkiln", "cache");
18763
+ const remotePath = path36.resolve(cachePathFor(lockPath));
18511
18764
  const referenced = referencedHashes(lock);
18512
18765
  let content = await inspectContentCache(contentDir, referenced);
18513
18766
  let remoteHashes = await inspectRemoteHashCache(remotePath);
@@ -18519,7 +18772,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
18519
18772
  ]);
18520
18773
  for (const name of names) {
18521
18774
  try {
18522
- await rm11(path35.join(contentDir, name), { force: true });
18775
+ await rm12(path36.join(contentDir, name), { force: true });
18523
18776
  removed.contentFiles++;
18524
18777
  } catch {
18525
18778
  }
@@ -18528,7 +18781,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
18528
18781
  await saveCache(remotePath, { version: 1, hashes: {} });
18529
18782
  removed.resetRemoteHashCache = true;
18530
18783
  } else if (remoteHashes.invalidIds.length) {
18531
- const cache = parseCache(JSON.parse(await readFile25(remotePath, "utf8")));
18784
+ const cache = parseCache(JSON.parse(await readFile26(remotePath, "utf8")));
18532
18785
  for (const id of remoteHashes.invalidIds) delete cache.hashes[id];
18533
18786
  removed.remoteHashEntries = remoteHashes.invalidIds.length;
18534
18787
  await saveCache(remotePath, cache);
@@ -18546,7 +18799,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
18546
18799
  async function inspectContentCache(contentDir, referenced) {
18547
18800
  const report = {
18548
18801
  path: contentDir,
18549
- exists: existsSync27(contentDir),
18802
+ exists: existsSync28(contentDir),
18550
18803
  files: 0,
18551
18804
  bytes: 0,
18552
18805
  valid: 0,
@@ -18567,12 +18820,12 @@ async function inspectContentCache(contentDir, referenced) {
18567
18820
  continue;
18568
18821
  }
18569
18822
  report.files++;
18570
- const file = path35.join(contentDir, entry.name);
18823
+ const file = path36.join(contentDir, entry.name);
18571
18824
  const mediaType = mediaTypeFromExtension(entry.name);
18572
18825
  const expected = mediaType ? entry.name.slice(0, -4) : "";
18573
18826
  let bytes;
18574
18827
  try {
18575
- bytes = await readFile25(file);
18828
+ bytes = await readFile26(file);
18576
18829
  report.bytes += bytes.length;
18577
18830
  } catch (err) {
18578
18831
  report.invalid.push({
@@ -18610,7 +18863,7 @@ async function inspectContentCache(contentDir, referenced) {
18610
18863
  async function inspectRemoteHashCache(remotePath) {
18611
18864
  const report = {
18612
18865
  path: remotePath,
18613
- exists: existsSync27(remotePath),
18866
+ exists: existsSync28(remotePath),
18614
18867
  entries: 0,
18615
18868
  valid: 0,
18616
18869
  invalidIds: [],
@@ -18619,7 +18872,7 @@ async function inspectRemoteHashCache(remotePath) {
18619
18872
  if (!report.exists) return report;
18620
18873
  let cache;
18621
18874
  try {
18622
- cache = parseCache(JSON.parse(await readFile25(remotePath, "utf8")));
18875
+ cache = parseCache(JSON.parse(await readFile26(remotePath, "utf8")));
18623
18876
  } catch (err) {
18624
18877
  report.error = err instanceof Error ? err.message : String(err);
18625
18878
  return report;
@@ -18683,7 +18936,7 @@ async function runAccept(args) {
18683
18936
  let intact = true;
18684
18937
  for (const output of entry.outputs) {
18685
18938
  const file = resolveOutputPath(output.path, item.spec.root);
18686
- if (!existsSync28(file) || await sha256File(file) !== output.sha256) {
18939
+ if (!existsSync29(file) || await sha256File(file) !== output.sha256) {
18687
18940
  intact = false;
18688
18941
  break;
18689
18942
  }
@@ -18738,42 +18991,42 @@ This drops their provenance from the lockfile. The art on disk is`);
18738
18991
  }
18739
18992
 
18740
18993
  // src/cli/commands/init.ts
18741
- import path36 from "path";
18742
- import { existsSync as existsSync29 } from "fs";
18994
+ import path37 from "path";
18995
+ import { existsSync as existsSync30 } from "fs";
18743
18996
  async function runInit(args) {
18744
18997
  if (!args.from) throw new Error("init needs --from <dir> pointing at your existing PNGs.");
18745
- const root = path36.resolve(args.from);
18746
- if (!existsSync29(root)) throw new Error(`No directory at ${root}`);
18998
+ const root = path37.resolve(args.from);
18999
+ if (!existsSync30(root)) throw new Error(`No directory at ${root}`);
18747
19000
  const generator = args.generator ?? "map";
18748
19001
  if (generator !== "1dir" && generator !== "map") {
18749
19002
  throw new Error(`--generator must be "1dir" or "map", got "${args.generator}".`);
18750
19003
  }
18751
- const target = path36.resolve(args.out ?? "pixelkiln.manifest.json");
19004
+ const target = path37.resolve(args.out ?? "pixelkiln.manifest.json");
18752
19005
  const { assets, skipped } = await scanAssets(root, { exclude: args.exclude });
18753
19006
  if (!assets.length) throw new Error(`No PNGs found under ${root}`);
18754
19007
  const manifest = buildManifest(
18755
- args.name ?? path36.basename(path36.dirname(target)),
19008
+ args.name ?? path37.basename(path37.dirname(target)),
18756
19009
  args.styles[0] ?? "base",
18757
19010
  generator,
18758
- path36.relative(path36.dirname(target), root) || ".",
19011
+ path37.relative(path37.dirname(target), root) || ".",
18759
19012
  assets
18760
19013
  );
18761
19014
  await writeManifestFile(target, manifest);
18762
- log(` scanned ${assets.length} PNG(s) under ${path36.relative(process.cwd(), root)}`);
19015
+ log(` scanned ${assets.length} PNG(s) under ${path37.relative(process.cwd(), root)}`);
18763
19016
  if (skipped.length) log(` skipped ${skipped.length} unreadable file(s)`);
18764
- log(` wrote ${path36.relative(process.cwd(), target)}`);
19017
+ log(` wrote ${path37.relative(process.cwd(), target)}`);
18765
19018
  log(`
18766
19019
  Prompts are intentionally empty. To recover the real ones from your`);
18767
19020
  log(` PixelLab account instead of inventing them:`);
18768
19021
  log(`
18769
- pixelkiln adopt --manifest ${path36.relative(process.cwd(), target)} --write-prompts
19022
+ pixelkiln adopt --manifest ${path37.relative(process.cwd(), target)} --write-prompts
18770
19023
  `);
18771
19024
  }
18772
19025
 
18773
19026
  // src/pipeline/doctor.ts
18774
- import { constants, existsSync as existsSync30 } from "fs";
19027
+ import { constants, existsSync as existsSync31 } from "fs";
18775
19028
  import { access, stat as stat4 } from "fs/promises";
18776
- import path37 from "path";
19029
+ import path38 from "path";
18777
19030
  async function doctor(loaded, specs, lock, lockPath, opts = {}) {
18778
19031
  const checks = [];
18779
19032
  const add = (id, level, message6) => checks.push({ id, level, message: message6 });
@@ -18784,8 +19037,8 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
18784
19037
  );
18785
19038
  add("lock", "ok", `${Object.keys(lock.entries).length} valid v2 lock entr(ies)`);
18786
19039
  const dirs = [...new Set(specs.flatMap((spec) => [
18787
- path37.dirname(spec.outFile),
18788
- ...spec.quality ? [path37.dirname(spec.quality.outFile)] : []
19040
+ path38.dirname(spec.outFile),
19041
+ ...spec.quality ? [path38.dirname(spec.quality.outFile)] : []
18789
19042
  ]))];
18790
19043
  const unwritable = [];
18791
19044
  for (const dir of dirs) {
@@ -18808,7 +19061,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
18808
19061
  for (let index = 0; index < entry.outputs.length; index++) {
18809
19062
  const output = entry.outputs[index];
18810
19063
  const spec = specByKey.get(key);
18811
- const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, path37.dirname(lockPath));
19064
+ const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, path38.dirname(lockPath));
18812
19065
  const owner = outputOwners.get(absolute);
18813
19066
  if (owner && owner !== key) duplicateOutputs.push(`${output.path} (${owner}, ${key})`);
18814
19067
  outputOwners.set(absolute, key);
@@ -18823,7 +19076,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
18823
19076
  } else {
18824
19077
  add("lock-outputs", "ok", `${outputOwners.size} recorded output path(s) are uniquely owned`);
18825
19078
  }
18826
- const cacheDir = path37.join(path37.dirname(lockPath), ".pixelkiln", "cache");
19079
+ const cacheDir = path38.join(path38.dirname(lockPath), ".pixelkiln", "cache");
18827
19080
  const stranded = [];
18828
19081
  const invalidState = [];
18829
19082
  const staleMapUrls = [];
@@ -18839,11 +19092,11 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
18839
19092
  const hasSource = Boolean(entry.sourceUrl || entry.sourceUrls?.length);
18840
19093
  let hasCache = false;
18841
19094
  for (const output of entry.outputs) {
18842
- const cached = path37.join(
19095
+ const cached = path38.join(
18843
19096
  cacheDir,
18844
19097
  cacheFileName(output.sha256, output.mediaType ?? MediaType.PNG)
18845
19098
  );
18846
- if (existsSync30(cached) && await sha256File(cached) === output.sha256) {
19099
+ if (existsSync31(cached) && await sha256File(cached) === output.sha256) {
18847
19100
  hasCache = true;
18848
19101
  break;
18849
19102
  }
@@ -18939,16 +19192,16 @@ async function nearestExistingDirectory(start) {
18939
19192
  if ((await stat4(current)).isDirectory()) return current;
18940
19193
  } catch {
18941
19194
  }
18942
- const parent = path37.dirname(current);
19195
+ const parent = path38.dirname(current);
18943
19196
  if (parent === current) return current;
18944
19197
  current = parent;
18945
19198
  }
18946
19199
  }
18947
19200
 
18948
19201
  // src/pipeline/audit.ts
18949
- import { readFile as readFile26 } from "fs/promises";
18950
- import { existsSync as existsSync31 } from "fs";
18951
- import path38 from "path";
19202
+ import { readFile as readFile27 } from "fs/promises";
19203
+ import { existsSync as existsSync32 } from "fs";
19204
+ import path39 from "path";
18952
19205
  function paletteDistance(asset, reference) {
18953
19206
  if (!asset.length || !reference.length) return 0;
18954
19207
  let total = 0;
@@ -18984,12 +19237,12 @@ async function auditStyle(loaded, specs, styleId, lock) {
18984
19237
  const unreadable = [];
18985
19238
  for (const spec of mine) {
18986
19239
  for (const output of resolveSpecOutputs(spec, lock, loaded.root)) {
18987
- if (!existsSync31(output.absolutePath)) {
19240
+ if (!existsSync32(output.absolutePath)) {
18988
19241
  missing.push(output.id);
18989
19242
  continue;
18990
19243
  }
18991
19244
  try {
18992
- const png = decodePng(await readFile26(output.absolutePath));
19245
+ const png = decodePng(await readFile27(output.absolutePath));
18993
19246
  const palette = extractPalette(png, 12);
18994
19247
  assets.push({
18995
19248
  assetId: spec.assetId,
@@ -19013,10 +19266,10 @@ async function auditStyle(loaded, specs, styleId, lock) {
19013
19266
  let referenceFromStyleImages = false;
19014
19267
  const refPalettes = [];
19015
19268
  for (const img of style.styleImages) {
19016
- const abs = path38.resolve(loaded.root, img.path);
19017
- if (!existsSync31(abs)) continue;
19269
+ const abs = path39.resolve(loaded.root, img.path);
19270
+ if (!existsSync32(abs)) continue;
19018
19271
  try {
19019
- refPalettes.push(extractPalette(decodePng(await readFile26(abs)), 12));
19272
+ refPalettes.push(extractPalette(decodePng(await readFile27(abs)), 12));
19020
19273
  } catch {
19021
19274
  }
19022
19275
  }
@@ -19244,24 +19497,24 @@ async function runAudit(args) {
19244
19497
  }
19245
19498
 
19246
19499
  // src/cli/commands/pack.ts
19247
- import path40 from "path";
19248
- import { readFile as readFile27 } from "fs/promises";
19500
+ import path41 from "path";
19501
+ import { readFile as readFile28 } from "fs/promises";
19249
19502
 
19250
19503
  // src/pipeline/pack.ts
19251
19504
  import { readFileSync as readFileSync5 } from "fs";
19252
- import path39 from "path";
19505
+ import path40 from "path";
19253
19506
  var DEFAULT_FRAME_SET_FPS = 12;
19254
19507
  function resolvePackInputs(raw, inputsFilePath) {
19255
19508
  if (!Array.isArray(raw) || !raw.length) {
19256
19509
  throw new Error("--inputs must be a non-empty JSON array of { id, path }");
19257
19510
  }
19258
- const baseDir = path39.dirname(path39.resolve(inputsFilePath));
19511
+ const baseDir = path40.dirname(path40.resolve(inputsFilePath));
19259
19512
  return raw.map((entry, i) => {
19260
19513
  const e = entry;
19261
19514
  if (typeof e.id !== "string" || typeof e.path !== "string") {
19262
19515
  throw new Error(`--inputs[${i}] needs string "id" and "path"`);
19263
19516
  }
19264
- return { id: e.id, path: path39.resolve(baseDir, e.path) };
19517
+ return { id: e.id, path: path40.resolve(baseDir, e.path) };
19265
19518
  });
19266
19519
  }
19267
19520
  function packSprites(inputs, options = {}) {
@@ -19345,7 +19598,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
19345
19598
  }
19346
19599
  const inputs2 = Object.entries(options.sourceOverrides).map(([id, source]) => ({
19347
19600
  id,
19348
- path: path39.resolve(source)
19601
+ path: path40.resolve(source)
19349
19602
  }));
19350
19603
  if (!inputs2.length) throw new Error(`No approved quality outputs for style "${styleId}".`);
19351
19604
  const packed2 = packSprites(inputs2, options);
@@ -19369,7 +19622,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
19369
19622
  let outputs = resolveEntryOutputs(entry, id, manifestDir);
19370
19623
  const source = sources[id];
19371
19624
  if (source && outputs.length <= 1) {
19372
- inputs.push({ id, path: path39.resolve(manifestDir, source) });
19625
+ inputs.push({ id, path: path40.resolve(manifestDir, source) });
19373
19626
  continue;
19374
19627
  }
19375
19628
  if (source && sourceIsStem(source, entry, manifestDir)) {
@@ -19403,7 +19656,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
19403
19656
  }
19404
19657
  }
19405
19658
  for (const [id, source] of Object.entries(sources)) {
19406
- if (!locked.has(id)) inputs.push({ id, path: path39.resolve(manifestDir, source) });
19659
+ if (!locked.has(id)) inputs.push({ id, path: path40.resolve(manifestDir, source) });
19407
19660
  }
19408
19661
  if (!inputs.length) {
19409
19662
  throw new Error(`No readable sprites for style "${styleId}"; ${noOutput.length} skipped.`);
@@ -19511,7 +19764,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
19511
19764
  const source = sources[id];
19512
19765
  const entry = lock.entries[`${styleId}/${id}`];
19513
19766
  if (source && !(entry && sourceIsStem(source, entry, manifestDir))) {
19514
- placements.push({ id, path: path39.resolve(manifestDir, source), cell });
19767
+ placements.push({ id, path: path40.resolve(manifestDir, source), cell });
19515
19768
  continue;
19516
19769
  }
19517
19770
  if (source && entry) {
@@ -19544,7 +19797,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
19544
19797
  `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}`
19545
19798
  );
19546
19799
  }
19547
- const basePng = mount.base ? readFileSync5(path39.resolve(manifestDir, mount.base)) : void 0;
19800
+ const basePng = mount.base ? readFileSync5(path40.resolve(manifestDir, mount.base)) : void 0;
19548
19801
  const mounted = mountSprites(placements, {
19549
19802
  cellWidth: mount.cellWidth,
19550
19803
  cellHeight: mount.cellHeight,
@@ -19554,7 +19807,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
19554
19807
  ...mounted,
19555
19808
  sources: mount.base ? [{
19556
19809
  id: "$base",
19557
- path: path39.resolve(manifestDir, mount.base),
19810
+ path: path40.resolve(manifestDir, mount.base),
19558
19811
  sha256: sha256(basePng),
19559
19812
  included: true
19560
19813
  }, ...mounted.sources] : mounted.sources,
@@ -19958,12 +20211,12 @@ async function runPack(args) {
19958
20211
  if (args.primaryOnly || args.outputRoles.length) {
19959
20212
  throw new Error("--primary-only and --output-role require manifest-driven pack");
19960
20213
  }
19961
- const raw = JSON.parse(await readFile27(path40.resolve(args.inputs), "utf8"));
20214
+ const raw = JSON.parse(await readFile28(path41.resolve(args.inputs), "utf8"));
19962
20215
  const inputs = resolvePackInputs(raw, args.inputs);
19963
20216
  const { png, atlas, skipped, sources } = packSprites(inputs, { columns: args.columns });
19964
20217
  const format = sheetFormat(args);
19965
- const base = path40.resolve(args.out.replace(/\.(?:png|json|tres)$/i, ""));
19966
- const { extension, document } = renderSheetDocument(format, atlas, { imageName: path40.basename(`${base}.png`) });
20218
+ const base = path41.resolve(args.out.replace(/\.(?:png|json|tres)$/i, ""));
20219
+ const { extension, document } = renderSheetDocument(format, atlas, { imageName: path41.basename(`${base}.png`) });
19967
20220
  const outputs = [
19968
20221
  { path: `${base}.png`, data: png },
19969
20222
  { path: `${base}${extension}`, data: document }
@@ -19976,7 +20229,7 @@ async function runPack(args) {
19976
20229
  log(
19977
20230
  ` ${atlas.frames.length} sprite(s), ${atlas.sheet.width}x${atlas.sheet.height} in ${atlas.columns} column(s), ${(png.length / 1024).toFixed(1)} KB (${format})`
19978
20231
  );
19979
- log(` ${path40.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
20232
+ log(` ${path41.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
19980
20233
  for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
19981
20234
  return;
19982
20235
  }
@@ -19984,7 +20237,7 @@ async function runPack(args) {
19984
20237
  if (args.primaryOnly && args.outputRoles.length) {
19985
20238
  throw new Error("pack accepts either --primary-only or --output-role, not both");
19986
20239
  }
19987
- const manifestDir = path40.dirname(path40.resolve(args.manifest));
20240
+ const manifestDir = path41.dirname(path41.resolve(args.manifest));
19988
20241
  const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
19989
20242
  for (const styleId of styleIds) {
19990
20243
  const style = loaded.manifest.styles[styleId];
@@ -19999,9 +20252,9 @@ async function runPack(args) {
19999
20252
  sourceOverrides: qualitySources,
20000
20253
  sources: manifestSources(loaded.manifest, styleId)
20001
20254
  });
20002
- const base = args.out ? path40.resolve(args.out.replace(/\.(?:png|json|tres)$/i, "")) : path40.resolve(manifestDir, style.outDir, `${styleId}-sheet`);
20255
+ const base = args.out ? path41.resolve(args.out.replace(/\.(?:png|json|tres)$/i, "")) : path41.resolve(manifestDir, style.outDir, `${styleId}-sheet`);
20003
20256
  const format = sheetFormat(args);
20004
- const { extension, document } = renderSheetDocument(format, atlas, { imageName: path40.basename(`${base}.png`) });
20257
+ const { extension, document } = renderSheetDocument(format, atlas, { imageName: path41.basename(`${base}.png`) });
20005
20258
  const outputs = [
20006
20259
  { path: `${base}.png`, data: png },
20007
20260
  { path: `${base}${extension}`, data: document }
@@ -20033,13 +20286,13 @@ async function runPack(args) {
20033
20286
  log(
20034
20287
  ` ${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})`
20035
20288
  );
20036
- log(` ${path40.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
20289
+ log(` ${path41.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
20037
20290
  for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
20038
20291
  }
20039
20292
  }
20040
20293
  async function runMount(args) {
20041
20294
  const { loaded, lock } = await openProject2(args);
20042
- const manifestDir = path40.dirname(path40.resolve(args.manifest));
20295
+ const manifestDir = path41.dirname(path41.resolve(args.manifest));
20043
20296
  const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
20044
20297
  for (const styleId of styleIds) {
20045
20298
  const style = loaded.manifest.styles[styleId];
@@ -20078,9 +20331,9 @@ async function runMount(args) {
20078
20331
  sources,
20079
20332
  outputRoles
20080
20333
  );
20081
- const out = path40.resolve(manifestDir, style.mount.out);
20334
+ const out = path41.resolve(manifestDir, style.mount.out);
20082
20335
  const format = sheetFormat(args);
20083
- const { extension, document } = renderSheetDocument(format, atlas, { imageName: path40.basename(out) });
20336
+ const { extension, document } = renderSheetDocument(format, atlas, { imageName: path41.basename(out) });
20084
20337
  const metadata = out.replace(/\.png$/, "") + extension;
20085
20338
  const companion = out.replace(/\.png$/, "") + ".pixelkiln.json";
20086
20339
  const outputs = [
@@ -20100,7 +20353,7 @@ async function runMount(args) {
20100
20353
  await provenanceFile("$lock", args.lock),
20101
20354
  ...qualityRecords,
20102
20355
  ...artifactSources.filter(
20103
- (source) => source.id !== "$base" || path40.resolve(source.path) !== out
20356
+ (source) => source.id !== "$base" || path41.resolve(source.path) !== out
20104
20357
  )
20105
20358
  ],
20106
20359
  options: {
@@ -20114,14 +20367,14 @@ async function runMount(args) {
20114
20367
  log(
20115
20368
  ` ${styleId}: ${atlas.frames.length} cell(s) into ${atlas.sheet.width}x${atlas.sheet.height}` + (overBase ? ` over ${style.mount.base}` : " (new sheet)")
20116
20369
  );
20117
- log(` ${path40.relative(process.cwd(), out)} + atlas/provenance JSON`);
20370
+ log(` ${path41.relative(process.cwd(), out)} + atlas/provenance JSON`);
20118
20371
  for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
20119
20372
  }
20120
20373
  }
20121
20374
  async function runExport(args) {
20122
20375
  const { loaded, specs, lock } = await openProject2(args);
20123
20376
  const format = args.format ?? "generic";
20124
- const manifestDir = path40.dirname(path40.resolve(args.manifest));
20377
+ const manifestDir = path41.dirname(path41.resolve(args.manifest));
20125
20378
  const selected = specs.filter((spec) => {
20126
20379
  if (spec.generator !== "tiles" && spec.generator !== "terrain") return false;
20127
20380
  if (args.styles.length && !args.styles.includes(spec.styleId)) return false;
@@ -20137,12 +20390,12 @@ async function runExport(args) {
20137
20390
  for (const spec of selected) {
20138
20391
  const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
20139
20392
  const style = loaded.manifest.styles[spec.styleId];
20140
- const defaultBase = path40.resolve(manifestDir, style.outDir, `${spec.assetId}-tileset`);
20141
- const base = args.out ? path40.resolve(args.out.replace(/\.(?:png|json|tsj|tres)$/i, "")) : defaultBase;
20393
+ const defaultBase = path41.resolve(manifestDir, style.outDir, `${spec.assetId}-tileset`);
20394
+ const base = args.out ? path41.resolve(args.out.replace(/\.(?:png|json|tsj|tres)$/i, "")) : defaultBase;
20142
20395
  const result = exportTileset(entry, spec, {
20143
20396
  format,
20144
20397
  manifestDir,
20145
- imageName: path40.basename(`${base}.png`),
20398
+ imageName: path41.basename(`${base}.png`),
20146
20399
  columns: args.columns
20147
20400
  });
20148
20401
  const outputs = [
@@ -20160,7 +20413,7 @@ async function runExport(args) {
20160
20413
  asset: spec.assetId,
20161
20414
  columns: args.columns ?? null,
20162
20415
  format,
20163
- image: path40.basename(`${base}.png`),
20416
+ image: path41.basename(`${base}.png`),
20164
20417
  providerRules: result.generic.providerRules,
20165
20418
  style: spec.styleId,
20166
20419
  tileType: spec.tileType ?? null
@@ -20170,7 +20423,7 @@ async function runExport(args) {
20170
20423
  ` ${spec.styleId}/${spec.assetId}: ${result.generic.tiles.length} tile(s), ${result.generic.sheet.width}x${result.generic.sheet.height} (${format})`
20171
20424
  );
20172
20425
  log(
20173
- ` ${path40.relative(process.cwd(), base)}.png + ${path40.basename(base)}${result.extension} + .pixelkiln.json`
20426
+ ` ${path41.relative(process.cwd(), base)}.png + ${path41.basename(base)}${result.extension} + .pixelkiln.json`
20174
20427
  );
20175
20428
  }
20176
20429
  }
@@ -20183,23 +20436,23 @@ function sheetFormat(args) {
20183
20436
  }
20184
20437
 
20185
20438
  // src/cli/commands/pixellab-utils.ts
20186
- import path42 from "path";
20439
+ import path43 from "path";
20187
20440
 
20188
20441
  // src/pixellab-utils.ts
20189
- import { existsSync as existsSync32 } from "fs";
20190
- import { mkdir as mkdir13, readFile as readFile28, writeFile as writeFile17 } from "fs/promises";
20191
- import path41 from "path";
20442
+ import { existsSync as existsSync33 } from "fs";
20443
+ import { mkdir as mkdir13, readFile as readFile29, writeFile as writeFile18 } from "fs/promises";
20444
+ import path42 from "path";
20192
20445
  var UNZOOM_MIN_SIDE = 256;
20193
20446
  var UNZOOM_MAX_AREA = 2048 * 2048;
20194
20447
  function defaultUnzoomOut(input) {
20195
- const ext = path41.extname(input);
20448
+ const ext = path42.extname(input);
20196
20449
  return `${ext ? input.slice(0, -ext.length) : input}.unzoomed.png`;
20197
20450
  }
20198
20451
  async function unzoomFile(client2, input, opts = {}) {
20199
- const out = path41.resolve(opts.out ?? defaultUnzoomOut(input));
20200
- if (path41.resolve(input) === out) throw new Error("unzoom will not overwrite its own input; pass a different --out");
20201
- if (existsSync32(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
20202
- const bytes = await readFile28(input);
20452
+ const out = path42.resolve(opts.out ?? defaultUnzoomOut(input));
20453
+ if (path42.resolve(input) === out) throw new Error("unzoom will not overwrite its own input; pass a different --out");
20454
+ if (existsSync33(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
20455
+ const bytes = await readFile29(input);
20203
20456
  const metadata = imageMetadata(bytes);
20204
20457
  if (!metadata) throw new Error(`${input} is not a readable PNG or JPEG`);
20205
20458
  const { width, height, format } = metadata;
@@ -20212,22 +20465,22 @@ async function unzoomFile(client2, input, opts = {}) {
20212
20465
  throw new Error(`${input} is ${width}x${height}; PixelLab's unzoom takes at most 2048x2048 worth of pixels`);
20213
20466
  }
20214
20467
  const res = await client2.unzoom({ image: { base64: bytes.toString("base64"), format }, quantize: opts.quantize });
20215
- await mkdir13(path41.dirname(out), { recursive: true });
20216
- await writeFile17(out, res.png);
20468
+ await mkdir13(path42.dirname(out), { recursive: true });
20469
+ await writeFile18(out, res.png);
20217
20470
  return { out, original: res.originalSize, unzoomed: res.unzoomedSize, zoomFactor: res.zoomFactor, usage: res.usage };
20218
20471
  }
20219
20472
  var FONT_WEIGHTS = ["Bold", "Regular"];
20220
20473
  var FONT_GLYPH_SIZES = [8, 16, 32, 64];
20221
20474
  var FONT_COST_GENERATIONS = 25;
20222
20475
  function fontOutputPaths(out) {
20223
- const base = path41.resolve(out.replace(/\.(ttf|png)$/i, ""));
20476
+ const base = path42.resolve(out.replace(/\.(ttf|png)$/i, ""));
20224
20477
  return { ttf: `${base}.ttf`, atlas: `${base}.png` };
20225
20478
  }
20226
20479
  async function generateFont(client2, opts) {
20227
20480
  const { ttf, atlas } = fontOutputPaths(opts.out);
20228
20481
  if (!opts.force) {
20229
20482
  for (const file of [ttf, atlas]) {
20230
- if (existsSync32(file)) throw new Error(`${file} already exists; pass --force to replace it`);
20483
+ if (existsSync33(file)) throw new Error(`${file} already exists; pass --force to replace it`);
20231
20484
  }
20232
20485
  }
20233
20486
  const sleep3 = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
@@ -20254,9 +20507,9 @@ async function generateFont(client2, opts) {
20254
20507
  client2.download(job.download_ttf_url),
20255
20508
  client2.download(job.download_atlas_url)
20256
20509
  ]);
20257
- await mkdir13(path41.dirname(ttf), { recursive: true });
20258
- await writeFile17(ttf, ttfBytes);
20259
- await writeFile17(atlas, atlasBytes);
20510
+ await mkdir13(path42.dirname(ttf), { recursive: true });
20511
+ await writeFile18(ttf, ttfBytes);
20512
+ await writeFile18(atlas, atlasBytes);
20260
20513
  return { jobId, ttf, atlas, glyphPx: job.glyph_px ?? null, usage: job.usage };
20261
20514
  }
20262
20515
  if (Date.now() - started > timeout) {
@@ -20270,7 +20523,7 @@ async function generateFont(client2, opts) {
20270
20523
 
20271
20524
  // src/cli/commands/pixellab-utils.ts
20272
20525
  function loadKeys(args) {
20273
- loadEnvFiles(path42.dirname(path42.resolve(args.manifest)));
20526
+ loadEnvFiles(path43.dirname(path43.resolve(args.manifest)));
20274
20527
  loadEnvFiles(process.cwd());
20275
20528
  }
20276
20529
  async function runUnzoom(args) {
@@ -20336,19 +20589,19 @@ async function runFont(args) {
20336
20589
  }
20337
20590
 
20338
20591
  // src/cli/commands/quality.ts
20339
- import path44 from "path";
20340
- import { readFile as readFile30 } from "fs/promises";
20592
+ import path45 from "path";
20593
+ import { readFile as readFile31 } from "fs/promises";
20341
20594
 
20342
20595
  // src/pipeline/quality-regression.ts
20343
- import path43 from "path";
20344
- import { existsSync as existsSync33 } from "fs";
20345
- import { readFile as readFile29 } from "fs/promises";
20346
- import { z as z12 } from "zod";
20596
+ import path44 from "path";
20597
+ import { existsSync as existsSync34 } from "fs";
20598
+ import { readFile as readFile30 } from "fs/promises";
20599
+ import { z as z13 } from "zod";
20347
20600
  var SHA256_RE = /^[0-9a-f]{64}$/;
20348
20601
  var HEX_RE = /^#[0-9a-f]{6}$/;
20349
20602
  var MAX_REDMEAN_DISTANCE = 765;
20350
- var QualityPathSchema = z12.string().min(1).refine(
20351
- (value) => !path43.posix.isAbsolute(value) && !value.includes("\\"),
20603
+ var QualityPathSchema = z13.string().min(1).refine(
20604
+ (value) => !path44.posix.isAbsolute(value) && !value.includes("\\"),
20352
20605
  "expected a portable relative path"
20353
20606
  );
20354
20607
  var DEFAULT_QUALITY_TOLERANCES = {
@@ -20361,53 +20614,53 @@ var DEFAULT_QUALITY_TOLERANCES = {
20361
20614
  maxEdgeContrastDrop: 0.03,
20362
20615
  maxIsolatedPixelIncrease: 5e-3
20363
20616
  };
20364
- var QualityToleranceSchema = z12.object({
20365
- requireExactHash: z12.boolean(),
20366
- maxColorCountIncrease: z12.number().int().min(0),
20367
- maxNewColors: z12.number().int().min(0),
20368
- maxTransparencyDelta: z12.number().min(0).max(1),
20369
- maxPartialAlphaIncrease: z12.number().min(0).max(1),
20370
- maxEdgeDensityDelta: z12.number().min(0).max(1),
20371
- maxEdgeContrastDrop: z12.number().min(0).max(1),
20372
- maxIsolatedPixelIncrease: z12.number().min(0).max(1)
20617
+ var QualityToleranceSchema = z13.object({
20618
+ requireExactHash: z13.boolean(),
20619
+ maxColorCountIncrease: z13.number().int().min(0),
20620
+ maxNewColors: z13.number().int().min(0),
20621
+ maxTransparencyDelta: z13.number().min(0).max(1),
20622
+ maxPartialAlphaIncrease: z13.number().min(0).max(1),
20623
+ maxEdgeDensityDelta: z13.number().min(0).max(1),
20624
+ maxEdgeContrastDrop: z13.number().min(0).max(1),
20625
+ maxIsolatedPixelIncrease: z13.number().min(0).max(1)
20373
20626
  }).strict();
20374
- var ImageQualityMetricsSchema = z12.object({
20375
- sha256: z12.string().regex(SHA256_RE),
20376
- width: z12.number().int().min(1),
20377
- height: z12.number().int().min(1),
20378
- colorCount: z12.number().int().min(0),
20379
- colors: z12.array(z12.string().regex(HEX_RE)),
20380
- transparency: z12.number().min(0).max(1),
20381
- partialAlpha: z12.number().min(0).max(1),
20382
- edgeDensity: z12.number().min(0).max(1),
20383
- meanEdgeContrast: z12.number().min(0).max(1),
20384
- isolatedPixelRatio: z12.number().min(0).max(1)
20627
+ var ImageQualityMetricsSchema = z13.object({
20628
+ sha256: z13.string().regex(SHA256_RE),
20629
+ width: z13.number().int().min(1),
20630
+ height: z13.number().int().min(1),
20631
+ colorCount: z13.number().int().min(0),
20632
+ colors: z13.array(z13.string().regex(HEX_RE)),
20633
+ transparency: z13.number().min(0).max(1),
20634
+ partialAlpha: z13.number().min(0).max(1),
20635
+ edgeDensity: z13.number().min(0).max(1),
20636
+ meanEdgeContrast: z13.number().min(0).max(1),
20637
+ isolatedPixelRatio: z13.number().min(0).max(1)
20385
20638
  }).strict();
20386
- var QualityBaselineCaseSchema = z12.object({
20387
- id: z12.string().regex(/^[a-z0-9][a-z0-9/_-]*$/),
20639
+ var QualityBaselineCaseSchema = z13.object({
20640
+ id: z13.string().regex(/^[a-z0-9][a-z0-9/_-]*$/),
20388
20641
  file: QualityPathSchema,
20389
- record: z12.object({
20642
+ record: z13.object({
20390
20643
  path: QualityPathSchema,
20391
- sha256: z12.string().regex(SHA256_RE)
20644
+ sha256: z13.string().regex(SHA256_RE)
20392
20645
  }).strict().optional(),
20393
20646
  expected: ImageQualityMetricsSchema,
20394
20647
  tolerances: QualityToleranceSchema
20395
20648
  }).strict();
20396
- var QualityBaselineSchema = z12.object({
20397
- $schema: z12.string().url().optional(),
20398
- format: z12.literal("pixelkiln-quality-baseline"),
20399
- schemaVersion: z12.literal(1),
20400
- cases: z12.array(QualityBaselineCaseSchema).min(1)
20649
+ var QualityBaselineSchema = z13.object({
20650
+ $schema: z13.string().url().optional(),
20651
+ format: z13.literal("pixelkiln-quality-baseline"),
20652
+ schemaVersion: z13.literal(1),
20653
+ cases: z13.array(QualityBaselineCaseSchema).min(1)
20401
20654
  }).strict().superRefine((baseline, context) => {
20402
20655
  const ids = baseline.cases.map((entry) => entry.id);
20403
20656
  if (new Set(ids).size !== ids.length) {
20404
- context.addIssue({ code: z12.ZodIssueCode.custom, path: ["cases"], message: "duplicate case id" });
20657
+ context.addIssue({ code: z13.ZodIssueCode.custom, path: ["cases"], message: "duplicate case id" });
20405
20658
  }
20406
20659
  });
20407
- var QualityInputSchema = z12.object({
20408
- id: z12.string().regex(/^[a-z0-9][a-z0-9/_-]*$/),
20409
- path: z12.string().min(1),
20410
- record: z12.string().min(1).optional(),
20660
+ var QualityInputSchema = z13.object({
20661
+ id: z13.string().regex(/^[a-z0-9][a-z0-9/_-]*$/),
20662
+ path: z13.string().min(1),
20663
+ record: z13.string().min(1).optional(),
20411
20664
  tolerances: QualityToleranceSchema.partial().optional()
20412
20665
  }).strict();
20413
20666
  function message4(error) {
@@ -20495,10 +20748,10 @@ function measureDecodedImageQuality(png, digest2) {
20495
20748
  };
20496
20749
  }
20497
20750
  async function measureImageQuality(file) {
20498
- const absolute = path43.resolve(file);
20751
+ const absolute = path44.resolve(file);
20499
20752
  let bytes;
20500
20753
  try {
20501
- bytes = await readFile29(absolute);
20754
+ bytes = await readFile30(absolute);
20502
20755
  } catch (error) {
20503
20756
  throw new Error(`Cannot read quality image ${absolute}: ${message4(error)}`, { cause: error });
20504
20757
  }
@@ -20509,7 +20762,7 @@ async function measureImageQuality(file) {
20509
20762
  }
20510
20763
  }
20511
20764
  function resolveQualityInputs(raw, inputsFilePath) {
20512
- const parsed = z12.array(QualityInputSchema).min(1).safeParse(raw);
20765
+ const parsed = z13.array(QualityInputSchema).min(1).safeParse(raw);
20513
20766
  if (!parsed.success) {
20514
20767
  throw new Error(
20515
20768
  "--inputs must be a non-empty JSON array of quality cases:\n" + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
@@ -20517,11 +20770,11 @@ function resolveQualityInputs(raw, inputsFilePath) {
20517
20770
  }
20518
20771
  const ids = parsed.data.map((entry) => entry.id);
20519
20772
  if (new Set(ids).size !== ids.length) throw new Error("--inputs contains a duplicate quality case id.");
20520
- const root = path43.dirname(path43.resolve(inputsFilePath));
20773
+ const root = path44.dirname(path44.resolve(inputsFilePath));
20521
20774
  return parsed.data.map((entry) => ({
20522
20775
  id: entry.id,
20523
- file: path43.resolve(root, entry.path),
20524
- ...entry.record ? { record: path43.resolve(root, entry.record) } : {},
20776
+ file: path44.resolve(root, entry.path),
20777
+ ...entry.record ? { record: path44.resolve(root, entry.record) } : {},
20525
20778
  tolerances: QualityToleranceSchema.parse({
20526
20779
  ...DEFAULT_QUALITY_TOLERANCES,
20527
20780
  ...entry.tolerances
@@ -20529,7 +20782,7 @@ function resolveQualityInputs(raw, inputsFilePath) {
20529
20782
  }));
20530
20783
  }
20531
20784
  function portableRelative2(from, to) {
20532
- return path43.relative(from, path43.resolve(to)).split(path43.sep).join("/") || ".";
20785
+ return path44.relative(from, path44.resolve(to)).split(path44.sep).join("/") || ".";
20533
20786
  }
20534
20787
  async function verifyRecordForImage(recordPath, imagePath) {
20535
20788
  const verification = await verifyArtifactBundle(recordPath);
@@ -20544,20 +20797,20 @@ async function verifyRecordForImage(recordPath, imagePath) {
20544
20797
  if (record.kind !== "refine" || options === null || typeof options !== "object" || options.schema !== "pixelkiln-quality") {
20545
20798
  throw new Error(`Quality record ${recordPath} is not a PixelKiln refinement record.`);
20546
20799
  }
20547
- const outputs = record.outputs.map((output) => path43.resolve(path43.dirname(recordPath), output.path));
20548
- if (!outputs.includes(path43.resolve(imagePath))) {
20800
+ const outputs = record.outputs.map((output) => path44.resolve(path44.dirname(recordPath), output.path));
20801
+ if (!outputs.includes(path44.resolve(imagePath))) {
20549
20802
  throw new Error(`Quality record ${recordPath} does not own ${imagePath}.`);
20550
20803
  }
20551
20804
  }
20552
20805
  async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
20553
20806
  if (!inputs.length) throw new Error("Cannot snapshot an empty quality baseline.");
20554
- const absolute = path43.resolve(baselinePath);
20807
+ const absolute = path44.resolve(baselinePath);
20555
20808
  for (const input of inputs) {
20556
- if (path43.resolve(input.file) === absolute || input.record && path43.resolve(input.record) === absolute) {
20809
+ if (path44.resolve(input.file) === absolute || input.record && path44.resolve(input.record) === absolute) {
20557
20810
  throw new Error(`Quality baseline output would overwrite an input: ${absolute}.`);
20558
20811
  }
20559
20812
  }
20560
- const root = path43.dirname(absolute);
20813
+ const root = path44.dirname(absolute);
20561
20814
  const cases = [];
20562
20815
  for (const input of [...inputs].sort((a, b) => a.id.localeCompare(b.id))) {
20563
20816
  const expected = await measureImageQuality(input.file);
@@ -20584,8 +20837,8 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
20584
20837
  cases
20585
20838
  });
20586
20839
  const data = Buffer.from(JSON.stringify(baseline, null, 2) + "\n");
20587
- if (existsSync33(absolute) && !options.force) {
20588
- const current = await readFile29(absolute);
20840
+ if (existsSync34(absolute) && !options.force) {
20841
+ const current = await readFile30(absolute);
20589
20842
  if (!current.equals(data)) {
20590
20843
  throw new OverwriteRefusedError(
20591
20844
  `Quality baseline already exists with different content: ${absolute}. Pass --force to replace it.`,
@@ -20597,10 +20850,10 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
20597
20850
  return { baseline, path: absolute, changed: result.changed.length > 0 };
20598
20851
  }
20599
20852
  async function readQualityBaseline(baselinePath) {
20600
- const absolute = path43.resolve(baselinePath);
20853
+ const absolute = path44.resolve(baselinePath);
20601
20854
  let raw;
20602
20855
  try {
20603
- raw = JSON.parse(await readFile29(absolute, "utf8"));
20856
+ raw = JSON.parse(await readFile30(absolute, "utf8"));
20604
20857
  } catch (error) {
20605
20858
  throw new Error(`Could not read quality baseline ${absolute}: ${message4(error)}`, { cause: error });
20606
20859
  }
@@ -20670,12 +20923,12 @@ function regressionViolations(expected, actual, tolerances) {
20670
20923
  return { violations, warnings };
20671
20924
  }
20672
20925
  async function checkQualityBaseline(baselinePath) {
20673
- const absolute = path43.resolve(baselinePath);
20926
+ const absolute = path44.resolve(baselinePath);
20674
20927
  const baseline = await readQualityBaseline(absolute);
20675
- const root = path43.dirname(absolute);
20928
+ const root = path44.dirname(absolute);
20676
20929
  const cases = [];
20677
20930
  for (const entry of baseline.cases) {
20678
- const file = path43.resolve(root, entry.file);
20931
+ const file = path44.resolve(root, entry.file);
20679
20932
  let actual = null;
20680
20933
  const violations = [];
20681
20934
  const warnings = [];
@@ -20688,7 +20941,7 @@ async function checkQualityBaseline(baselinePath) {
20688
20941
  violations.push(message4(error));
20689
20942
  }
20690
20943
  if (entry.record) {
20691
- const recordPath = path43.resolve(root, entry.record.path);
20944
+ const recordPath = path44.resolve(root, entry.record.path);
20692
20945
  try {
20693
20946
  const recordHash = await sha256File(recordPath);
20694
20947
  if (recordHash !== entry.record.sha256) violations.push("quality record hash changed");
@@ -20730,15 +20983,15 @@ async function runQuality(args) {
20730
20983
  if (!args.out) throw new Error("quality snapshot needs --out <pixelkiln.quality.json>.");
20731
20984
  let raw;
20732
20985
  try {
20733
- raw = JSON.parse(await readFile30(path44.resolve(args.inputs), "utf8"));
20986
+ raw = JSON.parse(await readFile31(path45.resolve(args.inputs), "utf8"));
20734
20987
  } catch (error) {
20735
20988
  throw new Error(
20736
- `Could not read quality inputs ${path44.resolve(args.inputs)}: ${error instanceof Error ? error.message : String(error)}`,
20989
+ `Could not read quality inputs ${path45.resolve(args.inputs)}: ${error instanceof Error ? error.message : String(error)}`,
20737
20990
  { cause: error }
20738
20991
  );
20739
20992
  }
20740
20993
  const inputs = resolveQualityInputs(raw, args.inputs);
20741
- if (path44.resolve(args.inputs) === path44.resolve(args.out)) {
20994
+ if (path45.resolve(args.inputs) === path45.resolve(args.out)) {
20742
20995
  throw new Error("quality snapshot --out must not overwrite its --inputs file.");
20743
20996
  }
20744
20997
  const result = await snapshotQualityBaseline(inputs, args.out, { force: args.force });
@@ -20751,7 +21004,7 @@ async function runQuality(args) {
20751
21004
  }, null, 2));
20752
21005
  } else {
20753
21006
  log(
20754
- ` ${result.changed ? "wrote" : "unchanged"} ${path44.relative(process.cwd(), result.path)} (${result.baseline.cases.length} case(s))`
21007
+ ` ${result.changed ? "wrote" : "unchanged"} ${path45.relative(process.cwd(), result.path)} (${result.baseline.cases.length} case(s))`
20755
21008
  );
20756
21009
  log(` Review the tolerances, commit the baseline, then run quality check in CI.`);
20757
21010
  }
@@ -20777,54 +21030,54 @@ async function runQuality(args) {
20777
21030
  }
20778
21031
 
20779
21032
  // src/cli/commands/recipe.ts
20780
- import path46 from "path";
21033
+ import path47 from "path";
20781
21034
 
20782
21035
  // src/recipes.ts
20783
- import path45 from "path";
20784
- import { existsSync as existsSync34 } from "fs";
20785
- import { readdir as readdir3, readFile as readFile31 } from "fs/promises";
21036
+ import path46 from "path";
21037
+ import { existsSync as existsSync35 } from "fs";
21038
+ import { readdir as readdir3, readFile as readFile32 } from "fs/promises";
20786
21039
  import { fileURLToPath as fileURLToPath2 } from "url";
20787
- import { z as z13 } from "zod";
21040
+ import { z as z14 } from "zod";
20788
21041
  var SHA256_RE2 = /^[0-9a-f]{64}$/;
20789
21042
  var RECIPE_ID_RE = /^[a-z0-9][a-z0-9-]*\/[a-z0-9][a-z0-9-]*$/;
20790
21043
  var VERSION_RE = /^\d+\.\d+\.\d+$/;
20791
21044
  var RECIPE_FILE = "pixelkiln.recipe.json";
20792
21045
  var RECIPE_DIR_TOKEN = "{{recipeDir}}";
20793
- var PortablePathSchema = z13.string().min(1).refine((value) => {
20794
- if (value.includes("\\") || path45.posix.isAbsolute(value)) return false;
21046
+ var PortablePathSchema = z14.string().min(1).refine((value) => {
21047
+ if (value.includes("\\") || path46.posix.isAbsolute(value)) return false;
20795
21048
  const parts = value.split("/");
20796
21049
  return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
20797
21050
  }, "expected a portable relative path without '.', '..', or backslashes");
20798
- var RecipeBindingSchema = z13.object({
20799
- nodeId: z13.string().min(1),
20800
- input: z13.string().min(1)
21051
+ var RecipeBindingSchema = z14.object({
21052
+ nodeId: z14.string().min(1),
21053
+ input: z14.string().min(1)
20801
21054
  }).strict();
20802
- var RecipeSchema = z13.object({
20803
- $schema: z13.string().url().optional(),
20804
- format: z13.literal("pixelkiln-recipe"),
20805
- schemaVersion: z13.literal(1),
20806
- id: z13.string().regex(RECIPE_ID_RE, "expected provider/name"),
20807
- version: z13.string().regex(VERSION_RE, "expected x.y.z"),
20808
- provider: z13.string().regex(/^[a-z0-9][a-z0-9-]*$/),
20809
- summary: z13.string().min(1).max(240),
20810
- files: z13.array(z13.object({
21055
+ var RecipeSchema = z14.object({
21056
+ $schema: z14.string().url().optional(),
21057
+ format: z14.literal("pixelkiln-recipe"),
21058
+ schemaVersion: z14.literal(1),
21059
+ id: z14.string().regex(RECIPE_ID_RE, "expected provider/name"),
21060
+ version: z14.string().regex(VERSION_RE, "expected x.y.z"),
21061
+ provider: z14.string().regex(/^[a-z0-9][a-z0-9-]*$/),
21062
+ summary: z14.string().min(1).max(240),
21063
+ files: z14.array(z14.object({
20811
21064
  path: PortablePathSchema,
20812
- role: z13.enum(["workflow", "reference"]),
20813
- sha256: z13.string().regex(SHA256_RE2)
21065
+ role: z14.enum(["workflow", "reference"]),
21066
+ sha256: z14.string().regex(SHA256_RE2)
20814
21067
  }).strict()).default([]),
20815
- models: z13.array(z13.object({
21068
+ models: z14.array(z14.object({
20816
21069
  path: PortablePathSchema,
20817
- sha256: z13.string().regex(SHA256_RE2),
20818
- source: z13.string().url(),
20819
- license: z13.string().min(1)
21070
+ sha256: z14.string().regex(SHA256_RE2),
21071
+ source: z14.string().url(),
21072
+ license: z14.string().min(1)
20820
21073
  }).strict()).default([]),
20821
- styleId: z13.string().regex(/^[a-z0-9][a-z0-9-]*$/),
21074
+ styleId: z14.string().regex(/^[a-z0-9][a-z0-9-]*$/),
20822
21075
  style: StyleSchema,
20823
- workflow: z13.object({
21076
+ workflow: z14.object({
20824
21077
  path: PortablePathSchema,
20825
- outputNodeId: z13.string().min(1),
20826
- numImages: z13.number().int().min(1).max(16),
20827
- bindings: z13.object({
21078
+ outputNodeId: z14.string().min(1),
21079
+ numImages: z14.number().int().min(1).max(16),
21080
+ bindings: z14.object({
20828
21081
  prompt: RecipeBindingSchema,
20829
21082
  width: RecipeBindingSchema.optional(),
20830
21083
  height: RecipeBindingSchema.optional(),
@@ -20835,67 +21088,67 @@ var RecipeSchema = z13.object({
20835
21088
  strength: RecipeBindingSchema.optional()
20836
21089
  }).strict()
20837
21090
  }).strict().optional(),
20838
- quality: z13.object({
20839
- stage: z13.enum(["composition-source", "production-candidate"]),
20840
- workingCanvas: z13.object({
20841
- width: z13.number().int().min(16),
20842
- height: z13.number().int().min(16)
21091
+ quality: z14.object({
21092
+ stage: z14.enum(["composition-source", "production-candidate"]),
21093
+ workingCanvas: z14.object({
21094
+ width: z14.number().int().min(16),
21095
+ height: z14.number().int().min(16)
20843
21096
  }).strict(),
20844
- recommendedNativeSize: z13.object({
20845
- min: z13.number().int().min(1),
20846
- max: z13.number().int().min(1)
21097
+ recommendedNativeSize: z14.object({
21098
+ min: z14.number().int().min(1),
21099
+ max: z14.number().int().min(1)
20847
21100
  }).strict(),
20848
- paletteColors: z13.object({
20849
- min: z13.number().int().min(1),
20850
- max: z13.number().int().min(1)
21101
+ paletteColors: z14.object({
21102
+ min: z14.number().int().min(1),
21103
+ max: z14.number().int().min(1)
20851
21104
  }).strict(),
20852
- background: z13.enum(["opaque", "transparent", "full-bleed"]),
20853
- checks: z13.array(z13.enum([
21105
+ background: z14.enum(["opaque", "transparent", "full-bleed"]),
21106
+ checks: z14.array(z14.enum([
20854
21107
  "prompt-coverage",
20855
21108
  "native-grid",
20856
21109
  "final-palette",
20857
21110
  "alpha-edge",
20858
21111
  "human-1x"
20859
21112
  ])).min(1),
20860
- notes: z13.array(z13.string().min(1)).default([])
21113
+ notes: z14.array(z14.string().min(1)).default([])
20861
21114
  }).strict(),
20862
- integrity: z13.object({
20863
- algorithm: z13.literal("sha256"),
20864
- digest: z13.string().regex(SHA256_RE2)
21115
+ integrity: z14.object({
21116
+ algorithm: z14.literal("sha256"),
21117
+ digest: z14.string().regex(SHA256_RE2)
20865
21118
  }).strict()
20866
21119
  }).strict().superRefine((recipe, context) => {
20867
21120
  if (recipe.style.provider && recipe.style.provider !== recipe.provider) {
20868
21121
  context.addIssue({
20869
- code: z13.ZodIssueCode.custom,
21122
+ code: z14.ZodIssueCode.custom,
20870
21123
  path: ["style", "provider"],
20871
21124
  message: "must match the recipe provider"
20872
21125
  });
20873
21126
  }
20874
21127
  const filePaths = recipe.files.map((file) => file.path);
20875
21128
  if (new Set(filePaths).size !== filePaths.length) {
20876
- context.addIssue({ code: z13.ZodIssueCode.custom, path: ["files"], message: "duplicate file path" });
21129
+ context.addIssue({ code: z14.ZodIssueCode.custom, path: ["files"], message: "duplicate file path" });
20877
21130
  }
20878
21131
  const modelPaths = recipe.models.map((model) => model.path);
20879
21132
  if (new Set(modelPaths).size !== modelPaths.length) {
20880
- context.addIssue({ code: z13.ZodIssueCode.custom, path: ["models"], message: "duplicate model path" });
21133
+ context.addIssue({ code: z14.ZodIssueCode.custom, path: ["models"], message: "duplicate model path" });
20881
21134
  }
20882
21135
  if (filePaths.includes(RECIPE_FILE)) {
20883
21136
  context.addIssue({
20884
- code: z13.ZodIssueCode.custom,
21137
+ code: z14.ZodIssueCode.custom,
20885
21138
  path: ["files"],
20886
21139
  message: `${RECIPE_FILE} cannot hash itself`
20887
21140
  });
20888
21141
  }
20889
21142
  if (recipe.quality.recommendedNativeSize.min > recipe.quality.recommendedNativeSize.max) {
20890
21143
  context.addIssue({
20891
- code: z13.ZodIssueCode.custom,
21144
+ code: z14.ZodIssueCode.custom,
20892
21145
  path: ["quality", "recommendedNativeSize"],
20893
21146
  message: "min cannot exceed max"
20894
21147
  });
20895
21148
  }
20896
21149
  if (recipe.quality.paletteColors.min > recipe.quality.paletteColors.max) {
20897
21150
  context.addIssue({
20898
- code: z13.ZodIssueCode.custom,
21151
+ code: z14.ZodIssueCode.custom,
20899
21152
  path: ["quality", "paletteColors"],
20900
21153
  message: "min cannot exceed max"
20901
21154
  });
@@ -20904,7 +21157,7 @@ var RecipeSchema = z13.object({
20904
21157
  (file) => file.path === recipe.workflow.path && file.role === "workflow"
20905
21158
  )) {
20906
21159
  context.addIssue({
20907
- code: z13.ZodIssueCode.custom,
21160
+ code: z14.ZodIssueCode.custom,
20908
21161
  path: ["workflow", "path"],
20909
21162
  message: "must name a file whose role is workflow"
20910
21163
  });
@@ -20912,7 +21165,7 @@ var RecipeSchema = z13.object({
20912
21165
  if (recipe.provider === "comfyui") {
20913
21166
  if (!recipe.workflow) {
20914
21167
  context.addIssue({
20915
- code: z13.ZodIssueCode.custom,
21168
+ code: z14.ZodIssueCode.custom,
20916
21169
  path: ["workflow"],
20917
21170
  message: "ComfyUI recipes require workflow metadata"
20918
21171
  });
@@ -20921,7 +21174,7 @@ var RecipeSchema = z13.object({
20921
21174
  const options = recipe.style.providerOptions.comfyui;
20922
21175
  if (!options || typeof options !== "object" || Array.isArray(options)) {
20923
21176
  context.addIssue({
20924
- code: z13.ZodIssueCode.custom,
21177
+ code: z14.ZodIssueCode.custom,
20925
21178
  path: ["style", "providerOptions", "comfyui"],
20926
21179
  message: "ComfyUI recipes require ComfyUI provider options"
20927
21180
  });
@@ -20935,7 +21188,7 @@ var RecipeSchema = z13.object({
20935
21188
  };
20936
21189
  if (JSON.stringify(canonical3(options)) !== JSON.stringify(canonical3(expected))) {
20937
21190
  context.addIssue({
20938
- code: z13.ZodIssueCode.custom,
21191
+ code: z14.ZodIssueCode.custom,
20939
21192
  path: ["style", "providerOptions", "comfyui"],
20940
21193
  message: "must match workflow metadata and use {{recipeDir}} for workflowFile"
20941
21194
  });
@@ -20967,10 +21220,10 @@ function message5(error) {
20967
21220
  return error instanceof Error ? error.message : String(error);
20968
21221
  }
20969
21222
  async function readRecipeFile(recipePath, bundled) {
20970
- const absolute = path45.resolve(recipePath);
21223
+ const absolute = path46.resolve(recipePath);
20971
21224
  let raw;
20972
21225
  try {
20973
- raw = JSON.parse(await readFile31(absolute, "utf8"));
21226
+ raw = JSON.parse(await readFile32(absolute, "utf8"));
20974
21227
  } catch (error) {
20975
21228
  throw new Error(`Could not read recipe ${absolute}: ${message5(error)}`, { cause: error });
20976
21229
  }
@@ -20981,13 +21234,13 @@ async function readRecipeFile(recipePath, bundled) {
20981
21234
  ` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
20982
21235
  );
20983
21236
  }
20984
- return { recipe: parsed.data, authored: raw, path: absolute, dir: path45.dirname(absolute), bundled };
21237
+ return { recipe: parsed.data, authored: raw, path: absolute, dir: path46.dirname(absolute), bundled };
20985
21238
  }
20986
21239
  async function walkRecipeFiles(root) {
20987
- if (!existsSync34(root)) return [];
21240
+ if (!existsSync35(root)) return [];
20988
21241
  const out = [];
20989
21242
  for (const entry of await readdir3(root, { withFileTypes: true })) {
20990
- const resolved = path45.join(root, entry.name);
21243
+ const resolved = path46.join(root, entry.name);
20991
21244
  if (entry.isDirectory()) out.push(...await walkRecipeFiles(resolved));
20992
21245
  else if (entry.isFile() && entry.name === RECIPE_FILE) out.push(resolved);
20993
21246
  }
@@ -20998,7 +21251,7 @@ function bundledRecipeRoot() {
20998
21251
  }
20999
21252
  async function listBundledRecipes(root = bundledRecipeRoot()) {
21000
21253
  const recipes = await Promise.all(
21001
- (await walkRecipeFiles(path45.resolve(root))).map((file) => readRecipeFile(file, true))
21254
+ (await walkRecipeFiles(path46.resolve(root))).map((file) => readRecipeFile(file, true))
21002
21255
  );
21003
21256
  return recipes.sort(
21004
21257
  (a, b) => a.recipe.id.localeCompare(b.recipe.id) || compareVersions(b.recipe.version, a.recipe.version)
@@ -21021,9 +21274,9 @@ function parseSelector(selector) {
21021
21274
  return { id: match[1], version: match[2] };
21022
21275
  }
21023
21276
  async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
21024
- const local = path45.resolve(target);
21025
- if (existsSync34(local)) {
21026
- const recipePath = path45.basename(local) === RECIPE_FILE ? local : path45.join(local, RECIPE_FILE);
21277
+ const local = path46.resolve(target);
21278
+ if (existsSync35(local)) {
21279
+ const recipePath = path46.basename(local) === RECIPE_FILE ? local : path46.join(local, RECIPE_FILE);
21027
21280
  return readRecipeFile(recipePath, false);
21028
21281
  }
21029
21282
  if (target.startsWith(".") || target.startsWith("/") || target.includes("\\")) {
@@ -21037,8 +21290,8 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
21037
21290
  return matches[0];
21038
21291
  }
21039
21292
  async function verifyFile(root, file) {
21040
- const absolute = path45.resolve(root, ...file.path.split("/"));
21041
- if (!existsSync34(absolute)) {
21293
+ const absolute = path46.resolve(root, ...file.path.split("/"));
21294
+ if (!existsSync35(absolute)) {
21042
21295
  return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
21043
21296
  }
21044
21297
  const actualSha256 = await sha256File(absolute);
@@ -21053,7 +21306,7 @@ async function verifyRecipe(target, options = {}) {
21053
21306
  const loaded = await resolveRecipe(target, options.bundledRoot);
21054
21307
  const actualIntegrity = recipeDigest(loaded.authored);
21055
21308
  const files = await Promise.all(loaded.recipe.files.map((file) => verifyFile(loaded.dir, file)));
21056
- const modelRoot = options.modelRoot ? path45.resolve(options.modelRoot) : null;
21309
+ const modelRoot = options.modelRoot ? path46.resolve(options.modelRoot) : null;
21057
21310
  const models = modelRoot ? await Promise.all(loaded.recipe.models.map(async (model) => ({
21058
21311
  ...await verifyFile(modelRoot, model),
21059
21312
  source: model.source,
@@ -21087,7 +21340,7 @@ async function verifyRecipe(target, options = {}) {
21087
21340
  };
21088
21341
  }
21089
21342
  function renderStyle(style, destination, cwd) {
21090
- const portableDir = path45.relative(cwd, destination).split(path45.sep).join("/") || ".";
21343
+ const portableDir = path46.relative(cwd, destination).split(path46.sep).join("/") || ".";
21091
21344
  const replace = (value) => {
21092
21345
  if (typeof value === "string") return value.replaceAll(RECIPE_DIR_TOKEN, portableDir);
21093
21346
  if (Array.isArray(value)) return value.map(replace);
@@ -21099,25 +21352,25 @@ function renderStyle(style, destination, cwd) {
21099
21352
  return replace(style);
21100
21353
  }
21101
21354
  async function installRecipe(target, options = {}) {
21102
- const cwd = path45.resolve(options.cwd ?? process.cwd());
21355
+ const cwd = path46.resolve(options.cwd ?? process.cwd());
21103
21356
  const loaded = await resolveRecipe(target, options.bundledRoot);
21104
21357
  const verification = await verifyRecipe(loaded.path);
21105
21358
  if (!verification.ok) {
21106
21359
  throw new Error(`Recipe ${loaded.recipe.id}@${loaded.recipe.version} failed integrity verification.`);
21107
21360
  }
21108
- const destination = options.out ? path45.resolve(cwd, options.out) : path45.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
21361
+ const destination = options.out ? path46.resolve(cwd, options.out) : path46.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
21109
21362
  const sources = [
21110
21363
  { path: RECIPE_FILE, source: loaded.path },
21111
- ...loaded.recipe.files.map((file) => ({ path: file.path, source: path45.join(loaded.dir, ...file.path.split("/")) }))
21364
+ ...loaded.recipe.files.map((file) => ({ path: file.path, source: path46.join(loaded.dir, ...file.path.split("/")) }))
21112
21365
  ];
21113
21366
  const files = await Promise.all(sources.map(async (file) => ({
21114
- path: path45.join(destination, ...file.path.split("/")),
21115
- data: await readFile31(file.source)
21367
+ path: path46.join(destination, ...file.path.split("/")),
21368
+ data: await readFile32(file.source)
21116
21369
  })));
21117
21370
  if (!options.force) {
21118
21371
  for (const file of files) {
21119
- if (!existsSync34(file.path)) continue;
21120
- const current = await readFile31(file.path);
21372
+ if (!existsSync35(file.path)) continue;
21373
+ const current = await readFile32(file.path);
21121
21374
  if (!current.equals(file.data)) {
21122
21375
  throw new OverwriteRefusedError(
21123
21376
  `Recipe destination has local changes: ${file.path}. Choose another --out or pass --force to replace declared recipe files.`,
@@ -21211,14 +21464,14 @@ async function runRecipe(args) {
21211
21464
  }, null, 2));
21212
21465
  } else {
21213
21466
  log(` installed ${result.recipe.id}@${result.recipe.version}`);
21214
- log(` ${path46.relative(process.cwd(), result.destination) || "."}`);
21467
+ log(` ${path47.relative(process.cwd(), result.destination) || "."}`);
21215
21468
  log(`
21216
21469
  Add this entry under your manifest's styles object:`);
21217
21470
  log(JSON.stringify({ [result.styleId]: result.style }, null, 2));
21218
21471
  if (result.recipe.models.length) {
21219
21472
  log(`
21220
21473
  Models are not downloaded automatically. Verify them with:`);
21221
- log(` pixelkiln recipe verify ${path46.relative(process.cwd(), result.destination)} --model-root <ComfyUI/models>`);
21474
+ log(` pixelkiln recipe verify ${path47.relative(process.cwd(), result.destination)} --model-root <ComfyUI/models>`);
21222
21475
  }
21223
21476
  }
21224
21477
  return;
@@ -21226,7 +21479,7 @@ async function runRecipe(args) {
21226
21479
  }
21227
21480
 
21228
21481
  // src/cli/commands/refine.ts
21229
- import path47 from "path";
21482
+ import path48 from "path";
21230
21483
  async function runRefine(args) {
21231
21484
  if (args.from || args.subcommand === "approve") {
21232
21485
  if (!args.from) {
@@ -21256,12 +21509,12 @@ async function runRefine(args) {
21256
21509
  ` recovered ${result3.detection.columns}x${result3.detection.rows} native grid (${result3.detection.confidence}: ${result3.detection.consensus})`
21257
21510
  );
21258
21511
  log(` enforced ${result3.palette.length}-color palette without dithering`);
21259
- log(` wrote ${path47.relative(process.cwd(), result3.output)}`);
21260
- log(` quality record: ${path47.relative(process.cwd(), result3.record)}`);
21512
+ log(` wrote ${path48.relative(process.cwd(), result3.output)}`);
21513
+ log(` quality record: ${path48.relative(process.cwd(), result3.record)}`);
21261
21514
  log(`
21262
21515
  Automated checks passed. Human 1\xD7 review is still required:`);
21263
21516
  log(
21264
- ` pixelkiln refine approve --from ${path47.relative(process.cwd(), result3.record)} --reviewer "Your Name"`
21517
+ ` pixelkiln refine approve --from ${path48.relative(process.cwd(), result3.record)} --reviewer "Your Name"`
21265
21518
  );
21266
21519
  }
21267
21520
  return;
@@ -21289,7 +21542,7 @@ async function runRefine(args) {
21289
21542
  if (args.json) log(JSON.stringify(result3, null, 2));
21290
21543
  else {
21291
21544
  log(
21292
- result3.options.frameSet ? ` approved ${result3.outputs.length} frames in ${path47.relative(process.cwd(), result3.record)} by ${args.reviewer.trim()}` : ` approved ${path47.relative(process.cwd(), result3.output)} by ${args.reviewer.trim()}`
21545
+ result3.options.frameSet ? ` approved ${result3.outputs.length} frames in ${path48.relative(process.cwd(), result3.record)} by ${args.reviewer.trim()}` : ` approved ${path48.relative(process.cwd(), result3.output)} by ${args.reviewer.trim()}`
21293
21546
  );
21294
21547
  }
21295
21548
  return;
@@ -21298,7 +21551,7 @@ async function runRefine(args) {
21298
21551
  if (args.json) {
21299
21552
  log(JSON.stringify(result2, null, 2));
21300
21553
  } else {
21301
- log(` ${result2.safe ? "release-ready" : "not release-ready"}: ${path47.relative(process.cwd(), result2.output || result2.record)}`);
21554
+ log(` ${result2.safe ? "release-ready" : "not release-ready"}: ${path48.relative(process.cwd(), result2.output || result2.record)}`);
21302
21555
  for (const reason of result2.reasons) log(` ${reason}`);
21303
21556
  }
21304
21557
  if (!result2.safe) process.exitCode = 1;
@@ -21344,7 +21597,7 @@ async function runRefine(args) {
21344
21597
  log(` ${item.state.padEnd(16)} ${item.key} ${item.reason}`);
21345
21598
  if (item.state === "needs-approval") {
21346
21599
  log(
21347
- ` pixelkiln refine approve --from ${path47.relative(process.cwd(), item.record)} --reviewer "Your Name"`
21600
+ ` pixelkiln refine approve --from ${path48.relative(process.cwd(), item.record)} --reviewer "Your Name"`
21348
21601
  );
21349
21602
  }
21350
21603
  }
@@ -21355,9 +21608,9 @@ async function runRefine(args) {
21355
21608
  }
21356
21609
 
21357
21610
  // src/cli/commands/skeleton.ts
21358
- import path48 from "path";
21359
- import { existsSync as existsSync35 } from "fs";
21360
- import { readFile as readFile32, writeFile as writeFile18 } from "fs/promises";
21611
+ import path49 from "path";
21612
+ import { existsSync as existsSync36 } from "fs";
21613
+ import { readFile as readFile33, writeFile as writeFile19 } from "fs/promises";
21361
21614
 
21362
21615
  // src/skeleton-preview.ts
21363
21616
  var RIGHT = [255, 140, 40, 255];
@@ -21468,12 +21721,12 @@ var Canvas = class {
21468
21721
  async function runEstimateSkeleton(args) {
21469
21722
  const image = args.target;
21470
21723
  if (!image) throw new UsageError("estimate-skeleton needs an image path: pixelkiln estimate-skeleton <image> [--out keypoints.json]");
21471
- if (args.out && existsSync35(path48.resolve(args.out)) && !args.force) {
21724
+ if (args.out && existsSync36(path49.resolve(args.out)) && !args.force) {
21472
21725
  throw new UsageError(`${args.out} already exists; pass --force to replace it`);
21473
21726
  }
21474
- loadEnvFiles(path48.dirname(path48.resolve(args.manifest)));
21727
+ loadEnvFiles(path49.dirname(path49.resolve(args.manifest)));
21475
21728
  loadEnvFiles(process.cwd());
21476
- const bytes = await readFile32(path48.resolve(image));
21729
+ const bytes = await readFile33(path49.resolve(image));
21477
21730
  const metadata = imageMetadata(bytes);
21478
21731
  if (!metadata) throw new UsageError(`${image} is not a readable PNG or JPEG`);
21479
21732
  const sizeProblem = estimateSkeletonSizeProblem(metadata.width, metadata.height);
@@ -21485,7 +21738,7 @@ async function runEstimateSkeleton(args) {
21485
21738
  const frames = args.frames ?? DEFAULT_SCAFFOLD_FRAMES;
21486
21739
  const json = JSON.stringify(scaffoldSkeletonSet(res.keypoints, frames), null, 2);
21487
21740
  if (args.out) {
21488
- await writeFile18(path48.resolve(args.out), json + "\n");
21741
+ await writeFile19(path49.resolve(args.out), json + "\n");
21489
21742
  log(` wrote ${args.out}: the estimated pose and ${frames} frames to edit`);
21490
21743
  log(` preview it with: pixelkiln skeleton-preview ${args.out} --from ${image}`);
21491
21744
  } else {
@@ -21498,8 +21751,8 @@ async function runSkeletonPreview(args) {
21498
21751
  let sourceFile;
21499
21752
  let label;
21500
21753
  if (target.endsWith(".json")) {
21501
- keypointsFile = path48.resolve(target);
21502
- sourceFile = args.from ? path48.resolve(args.from) : void 0;
21754
+ keypointsFile = path49.resolve(target);
21755
+ sourceFile = args.from ? path49.resolve(args.from) : void 0;
21503
21756
  label = target;
21504
21757
  } else {
21505
21758
  const loaded = await loadManifest(args.manifest);
@@ -21511,31 +21764,31 @@ async function runSkeletonPreview(args) {
21511
21764
  );
21512
21765
  }
21513
21766
  keypointsFile = spec.revision.keypointsFile;
21514
- sourceFile = args.from ? path48.resolve(args.from) : spec.revision.sourceFile;
21767
+ sourceFile = args.from ? path49.resolve(args.from) : spec.revision.sourceFile;
21515
21768
  label = `${spec.styleId}/${spec.assetId}`;
21516
21769
  }
21517
- if (!existsSync35(keypointsFile)) throw new UsageError(`Keypoints file does not exist yet: ${keypointsFile}`);
21770
+ if (!existsSync36(keypointsFile)) throw new UsageError(`Keypoints file does not exist yet: ${keypointsFile}`);
21518
21771
  let json;
21519
21772
  try {
21520
- json = JSON.parse(await readFile32(keypointsFile, "utf8"));
21773
+ json = JSON.parse(await readFile33(keypointsFile, "utf8"));
21521
21774
  } catch (error) {
21522
21775
  throw new UsageError(`${keypointsFile} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
21523
21776
  }
21524
21777
  const set = parseSkeletonSet(json, keypointsFile);
21525
21778
  let source;
21526
21779
  if (sourceFile) {
21527
- if (!existsSync35(sourceFile)) {
21780
+ if (!existsSync36(sourceFile)) {
21528
21781
  log(` source image does not exist yet, drawing on a plain ground: ${sourceFile}`);
21529
21782
  } else {
21530
- const bytes = await readFile32(sourceFile);
21783
+ const bytes = await readFile33(sourceFile);
21531
21784
  if (imageMetadata(bytes)?.format !== "png") throw new UsageError(`${sourceFile} must be a PNG to preview over`);
21532
21785
  source = decodePng(bytes);
21533
21786
  }
21534
21787
  }
21535
- const out = path48.resolve(args.out ?? keypointsFile.replace(/\.json$/, "") + ".preview.png");
21536
- await writeFile18(out, renderSkeletonSheet(set, { source }));
21788
+ const out = path49.resolve(args.out ?? keypointsFile.replace(/\.json$/, "") + ".preview.png");
21789
+ await writeFile19(out, renderSkeletonSheet(set, { source }));
21537
21790
  log(` ${label}: ${set.frames.length} frames after the starting pose`);
21538
- const shown = path48.relative(process.cwd(), out);
21791
+ const shown = path49.relative(process.cwd(), out);
21539
21792
  log(` wrote ${shown.startsWith("..") ? out : shown}`);
21540
21793
  }
21541
21794
 
@@ -21581,15 +21834,15 @@ async function runTools(args) {
21581
21834
  }
21582
21835
 
21583
21836
  // src/cli/commands/workspace.ts
21584
- import path49 from "path";
21585
- import { existsSync as existsSync36 } from "fs";
21837
+ import path50 from "path";
21838
+ import { existsSync as existsSync37 } from "fs";
21586
21839
  async function runWorkspace(args) {
21587
- const workspacePath = path49.resolve(args.workspace ?? "pixelkiln.workspace.json");
21588
- const dir = path49.dirname(workspacePath);
21840
+ const workspacePath = path50.resolve(args.workspace ?? "pixelkiln.workspace.json");
21841
+ const dir = path50.dirname(workspacePath);
21589
21842
  if (args.subcommand === "add") {
21590
- const manifestPath = path49.resolve(args.target);
21843
+ const manifestPath = path50.resolve(args.target);
21591
21844
  const loadedTarget = await loadManifest(manifestPath);
21592
- const lockPath = args.explicitLock ? path49.resolve(args.explicitLock) : path49.join(path49.dirname(manifestPath), "pixelkiln.lock.json");
21845
+ const lockPath = args.explicitLock ? path50.resolve(args.explicitLock) : path50.join(path50.dirname(manifestPath), "pixelkiln.lock.json");
21593
21846
  const ws = await loadWorkspace(workspacePath);
21594
21847
  const id = args.name ?? loadedTarget.manifest.name;
21595
21848
  if (ws.projects.some((p) => p.id === id)) {
@@ -21609,10 +21862,10 @@ async function runWorkspace(args) {
21609
21862
  ...args.account ? { account: args.account } : {}
21610
21863
  };
21611
21864
  await saveWorkspace(workspacePath, { version: 1, projects: [...ws.projects, project] });
21612
- log(` registered "${id}" in ${path49.relative(process.cwd(), workspacePath)}`);
21865
+ log(` registered "${id}" in ${path50.relative(process.cwd(), workspacePath)}`);
21613
21866
  log(` manifest: ${project.manifest}`);
21614
21867
  log(` lock: ${project.lock}`);
21615
- if (!existsSync36(lockPath)) {
21868
+ if (!existsSync37(lockPath)) {
21616
21869
  log(
21617
21870
  ` warning: no lockfile there yet; this project contributes no claims until one is generated`
21618
21871
  );
@@ -21621,7 +21874,7 @@ async function runWorkspace(args) {
21621
21874
  }
21622
21875
  if (args.subcommand === "remove") {
21623
21876
  const ws = await loadWorkspace(workspacePath);
21624
- const resolvedTarget = path49.resolve(args.target);
21877
+ const resolvedTarget = path50.resolve(args.target);
21625
21878
  const match = ws.projects.find(
21626
21879
  (p) => p.id === args.target || resolveProject(dir, p).manifestPath === resolvedTarget
21627
21880
  );
@@ -21632,11 +21885,11 @@ async function runWorkspace(args) {
21632
21885
  version: 1,
21633
21886
  projects: ws.projects.filter((p) => p !== match)
21634
21887
  });
21635
- log(` removed "${match.id}" from ${path49.relative(process.cwd(), workspacePath)}`);
21888
+ log(` removed "${match.id}" from ${path50.relative(process.cwd(), workspacePath)}`);
21636
21889
  return;
21637
21890
  }
21638
21891
  if (args.subcommand === "list") {
21639
- if (!existsSync36(workspacePath)) {
21892
+ if (!existsSync37(workspacePath)) {
21640
21893
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
21641
21894
  }
21642
21895
  const ws = await loadWorkspace(workspacePath);
@@ -21644,9 +21897,9 @@ async function runWorkspace(args) {
21644
21897
  if (args.json) {
21645
21898
  log(JSON.stringify({ version: 1, workspace: workspacePath, projects: ws.projects, diagnostics }, null, 2));
21646
21899
  } else if (!ws.projects.length) {
21647
- log(` no projects registered in ${path49.relative(process.cwd(), workspacePath)}`);
21900
+ log(` no projects registered in ${path50.relative(process.cwd(), workspacePath)}`);
21648
21901
  } else {
21649
- log(` ${ws.projects.length} project(s) in ${path49.relative(process.cwd(), workspacePath)}:`);
21902
+ log(` ${ws.projects.length} project(s) in ${path50.relative(process.cwd(), workspacePath)}:`);
21650
21903
  for (const p of ws.projects) {
21651
21904
  log(` ${p.id.padEnd(24)} ${p.manifest.padEnd(40)} (${p.provider}${p.account ? `, ${p.account}` : ""})`);
21652
21905
  }
@@ -21656,7 +21909,7 @@ async function runWorkspace(args) {
21656
21909
  return;
21657
21910
  }
21658
21911
  if (args.subcommand === "status") {
21659
- if (!existsSync36(workspacePath)) {
21912
+ if (!existsSync37(workspacePath)) {
21660
21913
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
21661
21914
  }
21662
21915
  const ws = await loadWorkspace(workspacePath);
@@ -21664,7 +21917,7 @@ async function runWorkspace(args) {
21664
21917
  if (args.json) {
21665
21918
  log(JSON.stringify({ ...report, workspace: workspacePath }, null, 2));
21666
21919
  } else {
21667
- log(` workspace: ${path49.relative(process.cwd(), workspacePath)}`);
21920
+ log(` workspace: ${path50.relative(process.cwd(), workspacePath)}`);
21668
21921
  for (const p of report.projects) {
21669
21922
  if (p.error) {
21670
21923
  log(`
@@ -21718,8 +21971,8 @@ async function runHelp() {
21718
21971
  async function runVersion() {
21719
21972
  for (const up of ["../package.json", "../../package.json"]) {
21720
21973
  const url = new URL(up, import.meta.url);
21721
- if (!existsSync37(url)) continue;
21722
- const pkg = JSON.parse(await readFile33(url, "utf8"));
21974
+ if (!existsSync38(url)) continue;
21975
+ const pkg = JSON.parse(await readFile34(url, "utf8"));
21723
21976
  if (pkg.name === "pixelkiln") {
21724
21977
  log(`${pkg.name} ${pkg.version}`);
21725
21978
  return;