pixelkiln 0.83.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 and
13949
- * `interpolate` an ending-keyframe image the gallery does not offer, so
13950
- * those two are hand-edited; every other mode can be created here
13951
- * (`animate-skeleton` after the gallery writes its keypoints file).
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({
@@ -13965,15 +14095,38 @@ var NewAssetSchema = z6.object({
13965
14095
  dithering: RevisionDitheringSchema.optional(),
13966
14096
  ditheringStrength: z6.number().min(0).max(10).optional()
13967
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(),
13968
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(),
13969
14119
  z6.object({
13970
14120
  mode: z6.enum(["animate", "animate-pixminimax"]),
13971
14121
  from: z6.string().min(1),
13972
14122
  frames: z6.number().int().min(4).max(40).refine((n) => n % 2 === 0, "frames must be even").optional(),
13973
14123
  fps: z6.number().int().min(1).max(60).optional(),
13974
14124
  enhancePrompt: z6.boolean().optional(),
13975
- /** `animate-pixminimax` only; the loader refuses it on `animate`. */
13976
- direction: CharacterDirectionSchema.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()
13977
14130
  }).strict(),
13978
14131
  z6.object({
13979
14132
  mode: z6.literal("edit-animation"),
@@ -14645,19 +14798,19 @@ async function runEdit(args) {
14645
14798
  }
14646
14799
 
14647
14800
  // src/cli/commands/gallery.ts
14648
- import path34 from "path";
14649
- import { existsSync as existsSync26 } from "fs";
14801
+ import path35 from "path";
14802
+ import { existsSync as existsSync27 } from "fs";
14650
14803
 
14651
14804
  // src/gallery/snapshot.ts
14652
14805
  import { createHash as createHash3 } from "crypto";
14653
- import { existsSync as existsSync21 } from "fs";
14654
- import { readFile as readFile19, stat as stat2 } from "fs/promises";
14655
- 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";
14656
14809
 
14657
14810
  // src/gallery/edit.ts
14658
- import { readFile as readFile18, rename as rename9, writeFile as writeFile14 } from "fs/promises";
14659
- import path28 from "path";
14660
- 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";
14661
14814
 
14662
14815
  // src/gallery/upload.ts
14663
14816
  import { existsSync as existsSync18 } from "fs";
@@ -14700,50 +14853,106 @@ async function saveProjectImage(root, request) {
14700
14853
  return relative.split(path26.sep).join("/");
14701
14854
  }
14702
14855
 
14703
- // src/gallery/skeleton.ts
14856
+ // src/gallery/image-revision.ts
14704
14857
  import { existsSync as existsSync19 } from "fs";
14705
- 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";
14706
14859
  import path27 from "path";
14707
14860
  import { z as z9 } from "zod";
14708
- var SkeletonEstimateRequestSchema = z9.object({
14709
- project: z9.string().min(1).optional(),
14710
- styleId: z9.string().min(1),
14711
- assetId: z9.string().min(1),
14712
- frames: z9.number().int().min(3).max(15).optional()
14713
- }).strict();
14714
- var SkeletonAnimationRequestSchema = z9.object({
14715
- action: z9.literal("create-skeleton-animation"),
14861
+ var InpaintRevisionRequestSchema = z9.object({
14862
+ action: z9.literal("create-inpaint-revision"),
14716
14863
  project: z9.string().min(1).optional(),
14717
14864
  expectedSha256: z9.string().regex(/^[0-9a-f]{64}$/),
14718
14865
  styleId: z9.string().min(1),
14719
- /** The sprite the animation poses. */
14866
+ /** The sprite being repainted. */
14720
14867
  from: z9.string().min(1),
14721
14868
  assetId: z9.string().min(1).regex(/^[^/\\]+$/, "asset ids cannot contain slashes"),
14722
- /** The motion, sent as PixelLab's `action`. */
14869
+ /** What to paint into the masked area. */
14723
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),
14724
14933
  direction: CharacterDirectionSchema,
14725
- description: z9.string().min(1).optional(),
14726
- skeletonTemplate: z9.string().min(1).optional(),
14934
+ description: z10.string().min(1).optional(),
14935
+ skeletonTemplate: z10.string().min(1).optional(),
14727
14936
  /** Manifest-relative JSON path inside the project. */
14728
- keypointsFile: z9.string().min(1),
14937
+ keypointsFile: z10.string().min(1),
14729
14938
  /** Poses to write to `keypointsFile`; omitted to use a file that already exists. */
14730
14939
  set: SkeletonSetSchema.optional(),
14731
14940
  /** Replace an existing `keypointsFile` with `set`. */
14732
- overwrite: z9.boolean().optional()
14941
+ overwrite: z10.boolean().optional()
14733
14942
  }).strict();
14734
- var SkeletonKeypointsUpdateSchema = z9.object({
14735
- action: z9.literal("update-skeleton-keypoints"),
14736
- project: z9.string().min(1).optional(),
14737
- styleId: z9.string().min(1),
14738
- 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),
14739
14948
  /** The file's SHA-256 when the page loaded it; anything else on disk is a concurrent edit. */
14740
- expectedSha256: z9.string().regex(/^[0-9a-f]{64}$/),
14949
+ expectedSha256: z10.string().regex(/^[0-9a-f]{64}$/),
14741
14950
  set: SkeletonSetSchema
14742
14951
  }).strict();
14743
14952
  var DEFAULT_ESTIMATE_LIMIT = 10;
14744
14953
  function skeletonSourceFile(spec) {
14745
- if (spec.source) return path27.resolve(spec.root, spec.source);
14746
- 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;
14747
14956
  return spec.outFile;
14748
14957
  }
14749
14958
  function createGallerySkeletonHandlers(opts) {
@@ -14764,10 +14973,10 @@ function createGallerySkeletonHandlers(opts) {
14764
14973
  const spec = ctx.specs.find((candidate) => lockKey(candidate.styleId, candidate.assetId) === key);
14765
14974
  if (!spec) throw new ManifestEditError(`"${key}" is not declared by the manifest`);
14766
14975
  const file = skeletonSourceFile(spec);
14767
- if (!existsSync19(file)) throw new ManifestEditError(`${key} has no image on disk to estimate from`);
14768
- 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);
14769
14978
  const metadata = imageMetadata(bytes);
14770
- 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`);
14771
14980
  const problem = estimateSkeletonSizeProblem(metadata.width, metadata.height);
14772
14981
  if (problem) throw new ManifestEditError(`${key} is ${problem}`);
14773
14982
  if (used >= limit) {
@@ -14784,7 +14993,7 @@ function createGallerySkeletonHandlers(opts) {
14784
14993
  log2(` estimated a skeleton for ${key} (PixelLab estimate-skeleton, ${used}/${limit} this session${billed ? `, billed ${billed.amount} ${billed.unit}` : ""})`);
14785
14994
  return {
14786
14995
  set: scaffoldSkeletonSet(res.keypoints, request.frames ?? DEFAULT_SCAFFOLD_FRAMES),
14787
- source: path27.relative(ctx.loaded.root, file).split(path27.sep).join("/"),
14996
+ source: path28.relative(ctx.loaded.root, file).split(path28.sep).join("/"),
14788
14997
  session: status()
14789
14998
  };
14790
14999
  }
@@ -14798,21 +15007,21 @@ function billedAmount(usage) {
14798
15007
  return typeof amount === "number" && Number.isFinite(amount) ? { unit: type, amount } : null;
14799
15008
  }
14800
15009
  async function createSkeletonAnimation(manifestPath, request) {
14801
- const root = path27.dirname(path27.resolve(manifestPath));
14802
- const file = path27.resolve(root, request.keypointsFile);
14803
- const relative = path27.relative(root, file);
14804
- 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)) {
14805
15014
  throw new ManifestEditError("the keypoints file must be a .json path inside the project");
14806
15015
  }
14807
- const existed = existsSync19(file);
15016
+ const existed = existsSync20(file);
14808
15017
  let previous = null;
14809
15018
  if (request.set) {
14810
15019
  if (existed && !request.overwrite) {
14811
15020
  throw new ManifestEditError(`${request.keypointsFile} already exists; choose another path, or replace it`);
14812
15021
  }
14813
- if (existed) previous = await readFile17(file);
14814
- await mkdir11(path27.dirname(file), { recursive: true });
14815
- 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");
14816
15025
  } else if (!existed) {
14817
15026
  throw new ManifestEditError(`${request.keypointsFile} does not exist; paste poses or estimate them from the sprite`);
14818
15027
  }
@@ -14827,7 +15036,7 @@ async function createSkeletonAnimation(manifestPath, request) {
14827
15036
  revision: {
14828
15037
  mode: "animate-skeleton",
14829
15038
  from: request.from,
14830
- keypointsFile: relative.split(path27.sep).join("/"),
15039
+ keypointsFile: relative.split(path28.sep).join("/"),
14831
15040
  direction: request.direction,
14832
15041
  ...request.description ? { description: request.description } : {},
14833
15042
  ...request.skeletonTemplate ? { skeletonTemplate: request.skeletonTemplate } : {}
@@ -14836,8 +15045,8 @@ async function createSkeletonAnimation(manifestPath, request) {
14836
15045
  });
14837
15046
  } catch (error) {
14838
15047
  if (request.set) {
14839
- if (previous) await writeFile13(file, previous);
14840
- else await rm9(file, { force: true });
15048
+ if (previous) await writeFile14(file, previous);
15049
+ else await rm10(file, { force: true });
14841
15050
  }
14842
15051
  throw error;
14843
15052
  }
@@ -14848,64 +15057,64 @@ async function updateSkeletonKeypoints(ctx, request) {
14848
15057
  if (!spec) throw new ManifestEditError(`"${key}" is not declared by the manifest`);
14849
15058
  const file = spec.revision?.mode === "animate-skeleton" ? spec.revision.keypointsFile : void 0;
14850
15059
  if (!file) throw new ManifestEditError(`${key} is not an animate-skeleton revision`);
14851
- const relative = path27.relative(ctx.loaded.root, file);
14852
- if (relative.startsWith("..") || path27.isAbsolute(relative)) {
15060
+ const relative = path28.relative(ctx.loaded.root, file);
15061
+ if (relative.startsWith("..") || path28.isAbsolute(relative)) {
14853
15062
  throw new ManifestEditError(`${key}'s keypoints file is outside the project; edit it by hand`);
14854
15063
  }
14855
- const current = existsSync19(file) ? sha256(await readFile17(file)) : null;
15064
+ const current = existsSync20(file) ? sha256(await readFile18(file)) : null;
14856
15065
  if (current !== request.expectedSha256) {
14857
15066
  throw Object.assign(
14858
- 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`),
14859
15068
  { status: 409 }
14860
15069
  );
14861
15070
  }
14862
- await writeFile13(file, JSON.stringify(request.set, null, 2) + "\n");
14863
- return relative.split(path27.sep).join("/");
15071
+ await writeFile14(file, JSON.stringify(request.set, null, 2) + "\n");
15072
+ return relative.split(path28.sep).join("/");
14864
15073
  }
14865
15074
 
14866
15075
  // src/gallery/edit.ts
14867
- var Base64 = z10.string().regex(/^[A-Za-z0-9+/]*={0,2}$/).max(Math.ceil(MAX_HAND_EDIT_BYTES / 3) * 4);
14868
- var HandEditRequestSchema = z10.discriminatedUnion("action", [
14869
- z10.object({
14870
- action: z10.literal("start-edit"),
14871
- project: z10.string().min(1).optional(),
14872
- assetId: z10.string().min(1),
14873
- styleId: z10.string().min(1),
14874
- 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}$/),
14875
15084
  /** Also launch the author's editor on the file. */
14876
- open: z10.boolean().optional()
15085
+ open: z11.boolean().optional()
14877
15086
  }).strict(),
14878
- z10.object({
14879
- action: z10.literal("detach-edit"),
14880
- project: z10.string().min(1).optional(),
14881
- assetId: z10.string().min(1),
14882
- styleId: z10.string().min(1),
14883
- 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}$/)
14884
15093
  }).strict(),
14885
15094
  /** From the in-browser editor: the page holds the bytes, the server validates and writes. */
14886
- z10.object({
14887
- action: z10.literal("save-edit"),
14888
- project: z10.string().min(1).optional(),
14889
- assetId: z10.string().min(1),
14890
- styleId: z10.string().min(1),
14891
- 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}$/),
14892
15101
  /** The flattened image; for a frame set, `frames` instead. */
14893
15102
  png: Base64.min(1).optional(),
14894
15103
  /** Every frame of a set, tagged with the role it was opened under (null for one added in the editor). */
14895
- 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(),
14896
15105
  /** The editor's layered project file, kept beside the edit. */
14897
15106
  pxo: Base64.optional(),
14898
15107
  /** Editor identity from the bridge's ready message. */
14899
- editor: z10.string().min(1).max(200),
14900
- protocol: z10.number().int().positive()
15108
+ editor: z11.string().min(1).max(200),
15109
+ protocol: z11.number().int().positive()
14901
15110
  }).strict()
14902
15111
  ]);
14903
15112
  var UNDO_DEPTH = 20;
14904
- var UndoRequestSchema = z10.object({
14905
- action: z10.literal("undo-edit"),
14906
- project: z10.string().min(1).optional(),
15113
+ var UndoRequestSchema = z11.object({
15114
+ action: z11.literal("undo-edit"),
15115
+ project: z11.string().min(1).optional(),
14907
15116
  /** The manifest as the page last saw it; undo refuses a manifest someone else has changed since. */
14908
- expectedSha256: z10.string().regex(/^[0-9a-f]{64}$/)
15117
+ expectedSha256: z11.string().regex(/^[0-9a-f]{64}$/)
14909
15118
  }).strict();
14910
15119
  function createGalleryEditHandler(opts) {
14911
15120
  const log2 = opts.onProgress ?? (() => {
@@ -14914,7 +15123,7 @@ function createGalleryEditHandler(opts) {
14914
15123
  const history = /* @__PURE__ */ new Map();
14915
15124
  const manifestOf = async (project) => {
14916
15125
  try {
14917
- 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));
14918
15127
  } catch {
14919
15128
  return null;
14920
15129
  }
@@ -14924,7 +15133,7 @@ function createGalleryEditHandler(opts) {
14924
15133
  if (undo.success) {
14925
15134
  const manifestPath2 = await manifestOf(undo.data.project);
14926
15135
  if (!manifestPath2) throw new ManifestEditError("no manifest to undo an edit in");
14927
- const current = await readFile18(manifestPath2, "utf8");
15136
+ const current = await readFile19(manifestPath2, "utf8");
14928
15137
  if (sha256(current) !== undo.data.expectedSha256) throw new ManifestDriftError(manifestPath2);
14929
15138
  const stack = history.get(manifestPath2) ?? [];
14930
15139
  const last = stack[stack.length - 1];
@@ -14934,17 +15143,17 @@ function createGalleryEditHandler(opts) {
14934
15143
  throw Object.assign(new Error("the manifest changed outside the gallery since its last edit; undo would discard that change"), { status: 409 });
14935
15144
  }
14936
15145
  const tmp = manifestPath2 + ".pixelkiln-undo";
14937
- await writeFile14(tmp, last.before);
15146
+ await writeFile15(tmp, last.before);
14938
15147
  await rename9(tmp, manifestPath2);
14939
15148
  stack.pop();
14940
- log2(` manifest edit undone in ${path28.relative(process.cwd(), manifestPath2) || manifestPath2}`);
15149
+ log2(` manifest edit undone in ${path29.relative(process.cwd(), manifestPath2) || manifestPath2}`);
14941
15150
  return opts.reload();
14942
15151
  }
14943
15152
  const manifestPath = await manifestOf(body?.project);
14944
- const before = manifestPath ? await readFile18(manifestPath, "utf8").catch(() => null) : null;
15153
+ const before = manifestPath ? await readFile19(manifestPath, "utf8").catch(() => null) : null;
14945
15154
  const build = await apply(body);
14946
15155
  if (manifestPath && before !== null) {
14947
- const after = await readFile18(manifestPath, "utf8").catch(() => null);
15156
+ const after = await readFile19(manifestPath, "utf8").catch(() => null);
14948
15157
  if (after !== null && after !== before) {
14949
15158
  const stack = history.get(manifestPath) ?? [];
14950
15159
  stack.push({ before, afterSha: sha256(after) });
@@ -14999,7 +15208,7 @@ function createEditApplier(opts, log2) {
14999
15208
  } catch (error) {
15000
15209
  throw new ManifestEditError(error instanceof Error ? error.message : String(error));
15001
15210
  }
15002
- const written = await saveProjectImage(path28.dirname(path28.resolve(manifestPath2)), upload.data);
15211
+ const written = await saveProjectImage(path29.dirname(path29.resolve(manifestPath2)), upload.data);
15003
15212
  log2(` image added: ${written}`);
15004
15213
  return opts.reload();
15005
15214
  }
@@ -15035,6 +15244,23 @@ function createEditApplier(opts, log2) {
15035
15244
  "invalid skeleton animation: " + skeleton.error.issues.slice(0, 3).map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")
15036
15245
  );
15037
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
+ }
15038
15264
  const parsed = ManifestEditSchema.safeParse(body);
15039
15265
  if (!parsed.success) {
15040
15266
  throw new ManifestEditError(
@@ -15049,7 +15275,7 @@ function createEditApplier(opts, log2) {
15049
15275
  }
15050
15276
  const result = await applyManifestEdit(manifestPath, parsed.data);
15051
15277
  if (result.changed) {
15052
- const relative = path28.relative(process.cwd(), result.manifestPath);
15278
+ const relative = path29.relative(process.cwd(), result.manifestPath);
15053
15279
  const shown = !relative || relative.startsWith("..") ? result.manifestPath : relative;
15054
15280
  log2(` manifest edited: ${describeEdit(parsed.data)} in ${shown}`);
15055
15281
  }
@@ -15100,8 +15326,8 @@ function createGalleryPriceHandler(opts) {
15100
15326
  }
15101
15327
 
15102
15328
  // src/pipeline/history.ts
15103
- import { existsSync as existsSync20 } from "fs";
15104
- import path29 from "path";
15329
+ import { existsSync as existsSync21 } from "fs";
15330
+ import path30 from "path";
15105
15331
  var HISTORY_ENV = "PIXELKILN_HISTORY";
15106
15332
  function historyLimit(manifest, env = process.env) {
15107
15333
  if (manifest?.history !== void 0) return manifest.history;
@@ -15188,10 +15414,10 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
15188
15414
  }
15189
15415
  for (let i = 0; i < entry.outputs.length; i++) {
15190
15416
  const file = currentEntryOutputPath(entry, spec, i);
15191
- if (!existsSync20(file)) continue;
15417
+ if (!existsSync21(file)) continue;
15192
15418
  if (await sha256File(file) !== entry.outputs[i].sha256 && !opts.force) {
15193
15419
  throw new OverwriteRefusedError(
15194
- `${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`,
15195
15421
  [file]
15196
15422
  );
15197
15423
  }
@@ -15244,7 +15470,7 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
15244
15470
  // src/gallery/snapshot.ts
15245
15471
  var PROJECT_FILE_TYPE = "application/octet-stream";
15246
15472
  function galleryMediaId(absolutePath) {
15247
- 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);
15248
15474
  }
15249
15475
  function galleryMediaRoute(id) {
15250
15476
  return `/media/${id}`;
@@ -15263,9 +15489,9 @@ function describeHistory(media, entry, cacheDir) {
15263
15489
  return (entry.history ?? []).map((generation, i) => {
15264
15490
  const outputs = generation.outputs.map((output) => {
15265
15491
  const mediaType = output.mediaType ?? MediaType.PNG;
15266
- const file = path30.join(cacheDir, cacheFileName(output.sha256, mediaType));
15492
+ const file = path31.join(cacheDir, cacheFileName(output.sha256, mediaType));
15267
15493
  let url = null;
15268
- if (existsSync21(file)) {
15494
+ if (existsSync22(file)) {
15269
15495
  const id = galleryMediaId(file);
15270
15496
  media.set(id, { path: file, contentType: mediaType });
15271
15497
  url = `${galleryMediaRoute(id)}?v=${output.sha256.slice(0, 24)}`;
@@ -15297,7 +15523,7 @@ function describeHistory(media, entry, cacheDir) {
15297
15523
  var MAX_COMPARED_BYTES = 8 * 1024 * 1024;
15298
15524
  async function samePixels(a, b) {
15299
15525
  try {
15300
- const [left, right] = await Promise.all([readFile19(a), readFile19(b)]);
15526
+ const [left, right] = await Promise.all([readFile20(a), readFile20(b)]);
15301
15527
  if (left.length > MAX_COMPARED_BYTES || right.length > MAX_COMPARED_BYTES) return false;
15302
15528
  const x = decodePng(left);
15303
15529
  const y = decodePng(right);
@@ -15395,10 +15621,10 @@ async function describeOutput(media, root, absolutePath, recorded = {}) {
15395
15621
  async function describeQuality(media, root, inspection) {
15396
15622
  const outputPaths = inspection.outputs ?? [inspection.output];
15397
15623
  const outputs = await Promise.all(
15398
- outputPaths.map((file) => describeOutput(media, root, path30.resolve(root, file)))
15624
+ outputPaths.map((file) => describeOutput(media, root, path31.resolve(root, file)))
15399
15625
  );
15400
- const recordPath = path30.resolve(root, inspection.record);
15401
- const recordExists = existsSync21(recordPath);
15626
+ const recordPath = path31.resolve(root, inspection.record);
15627
+ const recordExists = existsSync22(recordPath);
15402
15628
  let options = null;
15403
15629
  let check = null;
15404
15630
  if (recordExists) {
@@ -15417,7 +15643,7 @@ async function describeQuality(media, root, inspection) {
15417
15643
  return {
15418
15644
  state: inspection.state,
15419
15645
  reason: inspection.reason,
15420
- output: portableOutputPath(path30.resolve(root, inspection.output), root),
15646
+ output: portableOutputPath(path31.resolve(root, inspection.output), root),
15421
15647
  record: portableOutputPath(recordPath, root),
15422
15648
  recordExists,
15423
15649
  outputs,
@@ -15455,9 +15681,9 @@ async function buildGallerySnapshot(opts) {
15455
15681
  const media = /* @__PURE__ */ new Map();
15456
15682
  const plan = await buildPlan(specs, lock);
15457
15683
  const items = [];
15458
- const manifestText = await readFile19(loaded.path, "utf8");
15684
+ const manifestText = await readFile20(loaded.path, "utf8");
15459
15685
  const manifestSha256 = sha256(manifestText);
15460
- const cacheDir = path30.resolve(path30.dirname(opts.lockPath), ".pixelkiln", "cache");
15686
+ const cacheDir = path31.resolve(path31.dirname(opts.lockPath), ".pixelkiln", "cache");
15461
15687
  let rawStyles = {};
15462
15688
  try {
15463
15689
  const raw = JSON.parse(manifestText);
@@ -15481,12 +15707,12 @@ async function buildGallerySnapshot(opts) {
15481
15707
  )
15482
15708
  );
15483
15709
  if (spec.source) {
15484
- const editPath = path30.resolve(root, spec.source);
15710
+ const editPath = path31.resolve(root, spec.source);
15485
15711
  const memberCount = sourceIsStem(spec.source, entry, root) ? entry.outputs.length : 1;
15486
15712
  const editShas = await Promise.all(
15487
15713
  Array.from({ length: memberCount }, async (_, index) => {
15488
15714
  const file = sourceOutputPath(spec.source, entry, index, root);
15489
- return { file, sha256: existsSync21(file) ? await sha256File(file) : null };
15715
+ return { file, sha256: existsSync22(file) ? await sha256File(file) : null };
15490
15716
  })
15491
15717
  );
15492
15718
  edits = await Promise.all(editShas.map(({ file, sha256: editSha }) => describeOutput(media, root, file, { sha256: editSha })));
@@ -15494,7 +15720,7 @@ async function buildGallerySnapshot(opts) {
15494
15720
  const companion = await readHandEditCompanion(editPath);
15495
15721
  if (companion) {
15496
15722
  const projectPath = handEditProjectPath(editPath);
15497
- const project = companion.project && existsSync21(projectPath) ? await fileInfo(projectPath) : null;
15723
+ const project = companion.project && existsSync22(projectPath) ? await fileInfo(projectPath) : null;
15498
15724
  let projectUrl = null;
15499
15725
  if (project) {
15500
15726
  const id = galleryMediaId(projectPath);
@@ -15530,8 +15756,8 @@ async function buildGallerySnapshot(opts) {
15530
15756
  editStatus = edits.some((member) => !member.exists) ? "missing" : editChanged.every((changed) => !changed) ? "same" : regenerated ? "regenerated-since" : "edited";
15531
15757
  }
15532
15758
  } else if (spec.source) {
15533
- outputs = [await describeOutput(media, root, path30.resolve(root, spec.source))];
15534
- } else if (!entry && existsSync21(spec.outFile)) {
15759
+ outputs = [await describeOutput(media, root, path31.resolve(root, spec.source))];
15760
+ } else if (!entry && existsSync22(spec.outFile)) {
15535
15761
  outputs = [await describeOutput(media, root, spec.outFile)];
15536
15762
  } else {
15537
15763
  outputs = [];
@@ -15734,7 +15960,7 @@ async function buildGallerySnapshot(opts) {
15734
15960
  id: loaded.manifest.name,
15735
15961
  name: loaded.manifest.name,
15736
15962
  manifest: loaded.path,
15737
- lock: path30.resolve(opts.lockPath),
15963
+ lock: path31.resolve(opts.lockPath),
15738
15964
  root,
15739
15965
  provider: loaded.manifest.provider,
15740
15966
  account: null,
@@ -15760,7 +15986,7 @@ async function buildGallerySnapshot(opts) {
15760
15986
  return { snapshot, media };
15761
15987
  }
15762
15988
  async function buildWorkspaceGallerySnapshot(opts) {
15763
- const dir = path30.dirname(path30.resolve(opts.workspacePath));
15989
+ const dir = path31.dirname(path31.resolve(opts.workspacePath));
15764
15990
  const media = /* @__PURE__ */ new Map();
15765
15991
  const projects = [];
15766
15992
  const items = [];
@@ -15803,11 +16029,11 @@ async function buildWorkspaceGallerySnapshot(opts) {
15803
16029
  id: project.id,
15804
16030
  name: loaded.manifest.name,
15805
16031
  manifest: manifestPath,
15806
- lock: path30.resolve(lockPath),
16032
+ lock: path31.resolve(lockPath),
15807
16033
  root: loaded.root,
15808
16034
  provider: loaded.manifest.provider,
15809
16035
  account: project.account ?? null,
15810
- manifestSha256: manifestSha256 ?? sha256(await readFile19(manifestPath, "utf8")),
16036
+ manifestSha256: manifestSha256 ?? sha256(await readFile20(manifestPath, "utf8")),
15811
16037
  historyLimit: historyLimit(loaded.manifest),
15812
16038
  entries: Object.keys(lock.entries).length,
15813
16039
  items: projectItems.length,
@@ -15819,8 +16045,8 @@ async function buildWorkspaceGallerySnapshot(opts) {
15819
16045
  id: project.id,
15820
16046
  name: project.id,
15821
16047
  manifest: manifestPath,
15822
- lock: path30.resolve(lockPath),
15823
- root: path30.dirname(manifestPath),
16048
+ lock: path31.resolve(lockPath),
16049
+ root: path31.dirname(manifestPath),
15824
16050
  provider: project.provider,
15825
16051
  account: project.account ?? null,
15826
16052
  manifestSha256: null,
@@ -15847,7 +16073,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
15847
16073
  version: 1,
15848
16074
  generatedAt: (opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
15849
16075
  project: null,
15850
- workspace: { path: path30.resolve(opts.workspacePath), dir, projects },
16076
+ workspace: { path: path31.resolve(opts.workspacePath), dir, projects },
15851
16077
  filter,
15852
16078
  totals: {
15853
16079
  entries: projects.reduce((sum, project) => sum + project.entries, 0),
@@ -15866,10 +16092,10 @@ async function buildWorkspaceGallerySnapshot(opts) {
15866
16092
  // src/gallery/server.ts
15867
16093
  import { randomBytes } from "crypto";
15868
16094
  import { createServer as createServer2 } from "http";
15869
- import { readFile as readFile20 } from "fs/promises";
16095
+ import { readFile as readFile21 } from "fs/promises";
15870
16096
 
15871
16097
  // src/gallery/page.ts
15872
- import { existsSync as existsSync22, readFileSync as readFileSync4 } from "fs";
16098
+ import { existsSync as existsSync23, readFileSync as readFileSync4 } from "fs";
15873
16099
 
15874
16100
  // src/gallery/client-bundle.ts
15875
16101
  import { createRequire } from "module";
@@ -15910,7 +16136,7 @@ function clientAssets() {
15910
16136
  const prebuilt = new URL("gallery.js", CLIENT_DIR);
15911
16137
  client ??= {
15912
16138
  css: readFileSync4(new URL("gallery.css", CLIENT_DIR), "utf8"),
15913
- js: existsSync22(prebuilt) ? readFileSync4(prebuilt, "utf8") : bundleGalleryClient()
16139
+ js: existsSync23(prebuilt) ? readFileSync4(prebuilt, "utf8") : bundleGalleryClient()
15914
16140
  };
15915
16141
  return client;
15916
16142
  }
@@ -16181,7 +16407,7 @@ async function serveGallery(opts) {
16181
16407
  const asset = reviewAssets.get(jobId)?.get(url2.pathname);
16182
16408
  if (!asset) return fail(404, "no such review asset");
16183
16409
  try {
16184
- const bytes = await readFile20(asset.path);
16410
+ const bytes = await readFile21(asset.path);
16185
16411
  res.writeHead(200, {
16186
16412
  "Content-Type": asset.contentType,
16187
16413
  "Cache-Control": "no-store",
@@ -16264,7 +16490,7 @@ async function serveGallery(opts) {
16264
16490
  if (!asset) return fail(404, "no such gallery media");
16265
16491
  let bytes;
16266
16492
  try {
16267
- bytes = await readFile20(asset.path);
16493
+ bytes = await readFile21(asset.path);
16268
16494
  } catch {
16269
16495
  return fail(404, "gallery media is no longer on disk; refresh the page");
16270
16496
  }
@@ -16308,7 +16534,7 @@ async function serveGallery(opts) {
16308
16534
 
16309
16535
  // src/gallery/generate.ts
16310
16536
  import { randomBytes as randomBytes2 } from "crypto";
16311
- import { z as z11 } from "zod";
16537
+ import { z as z12 } from "zod";
16312
16538
 
16313
16539
  // src/pipeline/poll.ts
16314
16540
  var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -16592,6 +16818,10 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
16592
16818
  ...spec.revision.keypointsSha256 ? { keypointsSha256: spec.revision.keypointsSha256 } : {},
16593
16819
  ...spec.revision.skeletonTemplate ? { skeletonTemplate: spec.revision.skeletonTemplate } : {},
16594
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 } : {},
16595
16825
  ...spec.revision.engine ? { engine: spec.revision.engine } : {}
16596
16826
  } : null,
16597
16827
  outfit: spec.character?.kind === "outfit" && outfitParentEntry ? {
@@ -16719,13 +16949,13 @@ function ownedReferenceImageId(spec, lock, providerId) {
16719
16949
  }
16720
16950
 
16721
16951
  // src/pick/server.ts
16722
- import { existsSync as existsSync24 } from "fs";
16723
- import { readFile as readFile22 } from "fs/promises";
16952
+ import { existsSync as existsSync25 } from "fs";
16953
+ import { readFile as readFile23 } from "fs/promises";
16724
16954
 
16725
16955
  // src/pipeline/init.ts
16726
- import { readdir, readFile as readFile21, writeFile as writeFile15 } from "fs/promises";
16727
- import { existsSync as existsSync23 } from "fs";
16728
- 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";
16729
16959
  function pngSize(buf) {
16730
16960
  if (buf.length < 24) return null;
16731
16961
  const isPng = buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834;
@@ -16739,7 +16969,7 @@ function slugify(value) {
16739
16969
  async function walk(dir, exclude) {
16740
16970
  const out = [];
16741
16971
  for (const entry of await readdir(dir, { withFileTypes: true })) {
16742
- const full = path31.join(dir, entry.name);
16972
+ const full = path32.join(dir, entry.name);
16743
16973
  if (exclude.some((x) => full.includes(x))) continue;
16744
16974
  if (entry.isDirectory()) out.push(...await walk(full, exclude));
16745
16975
  else if (entry.name.toLowerCase().endsWith(".png")) out.push(full);
@@ -16753,14 +16983,14 @@ async function scanAssets(root, opts = {}) {
16753
16983
  const skipped = [];
16754
16984
  const seen = /* @__PURE__ */ new Map();
16755
16985
  for (const file of files.sort()) {
16756
- const size = pngSize(await readFile21(file));
16986
+ const size = pngSize(await readFile22(file));
16757
16987
  if (!size) {
16758
16988
  skipped.push(`${file} (not a readable PNG)`);
16759
16989
  continue;
16760
16990
  }
16761
- const rel = path31.relative(root, file);
16762
- const category = path31.dirname(rel) === "." ? "" : path31.dirname(rel);
16763
- 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));
16764
16994
  if (seen.has(id)) {
16765
16995
  const n = seen.get(id) + 1;
16766
16996
  seen.set(id, n);
@@ -16818,10 +17048,10 @@ function mode(values) {
16818
17048
  return [...counts.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? 64;
16819
17049
  }
16820
17050
  async function writeManifestFile(target, manifest) {
16821
- if (existsSync23(target)) {
17051
+ if (existsSync24(target)) {
16822
17052
  throw new Error(`${target} already exists; refusing to overwrite an existing manifest.`);
16823
17053
  }
16824
- await writeFile15(target, JSON.stringify(manifest, null, 2) + "\n");
17054
+ await writeFile16(target, JSON.stringify(manifest, null, 2) + "\n");
16825
17055
  }
16826
17056
 
16827
17057
  // src/pick/sheet.ts
@@ -17234,13 +17464,13 @@ async function currentArt(entry, spec) {
17234
17464
  const previous = primaryOutput({ ...entry, outputs: entry.supersededOutputs });
17235
17465
  if (!previous) return null;
17236
17466
  const file = resolveOutputPath(previous.path, spec.root);
17237
- if (!existsSync24(file)) return null;
17467
+ if (!existsSync25(file)) return null;
17238
17468
  const contentType = previous.mediaType ?? mediaTypeFromExtension(file);
17239
17469
  if (!contentType) return null;
17240
17470
  let width = entry.width;
17241
17471
  let height = entry.height;
17242
17472
  if (contentType === "image/png") {
17243
- const size = pngSize(await readFile22(file));
17473
+ const size = pngSize(await readFile23(file));
17244
17474
  if (size) ({ width, height } = size);
17245
17475
  }
17246
17476
  return { path: file, contentType, width, height };
@@ -17408,26 +17638,26 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
17408
17638
  }
17409
17639
 
17410
17640
  // src/gallery/generate.ts
17411
- var GenerateRequestSchema = z11.object({
17412
- project: z11.string().min(1).optional(),
17413
- 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),
17414
17644
  /**
17415
17645
  * Regenerate work that is up to date, as `gen --force` does, and let the
17416
17646
  * fetch replace a locally modified or untracked file. With `restore`, put
17417
17647
  * the recorded bytes back over a modified file.
17418
17648
  */
17419
- force: z11.boolean().optional(),
17649
+ force: z12.boolean().optional(),
17420
17650
  /** Advance in-flight, review, or selected work without submitting anything. */
17421
- resume: z11.boolean().optional(),
17651
+ resume: z12.boolean().optional(),
17422
17652
  /** Re-download downloaded outputs and replace files whose object changed upstream. */
17423
- refresh: z11.boolean().optional(),
17653
+ refresh: z12.boolean().optional(),
17424
17654
  /** Put recorded outputs back on disk from the cache or the provider, without generating. */
17425
- restore: z11.boolean().optional(),
17655
+ restore: z12.boolean().optional(),
17426
17656
  /**
17427
17657
  * Bring a replaced generation back: 1-based position in the record's
17428
17658
  * history (newest first) or an output hash prefix. One key at a time.
17429
17659
  */
17430
- revert: z11.string().min(1).max(64).optional()
17660
+ revert: z12.string().min(1).max(64).optional()
17431
17661
  }).strict().refine((request) => [request.resume, request.refresh, request.restore, request.revert !== void 0].filter(Boolean).length <= 1, {
17432
17662
  message: "resume, refresh, restore, and revert are exclusive"
17433
17663
  }).refine((request) => request.revert === void 0 || request.keys.length === 1, { message: "revert takes one key" });
@@ -17773,15 +18003,15 @@ function createGenerateHandlers(opts) {
17773
18003
  }
17774
18004
 
17775
18005
  // src/gallery/editor.ts
17776
- import { readFile as readFile24 } from "fs/promises";
17777
- import path33 from "path";
18006
+ import { readFile as readFile25 } from "fs/promises";
18007
+ import path34 from "path";
17778
18008
 
17779
18009
  // src/editor/install.ts
17780
18010
  import { createHash as createHash4 } from "crypto";
17781
- import { existsSync as existsSync25 } from "fs";
17782
- 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";
17783
18013
  import os2 from "os";
17784
- import path32 from "path";
18014
+ import path33 from "path";
17785
18015
 
17786
18016
  // tools/pixelorama-bridge/pin.json
17787
18017
  var pin_default = {
@@ -17845,15 +18075,15 @@ var EditorInstallError = class extends Error {
17845
18075
  };
17846
18076
  function editorToolsRoot() {
17847
18077
  const override = process.env.PIXELKILN_TOOLS_DIR?.trim();
17848
- if (override) return path32.resolve(override);
17849
- 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");
17850
18080
  if (process.platform === "win32") {
17851
- 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");
17852
18082
  }
17853
- 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");
17854
18084
  }
17855
18085
  function editorDir(pin = EDITOR_PIN, root = editorToolsRoot()) {
17856
- return path32.join(root, "pixelorama", pin.release ?? "unpublished");
18086
+ return path33.join(root, "pixelorama", pin.release ?? "unpublished");
17857
18087
  }
17858
18088
  function editorBaseUrl(pin = EDITOR_PIN) {
17859
18089
  const override = process.env.PIXELKILN_EDITOR_URL?.trim();
@@ -17863,7 +18093,7 @@ function editorBaseUrl(pin = EDITOR_PIN) {
17863
18093
  }
17864
18094
  async function sha256Of(file) {
17865
18095
  try {
17866
- return createHash4("sha256").update(await readFile23(file)).digest("hex");
18096
+ return createHash4("sha256").update(await readFile24(file)).digest("hex");
17867
18097
  } catch {
17868
18098
  return null;
17869
18099
  }
@@ -17876,8 +18106,8 @@ async function editorStatus(opts = {}) {
17876
18106
  let totalBytes = 0;
17877
18107
  for (const [name, expected] of Object.entries(pin.files)) {
17878
18108
  totalBytes += expected.bytes;
17879
- const file = path32.join(dir, name);
17880
- 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;
17881
18111
  if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
17882
18112
  installedBytes += expected.bytes;
17883
18113
  } else {
@@ -17934,13 +18164,13 @@ async function installEditor(opts = {}) {
17934
18164
  if (digest2 !== expected.sha256) {
17935
18165
  throw new EditorInstallError(`${name}: sha256 ${digest2.slice(0, 12)}\u2026 does not match the pinned ${expected.sha256.slice(0, 12)}\u2026`, name);
17936
18166
  }
17937
- const final = path32.join(dir, name);
18167
+ const final = path33.join(dir, name);
17938
18168
  const tmp = `${final}.${process.pid}.part`;
17939
18169
  try {
17940
- await writeFile16(tmp, bytes);
18170
+ await writeFile17(tmp, bytes);
17941
18171
  await rename10(tmp, final);
17942
18172
  } finally {
17943
- await rm10(tmp, { force: true });
18173
+ await rm11(tmp, { force: true });
17944
18174
  }
17945
18175
  fetchedBytes += bytes.length;
17946
18176
  downloaded.push(name);
@@ -17980,7 +18210,7 @@ var EDITOR_CONTENT_TYPES = {
17980
18210
  ".json": "application/json; charset=utf-8"
17981
18211
  };
17982
18212
  function editorContentType(name) {
17983
- return EDITOR_CONTENT_TYPES[path32.extname(name).toLowerCase()] ?? null;
18213
+ return EDITOR_CONTENT_TYPES[path33.extname(name).toLowerCase()] ?? null;
17984
18214
  }
17985
18215
 
17986
18216
  // src/gallery/editor.ts
@@ -18056,7 +18286,7 @@ function createGalleryEditorHandlers(opts = {}) {
18056
18286
  if (!status.installed) return null;
18057
18287
  let bytes;
18058
18288
  try {
18059
- bytes = await readFile24(path33.join(dir, name));
18289
+ bytes = await readFile25(path34.join(dir, name));
18060
18290
  } catch {
18061
18291
  verified = null;
18062
18292
  return null;
@@ -18096,7 +18326,7 @@ async function serveUntilStopped(initial, reload, args, access2) {
18096
18326
  };
18097
18327
  const loadProject = async (project) => {
18098
18328
  const ctx = await access2.loadProject(project);
18099
- loadProjectEnv(path34.dirname(ctx.loaded.path));
18329
+ loadProjectEnv(path35.dirname(ctx.loaded.path));
18100
18330
  return ctx;
18101
18331
  };
18102
18332
  const loadProjectEnv = (dir) => {
@@ -18105,7 +18335,7 @@ async function serveUntilStopped(initial, reload, args, access2) {
18105
18335
  for (const [name, value] of Object.entries(declared)) {
18106
18336
  if (credentialNames.has(name) && name in process.env && process.env[name] !== value) {
18107
18337
  throw new Error(
18108
- `${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.`
18109
18339
  );
18110
18340
  }
18111
18341
  }
@@ -18113,7 +18343,7 @@ async function serveUntilStopped(initial, reload, args, access2) {
18113
18343
  };
18114
18344
  const quoters = /* @__PURE__ */ new Map();
18115
18345
  const quoteFor = async (project) => {
18116
- loadProjectEnv(path34.dirname(path34.resolve(await access2.manifestFor(project))));
18346
+ loadProjectEnv(path35.dirname(path35.resolve(await access2.manifestFor(project))));
18117
18347
  const key = process.env.PIXELLAB_API_KEY;
18118
18348
  if (!key) return null;
18119
18349
  let quoter = quoters.get(key);
@@ -18164,8 +18394,8 @@ async function serveUntilStopped(initial, reload, args, access2) {
18164
18394
  }
18165
18395
  async function runGallery(args) {
18166
18396
  if (args.workspace) {
18167
- const workspacePath = path34.resolve(args.workspace);
18168
- if (!existsSync26(workspacePath)) {
18397
+ const workspacePath = path35.resolve(args.workspace);
18398
+ if (!existsSync27(workspacePath)) {
18169
18399
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
18170
18400
  }
18171
18401
  const filter2 = { styles: args.styles, assets: args.assets };
@@ -18185,7 +18415,7 @@ async function runGallery(args) {
18185
18415
  const registered = async (projectId) => {
18186
18416
  const project = (await loadWorkspace(workspacePath)).projects.find((candidate) => candidate.id === projectId);
18187
18417
  if (!projectId || !project) throw new Error(`unknown workspace project "${projectId ?? ""}"`);
18188
- return resolveProject(path34.dirname(workspacePath), project);
18418
+ return resolveProject(path35.dirname(workspacePath), project);
18189
18419
  };
18190
18420
  await serveUntilStopped(initial, build2, args, {
18191
18421
  manifestFor: async (projectId) => (await registered(projectId)).manifestPath,
@@ -18210,7 +18440,7 @@ async function runGallery(args) {
18210
18440
  return buildGallerySnapshot({ ...fresh, filter });
18211
18441
  };
18212
18442
  await serveUntilStopped(await build(), reload, args, {
18213
- manifestFor: () => path34.resolve(args.manifest),
18443
+ manifestFor: () => path35.resolve(args.manifest),
18214
18444
  loadProject: () => openProject(args.manifest, { lockPath: args.lock, env: false })
18215
18445
  });
18216
18446
  }
@@ -18519,18 +18749,18 @@ async function runRestoreGeneration(args) {
18519
18749
  }
18520
18750
 
18521
18751
  // src/cli/commands/housekeeping.ts
18522
- import { existsSync as existsSync28 } from "fs";
18752
+ import { existsSync as existsSync29 } from "fs";
18523
18753
 
18524
18754
  // src/pipeline/cache-health.ts
18525
- import { existsSync as existsSync27 } from "fs";
18526
- import { readFile as readFile25, readdir as readdir2, rm as rm11 } from "fs/promises";
18527
- 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";
18528
18758
  async function inspectCaches(lock, lockPath, options = {}) {
18529
- if (options.prune && !existsSync27(lockPath)) {
18530
- 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)}`);
18531
18761
  }
18532
- const contentDir = path35.resolve(path35.dirname(lockPath), ".pixelkiln", "cache");
18533
- const remotePath = path35.resolve(cachePathFor(lockPath));
18762
+ const contentDir = path36.resolve(path36.dirname(lockPath), ".pixelkiln", "cache");
18763
+ const remotePath = path36.resolve(cachePathFor(lockPath));
18534
18764
  const referenced = referencedHashes(lock);
18535
18765
  let content = await inspectContentCache(contentDir, referenced);
18536
18766
  let remoteHashes = await inspectRemoteHashCache(remotePath);
@@ -18542,7 +18772,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
18542
18772
  ]);
18543
18773
  for (const name of names) {
18544
18774
  try {
18545
- await rm11(path35.join(contentDir, name), { force: true });
18775
+ await rm12(path36.join(contentDir, name), { force: true });
18546
18776
  removed.contentFiles++;
18547
18777
  } catch {
18548
18778
  }
@@ -18551,7 +18781,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
18551
18781
  await saveCache(remotePath, { version: 1, hashes: {} });
18552
18782
  removed.resetRemoteHashCache = true;
18553
18783
  } else if (remoteHashes.invalidIds.length) {
18554
- const cache = parseCache(JSON.parse(await readFile25(remotePath, "utf8")));
18784
+ const cache = parseCache(JSON.parse(await readFile26(remotePath, "utf8")));
18555
18785
  for (const id of remoteHashes.invalidIds) delete cache.hashes[id];
18556
18786
  removed.remoteHashEntries = remoteHashes.invalidIds.length;
18557
18787
  await saveCache(remotePath, cache);
@@ -18569,7 +18799,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
18569
18799
  async function inspectContentCache(contentDir, referenced) {
18570
18800
  const report = {
18571
18801
  path: contentDir,
18572
- exists: existsSync27(contentDir),
18802
+ exists: existsSync28(contentDir),
18573
18803
  files: 0,
18574
18804
  bytes: 0,
18575
18805
  valid: 0,
@@ -18590,12 +18820,12 @@ async function inspectContentCache(contentDir, referenced) {
18590
18820
  continue;
18591
18821
  }
18592
18822
  report.files++;
18593
- const file = path35.join(contentDir, entry.name);
18823
+ const file = path36.join(contentDir, entry.name);
18594
18824
  const mediaType = mediaTypeFromExtension(entry.name);
18595
18825
  const expected = mediaType ? entry.name.slice(0, -4) : "";
18596
18826
  let bytes;
18597
18827
  try {
18598
- bytes = await readFile25(file);
18828
+ bytes = await readFile26(file);
18599
18829
  report.bytes += bytes.length;
18600
18830
  } catch (err) {
18601
18831
  report.invalid.push({
@@ -18633,7 +18863,7 @@ async function inspectContentCache(contentDir, referenced) {
18633
18863
  async function inspectRemoteHashCache(remotePath) {
18634
18864
  const report = {
18635
18865
  path: remotePath,
18636
- exists: existsSync27(remotePath),
18866
+ exists: existsSync28(remotePath),
18637
18867
  entries: 0,
18638
18868
  valid: 0,
18639
18869
  invalidIds: [],
@@ -18642,7 +18872,7 @@ async function inspectRemoteHashCache(remotePath) {
18642
18872
  if (!report.exists) return report;
18643
18873
  let cache;
18644
18874
  try {
18645
- cache = parseCache(JSON.parse(await readFile25(remotePath, "utf8")));
18875
+ cache = parseCache(JSON.parse(await readFile26(remotePath, "utf8")));
18646
18876
  } catch (err) {
18647
18877
  report.error = err instanceof Error ? err.message : String(err);
18648
18878
  return report;
@@ -18706,7 +18936,7 @@ async function runAccept(args) {
18706
18936
  let intact = true;
18707
18937
  for (const output of entry.outputs) {
18708
18938
  const file = resolveOutputPath(output.path, item.spec.root);
18709
- if (!existsSync28(file) || await sha256File(file) !== output.sha256) {
18939
+ if (!existsSync29(file) || await sha256File(file) !== output.sha256) {
18710
18940
  intact = false;
18711
18941
  break;
18712
18942
  }
@@ -18761,42 +18991,42 @@ This drops their provenance from the lockfile. The art on disk is`);
18761
18991
  }
18762
18992
 
18763
18993
  // src/cli/commands/init.ts
18764
- import path36 from "path";
18765
- import { existsSync as existsSync29 } from "fs";
18994
+ import path37 from "path";
18995
+ import { existsSync as existsSync30 } from "fs";
18766
18996
  async function runInit(args) {
18767
18997
  if (!args.from) throw new Error("init needs --from <dir> pointing at your existing PNGs.");
18768
- const root = path36.resolve(args.from);
18769
- 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}`);
18770
19000
  const generator = args.generator ?? "map";
18771
19001
  if (generator !== "1dir" && generator !== "map") {
18772
19002
  throw new Error(`--generator must be "1dir" or "map", got "${args.generator}".`);
18773
19003
  }
18774
- const target = path36.resolve(args.out ?? "pixelkiln.manifest.json");
19004
+ const target = path37.resolve(args.out ?? "pixelkiln.manifest.json");
18775
19005
  const { assets, skipped } = await scanAssets(root, { exclude: args.exclude });
18776
19006
  if (!assets.length) throw new Error(`No PNGs found under ${root}`);
18777
19007
  const manifest = buildManifest(
18778
- args.name ?? path36.basename(path36.dirname(target)),
19008
+ args.name ?? path37.basename(path37.dirname(target)),
18779
19009
  args.styles[0] ?? "base",
18780
19010
  generator,
18781
- path36.relative(path36.dirname(target), root) || ".",
19011
+ path37.relative(path37.dirname(target), root) || ".",
18782
19012
  assets
18783
19013
  );
18784
19014
  await writeManifestFile(target, manifest);
18785
- 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)}`);
18786
19016
  if (skipped.length) log(` skipped ${skipped.length} unreadable file(s)`);
18787
- log(` wrote ${path36.relative(process.cwd(), target)}`);
19017
+ log(` wrote ${path37.relative(process.cwd(), target)}`);
18788
19018
  log(`
18789
19019
  Prompts are intentionally empty. To recover the real ones from your`);
18790
19020
  log(` PixelLab account instead of inventing them:`);
18791
19021
  log(`
18792
- pixelkiln adopt --manifest ${path36.relative(process.cwd(), target)} --write-prompts
19022
+ pixelkiln adopt --manifest ${path37.relative(process.cwd(), target)} --write-prompts
18793
19023
  `);
18794
19024
  }
18795
19025
 
18796
19026
  // src/pipeline/doctor.ts
18797
- import { constants, existsSync as existsSync30 } from "fs";
19027
+ import { constants, existsSync as existsSync31 } from "fs";
18798
19028
  import { access, stat as stat4 } from "fs/promises";
18799
- import path37 from "path";
19029
+ import path38 from "path";
18800
19030
  async function doctor(loaded, specs, lock, lockPath, opts = {}) {
18801
19031
  const checks = [];
18802
19032
  const add = (id, level, message6) => checks.push({ id, level, message: message6 });
@@ -18807,8 +19037,8 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
18807
19037
  );
18808
19038
  add("lock", "ok", `${Object.keys(lock.entries).length} valid v2 lock entr(ies)`);
18809
19039
  const dirs = [...new Set(specs.flatMap((spec) => [
18810
- path37.dirname(spec.outFile),
18811
- ...spec.quality ? [path37.dirname(spec.quality.outFile)] : []
19040
+ path38.dirname(spec.outFile),
19041
+ ...spec.quality ? [path38.dirname(spec.quality.outFile)] : []
18812
19042
  ]))];
18813
19043
  const unwritable = [];
18814
19044
  for (const dir of dirs) {
@@ -18831,7 +19061,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
18831
19061
  for (let index = 0; index < entry.outputs.length; index++) {
18832
19062
  const output = entry.outputs[index];
18833
19063
  const spec = specByKey.get(key);
18834
- 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));
18835
19065
  const owner = outputOwners.get(absolute);
18836
19066
  if (owner && owner !== key) duplicateOutputs.push(`${output.path} (${owner}, ${key})`);
18837
19067
  outputOwners.set(absolute, key);
@@ -18846,7 +19076,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
18846
19076
  } else {
18847
19077
  add("lock-outputs", "ok", `${outputOwners.size} recorded output path(s) are uniquely owned`);
18848
19078
  }
18849
- const cacheDir = path37.join(path37.dirname(lockPath), ".pixelkiln", "cache");
19079
+ const cacheDir = path38.join(path38.dirname(lockPath), ".pixelkiln", "cache");
18850
19080
  const stranded = [];
18851
19081
  const invalidState = [];
18852
19082
  const staleMapUrls = [];
@@ -18862,11 +19092,11 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
18862
19092
  const hasSource = Boolean(entry.sourceUrl || entry.sourceUrls?.length);
18863
19093
  let hasCache = false;
18864
19094
  for (const output of entry.outputs) {
18865
- const cached = path37.join(
19095
+ const cached = path38.join(
18866
19096
  cacheDir,
18867
19097
  cacheFileName(output.sha256, output.mediaType ?? MediaType.PNG)
18868
19098
  );
18869
- if (existsSync30(cached) && await sha256File(cached) === output.sha256) {
19099
+ if (existsSync31(cached) && await sha256File(cached) === output.sha256) {
18870
19100
  hasCache = true;
18871
19101
  break;
18872
19102
  }
@@ -18962,16 +19192,16 @@ async function nearestExistingDirectory(start) {
18962
19192
  if ((await stat4(current)).isDirectory()) return current;
18963
19193
  } catch {
18964
19194
  }
18965
- const parent = path37.dirname(current);
19195
+ const parent = path38.dirname(current);
18966
19196
  if (parent === current) return current;
18967
19197
  current = parent;
18968
19198
  }
18969
19199
  }
18970
19200
 
18971
19201
  // src/pipeline/audit.ts
18972
- import { readFile as readFile26 } from "fs/promises";
18973
- import { existsSync as existsSync31 } from "fs";
18974
- import path38 from "path";
19202
+ import { readFile as readFile27 } from "fs/promises";
19203
+ import { existsSync as existsSync32 } from "fs";
19204
+ import path39 from "path";
18975
19205
  function paletteDistance(asset, reference) {
18976
19206
  if (!asset.length || !reference.length) return 0;
18977
19207
  let total = 0;
@@ -19007,12 +19237,12 @@ async function auditStyle(loaded, specs, styleId, lock) {
19007
19237
  const unreadable = [];
19008
19238
  for (const spec of mine) {
19009
19239
  for (const output of resolveSpecOutputs(spec, lock, loaded.root)) {
19010
- if (!existsSync31(output.absolutePath)) {
19240
+ if (!existsSync32(output.absolutePath)) {
19011
19241
  missing.push(output.id);
19012
19242
  continue;
19013
19243
  }
19014
19244
  try {
19015
- const png = decodePng(await readFile26(output.absolutePath));
19245
+ const png = decodePng(await readFile27(output.absolutePath));
19016
19246
  const palette = extractPalette(png, 12);
19017
19247
  assets.push({
19018
19248
  assetId: spec.assetId,
@@ -19036,10 +19266,10 @@ async function auditStyle(loaded, specs, styleId, lock) {
19036
19266
  let referenceFromStyleImages = false;
19037
19267
  const refPalettes = [];
19038
19268
  for (const img of style.styleImages) {
19039
- const abs = path38.resolve(loaded.root, img.path);
19040
- if (!existsSync31(abs)) continue;
19269
+ const abs = path39.resolve(loaded.root, img.path);
19270
+ if (!existsSync32(abs)) continue;
19041
19271
  try {
19042
- refPalettes.push(extractPalette(decodePng(await readFile26(abs)), 12));
19272
+ refPalettes.push(extractPalette(decodePng(await readFile27(abs)), 12));
19043
19273
  } catch {
19044
19274
  }
19045
19275
  }
@@ -19267,24 +19497,24 @@ async function runAudit(args) {
19267
19497
  }
19268
19498
 
19269
19499
  // src/cli/commands/pack.ts
19270
- import path40 from "path";
19271
- import { readFile as readFile27 } from "fs/promises";
19500
+ import path41 from "path";
19501
+ import { readFile as readFile28 } from "fs/promises";
19272
19502
 
19273
19503
  // src/pipeline/pack.ts
19274
19504
  import { readFileSync as readFileSync5 } from "fs";
19275
- import path39 from "path";
19505
+ import path40 from "path";
19276
19506
  var DEFAULT_FRAME_SET_FPS = 12;
19277
19507
  function resolvePackInputs(raw, inputsFilePath) {
19278
19508
  if (!Array.isArray(raw) || !raw.length) {
19279
19509
  throw new Error("--inputs must be a non-empty JSON array of { id, path }");
19280
19510
  }
19281
- const baseDir = path39.dirname(path39.resolve(inputsFilePath));
19511
+ const baseDir = path40.dirname(path40.resolve(inputsFilePath));
19282
19512
  return raw.map((entry, i) => {
19283
19513
  const e = entry;
19284
19514
  if (typeof e.id !== "string" || typeof e.path !== "string") {
19285
19515
  throw new Error(`--inputs[${i}] needs string "id" and "path"`);
19286
19516
  }
19287
- return { id: e.id, path: path39.resolve(baseDir, e.path) };
19517
+ return { id: e.id, path: path40.resolve(baseDir, e.path) };
19288
19518
  });
19289
19519
  }
19290
19520
  function packSprites(inputs, options = {}) {
@@ -19368,7 +19598,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
19368
19598
  }
19369
19599
  const inputs2 = Object.entries(options.sourceOverrides).map(([id, source]) => ({
19370
19600
  id,
19371
- path: path39.resolve(source)
19601
+ path: path40.resolve(source)
19372
19602
  }));
19373
19603
  if (!inputs2.length) throw new Error(`No approved quality outputs for style "${styleId}".`);
19374
19604
  const packed2 = packSprites(inputs2, options);
@@ -19392,7 +19622,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
19392
19622
  let outputs = resolveEntryOutputs(entry, id, manifestDir);
19393
19623
  const source = sources[id];
19394
19624
  if (source && outputs.length <= 1) {
19395
- inputs.push({ id, path: path39.resolve(manifestDir, source) });
19625
+ inputs.push({ id, path: path40.resolve(manifestDir, source) });
19396
19626
  continue;
19397
19627
  }
19398
19628
  if (source && sourceIsStem(source, entry, manifestDir)) {
@@ -19426,7 +19656,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
19426
19656
  }
19427
19657
  }
19428
19658
  for (const [id, source] of Object.entries(sources)) {
19429
- 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) });
19430
19660
  }
19431
19661
  if (!inputs.length) {
19432
19662
  throw new Error(`No readable sprites for style "${styleId}"; ${noOutput.length} skipped.`);
@@ -19534,7 +19764,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
19534
19764
  const source = sources[id];
19535
19765
  const entry = lock.entries[`${styleId}/${id}`];
19536
19766
  if (source && !(entry && sourceIsStem(source, entry, manifestDir))) {
19537
- placements.push({ id, path: path39.resolve(manifestDir, source), cell });
19767
+ placements.push({ id, path: path40.resolve(manifestDir, source), cell });
19538
19768
  continue;
19539
19769
  }
19540
19770
  if (source && entry) {
@@ -19567,7 +19797,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
19567
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}`
19568
19798
  );
19569
19799
  }
19570
- 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;
19571
19801
  const mounted = mountSprites(placements, {
19572
19802
  cellWidth: mount.cellWidth,
19573
19803
  cellHeight: mount.cellHeight,
@@ -19577,7 +19807,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
19577
19807
  ...mounted,
19578
19808
  sources: mount.base ? [{
19579
19809
  id: "$base",
19580
- path: path39.resolve(manifestDir, mount.base),
19810
+ path: path40.resolve(manifestDir, mount.base),
19581
19811
  sha256: sha256(basePng),
19582
19812
  included: true
19583
19813
  }, ...mounted.sources] : mounted.sources,
@@ -19981,12 +20211,12 @@ async function runPack(args) {
19981
20211
  if (args.primaryOnly || args.outputRoles.length) {
19982
20212
  throw new Error("--primary-only and --output-role require manifest-driven pack");
19983
20213
  }
19984
- const raw = JSON.parse(await readFile27(path40.resolve(args.inputs), "utf8"));
20214
+ const raw = JSON.parse(await readFile28(path41.resolve(args.inputs), "utf8"));
19985
20215
  const inputs = resolvePackInputs(raw, args.inputs);
19986
20216
  const { png, atlas, skipped, sources } = packSprites(inputs, { columns: args.columns });
19987
20217
  const format = sheetFormat(args);
19988
- const base = path40.resolve(args.out.replace(/\.(?:png|json|tres)$/i, ""));
19989
- 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`) });
19990
20220
  const outputs = [
19991
20221
  { path: `${base}.png`, data: png },
19992
20222
  { path: `${base}${extension}`, data: document }
@@ -19999,7 +20229,7 @@ async function runPack(args) {
19999
20229
  log(
20000
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})`
20001
20231
  );
20002
- log(` ${path40.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
20232
+ log(` ${path41.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
20003
20233
  for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
20004
20234
  return;
20005
20235
  }
@@ -20007,7 +20237,7 @@ async function runPack(args) {
20007
20237
  if (args.primaryOnly && args.outputRoles.length) {
20008
20238
  throw new Error("pack accepts either --primary-only or --output-role, not both");
20009
20239
  }
20010
- const manifestDir = path40.dirname(path40.resolve(args.manifest));
20240
+ const manifestDir = path41.dirname(path41.resolve(args.manifest));
20011
20241
  const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
20012
20242
  for (const styleId of styleIds) {
20013
20243
  const style = loaded.manifest.styles[styleId];
@@ -20022,9 +20252,9 @@ async function runPack(args) {
20022
20252
  sourceOverrides: qualitySources,
20023
20253
  sources: manifestSources(loaded.manifest, styleId)
20024
20254
  });
20025
- 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`);
20026
20256
  const format = sheetFormat(args);
20027
- const { extension, document } = renderSheetDocument(format, atlas, { imageName: path40.basename(`${base}.png`) });
20257
+ const { extension, document } = renderSheetDocument(format, atlas, { imageName: path41.basename(`${base}.png`) });
20028
20258
  const outputs = [
20029
20259
  { path: `${base}.png`, data: png },
20030
20260
  { path: `${base}${extension}`, data: document }
@@ -20056,13 +20286,13 @@ async function runPack(args) {
20056
20286
  log(
20057
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})`
20058
20288
  );
20059
- log(` ${path40.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
20289
+ log(` ${path41.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
20060
20290
  for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
20061
20291
  }
20062
20292
  }
20063
20293
  async function runMount(args) {
20064
20294
  const { loaded, lock } = await openProject2(args);
20065
- const manifestDir = path40.dirname(path40.resolve(args.manifest));
20295
+ const manifestDir = path41.dirname(path41.resolve(args.manifest));
20066
20296
  const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
20067
20297
  for (const styleId of styleIds) {
20068
20298
  const style = loaded.manifest.styles[styleId];
@@ -20101,9 +20331,9 @@ async function runMount(args) {
20101
20331
  sources,
20102
20332
  outputRoles
20103
20333
  );
20104
- const out = path40.resolve(manifestDir, style.mount.out);
20334
+ const out = path41.resolve(manifestDir, style.mount.out);
20105
20335
  const format = sheetFormat(args);
20106
- const { extension, document } = renderSheetDocument(format, atlas, { imageName: path40.basename(out) });
20336
+ const { extension, document } = renderSheetDocument(format, atlas, { imageName: path41.basename(out) });
20107
20337
  const metadata = out.replace(/\.png$/, "") + extension;
20108
20338
  const companion = out.replace(/\.png$/, "") + ".pixelkiln.json";
20109
20339
  const outputs = [
@@ -20123,7 +20353,7 @@ async function runMount(args) {
20123
20353
  await provenanceFile("$lock", args.lock),
20124
20354
  ...qualityRecords,
20125
20355
  ...artifactSources.filter(
20126
- (source) => source.id !== "$base" || path40.resolve(source.path) !== out
20356
+ (source) => source.id !== "$base" || path41.resolve(source.path) !== out
20127
20357
  )
20128
20358
  ],
20129
20359
  options: {
@@ -20137,14 +20367,14 @@ async function runMount(args) {
20137
20367
  log(
20138
20368
  ` ${styleId}: ${atlas.frames.length} cell(s) into ${atlas.sheet.width}x${atlas.sheet.height}` + (overBase ? ` over ${style.mount.base}` : " (new sheet)")
20139
20369
  );
20140
- log(` ${path40.relative(process.cwd(), out)} + atlas/provenance JSON`);
20370
+ log(` ${path41.relative(process.cwd(), out)} + atlas/provenance JSON`);
20141
20371
  for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
20142
20372
  }
20143
20373
  }
20144
20374
  async function runExport(args) {
20145
20375
  const { loaded, specs, lock } = await openProject2(args);
20146
20376
  const format = args.format ?? "generic";
20147
- const manifestDir = path40.dirname(path40.resolve(args.manifest));
20377
+ const manifestDir = path41.dirname(path41.resolve(args.manifest));
20148
20378
  const selected = specs.filter((spec) => {
20149
20379
  if (spec.generator !== "tiles" && spec.generator !== "terrain") return false;
20150
20380
  if (args.styles.length && !args.styles.includes(spec.styleId)) return false;
@@ -20160,12 +20390,12 @@ async function runExport(args) {
20160
20390
  for (const spec of selected) {
20161
20391
  const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
20162
20392
  const style = loaded.manifest.styles[spec.styleId];
20163
- const defaultBase = path40.resolve(manifestDir, style.outDir, `${spec.assetId}-tileset`);
20164
- 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;
20165
20395
  const result = exportTileset(entry, spec, {
20166
20396
  format,
20167
20397
  manifestDir,
20168
- imageName: path40.basename(`${base}.png`),
20398
+ imageName: path41.basename(`${base}.png`),
20169
20399
  columns: args.columns
20170
20400
  });
20171
20401
  const outputs = [
@@ -20183,7 +20413,7 @@ async function runExport(args) {
20183
20413
  asset: spec.assetId,
20184
20414
  columns: args.columns ?? null,
20185
20415
  format,
20186
- image: path40.basename(`${base}.png`),
20416
+ image: path41.basename(`${base}.png`),
20187
20417
  providerRules: result.generic.providerRules,
20188
20418
  style: spec.styleId,
20189
20419
  tileType: spec.tileType ?? null
@@ -20193,7 +20423,7 @@ async function runExport(args) {
20193
20423
  ` ${spec.styleId}/${spec.assetId}: ${result.generic.tiles.length} tile(s), ${result.generic.sheet.width}x${result.generic.sheet.height} (${format})`
20194
20424
  );
20195
20425
  log(
20196
- ` ${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`
20197
20427
  );
20198
20428
  }
20199
20429
  }
@@ -20206,23 +20436,23 @@ function sheetFormat(args) {
20206
20436
  }
20207
20437
 
20208
20438
  // src/cli/commands/pixellab-utils.ts
20209
- import path42 from "path";
20439
+ import path43 from "path";
20210
20440
 
20211
20441
  // src/pixellab-utils.ts
20212
- import { existsSync as existsSync32 } from "fs";
20213
- import { mkdir as mkdir13, readFile as readFile28, writeFile as writeFile17 } from "fs/promises";
20214
- 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";
20215
20445
  var UNZOOM_MIN_SIDE = 256;
20216
20446
  var UNZOOM_MAX_AREA = 2048 * 2048;
20217
20447
  function defaultUnzoomOut(input) {
20218
- const ext = path41.extname(input);
20448
+ const ext = path42.extname(input);
20219
20449
  return `${ext ? input.slice(0, -ext.length) : input}.unzoomed.png`;
20220
20450
  }
20221
20451
  async function unzoomFile(client2, input, opts = {}) {
20222
- const out = path41.resolve(opts.out ?? defaultUnzoomOut(input));
20223
- if (path41.resolve(input) === out) throw new Error("unzoom will not overwrite its own input; pass a different --out");
20224
- if (existsSync32(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
20225
- 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);
20226
20456
  const metadata = imageMetadata(bytes);
20227
20457
  if (!metadata) throw new Error(`${input} is not a readable PNG or JPEG`);
20228
20458
  const { width, height, format } = metadata;
@@ -20235,22 +20465,22 @@ async function unzoomFile(client2, input, opts = {}) {
20235
20465
  throw new Error(`${input} is ${width}x${height}; PixelLab's unzoom takes at most 2048x2048 worth of pixels`);
20236
20466
  }
20237
20467
  const res = await client2.unzoom({ image: { base64: bytes.toString("base64"), format }, quantize: opts.quantize });
20238
- await mkdir13(path41.dirname(out), { recursive: true });
20239
- await writeFile17(out, res.png);
20468
+ await mkdir13(path42.dirname(out), { recursive: true });
20469
+ await writeFile18(out, res.png);
20240
20470
  return { out, original: res.originalSize, unzoomed: res.unzoomedSize, zoomFactor: res.zoomFactor, usage: res.usage };
20241
20471
  }
20242
20472
  var FONT_WEIGHTS = ["Bold", "Regular"];
20243
20473
  var FONT_GLYPH_SIZES = [8, 16, 32, 64];
20244
20474
  var FONT_COST_GENERATIONS = 25;
20245
20475
  function fontOutputPaths(out) {
20246
- const base = path41.resolve(out.replace(/\.(ttf|png)$/i, ""));
20476
+ const base = path42.resolve(out.replace(/\.(ttf|png)$/i, ""));
20247
20477
  return { ttf: `${base}.ttf`, atlas: `${base}.png` };
20248
20478
  }
20249
20479
  async function generateFont(client2, opts) {
20250
20480
  const { ttf, atlas } = fontOutputPaths(opts.out);
20251
20481
  if (!opts.force) {
20252
20482
  for (const file of [ttf, atlas]) {
20253
- 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`);
20254
20484
  }
20255
20485
  }
20256
20486
  const sleep3 = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
@@ -20277,9 +20507,9 @@ async function generateFont(client2, opts) {
20277
20507
  client2.download(job.download_ttf_url),
20278
20508
  client2.download(job.download_atlas_url)
20279
20509
  ]);
20280
- await mkdir13(path41.dirname(ttf), { recursive: true });
20281
- await writeFile17(ttf, ttfBytes);
20282
- await writeFile17(atlas, atlasBytes);
20510
+ await mkdir13(path42.dirname(ttf), { recursive: true });
20511
+ await writeFile18(ttf, ttfBytes);
20512
+ await writeFile18(atlas, atlasBytes);
20283
20513
  return { jobId, ttf, atlas, glyphPx: job.glyph_px ?? null, usage: job.usage };
20284
20514
  }
20285
20515
  if (Date.now() - started > timeout) {
@@ -20293,7 +20523,7 @@ async function generateFont(client2, opts) {
20293
20523
 
20294
20524
  // src/cli/commands/pixellab-utils.ts
20295
20525
  function loadKeys(args) {
20296
- loadEnvFiles(path42.dirname(path42.resolve(args.manifest)));
20526
+ loadEnvFiles(path43.dirname(path43.resolve(args.manifest)));
20297
20527
  loadEnvFiles(process.cwd());
20298
20528
  }
20299
20529
  async function runUnzoom(args) {
@@ -20359,19 +20589,19 @@ async function runFont(args) {
20359
20589
  }
20360
20590
 
20361
20591
  // src/cli/commands/quality.ts
20362
- import path44 from "path";
20363
- import { readFile as readFile30 } from "fs/promises";
20592
+ import path45 from "path";
20593
+ import { readFile as readFile31 } from "fs/promises";
20364
20594
 
20365
20595
  // src/pipeline/quality-regression.ts
20366
- import path43 from "path";
20367
- import { existsSync as existsSync33 } from "fs";
20368
- import { readFile as readFile29 } from "fs/promises";
20369
- 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";
20370
20600
  var SHA256_RE = /^[0-9a-f]{64}$/;
20371
20601
  var HEX_RE = /^#[0-9a-f]{6}$/;
20372
20602
  var MAX_REDMEAN_DISTANCE = 765;
20373
- var QualityPathSchema = z12.string().min(1).refine(
20374
- (value) => !path43.posix.isAbsolute(value) && !value.includes("\\"),
20603
+ var QualityPathSchema = z13.string().min(1).refine(
20604
+ (value) => !path44.posix.isAbsolute(value) && !value.includes("\\"),
20375
20605
  "expected a portable relative path"
20376
20606
  );
20377
20607
  var DEFAULT_QUALITY_TOLERANCES = {
@@ -20384,53 +20614,53 @@ var DEFAULT_QUALITY_TOLERANCES = {
20384
20614
  maxEdgeContrastDrop: 0.03,
20385
20615
  maxIsolatedPixelIncrease: 5e-3
20386
20616
  };
20387
- var QualityToleranceSchema = z12.object({
20388
- requireExactHash: z12.boolean(),
20389
- maxColorCountIncrease: z12.number().int().min(0),
20390
- maxNewColors: z12.number().int().min(0),
20391
- maxTransparencyDelta: z12.number().min(0).max(1),
20392
- maxPartialAlphaIncrease: z12.number().min(0).max(1),
20393
- maxEdgeDensityDelta: z12.number().min(0).max(1),
20394
- maxEdgeContrastDrop: z12.number().min(0).max(1),
20395
- 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)
20396
20626
  }).strict();
20397
- var ImageQualityMetricsSchema = z12.object({
20398
- sha256: z12.string().regex(SHA256_RE),
20399
- width: z12.number().int().min(1),
20400
- height: z12.number().int().min(1),
20401
- colorCount: z12.number().int().min(0),
20402
- colors: z12.array(z12.string().regex(HEX_RE)),
20403
- transparency: z12.number().min(0).max(1),
20404
- partialAlpha: z12.number().min(0).max(1),
20405
- edgeDensity: z12.number().min(0).max(1),
20406
- meanEdgeContrast: z12.number().min(0).max(1),
20407
- 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)
20408
20638
  }).strict();
20409
- var QualityBaselineCaseSchema = z12.object({
20410
- 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/_-]*$/),
20411
20641
  file: QualityPathSchema,
20412
- record: z12.object({
20642
+ record: z13.object({
20413
20643
  path: QualityPathSchema,
20414
- sha256: z12.string().regex(SHA256_RE)
20644
+ sha256: z13.string().regex(SHA256_RE)
20415
20645
  }).strict().optional(),
20416
20646
  expected: ImageQualityMetricsSchema,
20417
20647
  tolerances: QualityToleranceSchema
20418
20648
  }).strict();
20419
- var QualityBaselineSchema = z12.object({
20420
- $schema: z12.string().url().optional(),
20421
- format: z12.literal("pixelkiln-quality-baseline"),
20422
- schemaVersion: z12.literal(1),
20423
- 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)
20424
20654
  }).strict().superRefine((baseline, context) => {
20425
20655
  const ids = baseline.cases.map((entry) => entry.id);
20426
20656
  if (new Set(ids).size !== ids.length) {
20427
- 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" });
20428
20658
  }
20429
20659
  });
20430
- var QualityInputSchema = z12.object({
20431
- id: z12.string().regex(/^[a-z0-9][a-z0-9/_-]*$/),
20432
- path: z12.string().min(1),
20433
- 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(),
20434
20664
  tolerances: QualityToleranceSchema.partial().optional()
20435
20665
  }).strict();
20436
20666
  function message4(error) {
@@ -20518,10 +20748,10 @@ function measureDecodedImageQuality(png, digest2) {
20518
20748
  };
20519
20749
  }
20520
20750
  async function measureImageQuality(file) {
20521
- const absolute = path43.resolve(file);
20751
+ const absolute = path44.resolve(file);
20522
20752
  let bytes;
20523
20753
  try {
20524
- bytes = await readFile29(absolute);
20754
+ bytes = await readFile30(absolute);
20525
20755
  } catch (error) {
20526
20756
  throw new Error(`Cannot read quality image ${absolute}: ${message4(error)}`, { cause: error });
20527
20757
  }
@@ -20532,7 +20762,7 @@ async function measureImageQuality(file) {
20532
20762
  }
20533
20763
  }
20534
20764
  function resolveQualityInputs(raw, inputsFilePath) {
20535
- const parsed = z12.array(QualityInputSchema).min(1).safeParse(raw);
20765
+ const parsed = z13.array(QualityInputSchema).min(1).safeParse(raw);
20536
20766
  if (!parsed.success) {
20537
20767
  throw new Error(
20538
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")
@@ -20540,11 +20770,11 @@ function resolveQualityInputs(raw, inputsFilePath) {
20540
20770
  }
20541
20771
  const ids = parsed.data.map((entry) => entry.id);
20542
20772
  if (new Set(ids).size !== ids.length) throw new Error("--inputs contains a duplicate quality case id.");
20543
- const root = path43.dirname(path43.resolve(inputsFilePath));
20773
+ const root = path44.dirname(path44.resolve(inputsFilePath));
20544
20774
  return parsed.data.map((entry) => ({
20545
20775
  id: entry.id,
20546
- file: path43.resolve(root, entry.path),
20547
- ...entry.record ? { record: path43.resolve(root, entry.record) } : {},
20776
+ file: path44.resolve(root, entry.path),
20777
+ ...entry.record ? { record: path44.resolve(root, entry.record) } : {},
20548
20778
  tolerances: QualityToleranceSchema.parse({
20549
20779
  ...DEFAULT_QUALITY_TOLERANCES,
20550
20780
  ...entry.tolerances
@@ -20552,7 +20782,7 @@ function resolveQualityInputs(raw, inputsFilePath) {
20552
20782
  }));
20553
20783
  }
20554
20784
  function portableRelative2(from, to) {
20555
- return path43.relative(from, path43.resolve(to)).split(path43.sep).join("/") || ".";
20785
+ return path44.relative(from, path44.resolve(to)).split(path44.sep).join("/") || ".";
20556
20786
  }
20557
20787
  async function verifyRecordForImage(recordPath, imagePath) {
20558
20788
  const verification = await verifyArtifactBundle(recordPath);
@@ -20567,20 +20797,20 @@ async function verifyRecordForImage(recordPath, imagePath) {
20567
20797
  if (record.kind !== "refine" || options === null || typeof options !== "object" || options.schema !== "pixelkiln-quality") {
20568
20798
  throw new Error(`Quality record ${recordPath} is not a PixelKiln refinement record.`);
20569
20799
  }
20570
- const outputs = record.outputs.map((output) => path43.resolve(path43.dirname(recordPath), output.path));
20571
- 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))) {
20572
20802
  throw new Error(`Quality record ${recordPath} does not own ${imagePath}.`);
20573
20803
  }
20574
20804
  }
20575
20805
  async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
20576
20806
  if (!inputs.length) throw new Error("Cannot snapshot an empty quality baseline.");
20577
- const absolute = path43.resolve(baselinePath);
20807
+ const absolute = path44.resolve(baselinePath);
20578
20808
  for (const input of inputs) {
20579
- 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) {
20580
20810
  throw new Error(`Quality baseline output would overwrite an input: ${absolute}.`);
20581
20811
  }
20582
20812
  }
20583
- const root = path43.dirname(absolute);
20813
+ const root = path44.dirname(absolute);
20584
20814
  const cases = [];
20585
20815
  for (const input of [...inputs].sort((a, b) => a.id.localeCompare(b.id))) {
20586
20816
  const expected = await measureImageQuality(input.file);
@@ -20607,8 +20837,8 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
20607
20837
  cases
20608
20838
  });
20609
20839
  const data = Buffer.from(JSON.stringify(baseline, null, 2) + "\n");
20610
- if (existsSync33(absolute) && !options.force) {
20611
- const current = await readFile29(absolute);
20840
+ if (existsSync34(absolute) && !options.force) {
20841
+ const current = await readFile30(absolute);
20612
20842
  if (!current.equals(data)) {
20613
20843
  throw new OverwriteRefusedError(
20614
20844
  `Quality baseline already exists with different content: ${absolute}. Pass --force to replace it.`,
@@ -20620,10 +20850,10 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
20620
20850
  return { baseline, path: absolute, changed: result.changed.length > 0 };
20621
20851
  }
20622
20852
  async function readQualityBaseline(baselinePath) {
20623
- const absolute = path43.resolve(baselinePath);
20853
+ const absolute = path44.resolve(baselinePath);
20624
20854
  let raw;
20625
20855
  try {
20626
- raw = JSON.parse(await readFile29(absolute, "utf8"));
20856
+ raw = JSON.parse(await readFile30(absolute, "utf8"));
20627
20857
  } catch (error) {
20628
20858
  throw new Error(`Could not read quality baseline ${absolute}: ${message4(error)}`, { cause: error });
20629
20859
  }
@@ -20693,12 +20923,12 @@ function regressionViolations(expected, actual, tolerances) {
20693
20923
  return { violations, warnings };
20694
20924
  }
20695
20925
  async function checkQualityBaseline(baselinePath) {
20696
- const absolute = path43.resolve(baselinePath);
20926
+ const absolute = path44.resolve(baselinePath);
20697
20927
  const baseline = await readQualityBaseline(absolute);
20698
- const root = path43.dirname(absolute);
20928
+ const root = path44.dirname(absolute);
20699
20929
  const cases = [];
20700
20930
  for (const entry of baseline.cases) {
20701
- const file = path43.resolve(root, entry.file);
20931
+ const file = path44.resolve(root, entry.file);
20702
20932
  let actual = null;
20703
20933
  const violations = [];
20704
20934
  const warnings = [];
@@ -20711,7 +20941,7 @@ async function checkQualityBaseline(baselinePath) {
20711
20941
  violations.push(message4(error));
20712
20942
  }
20713
20943
  if (entry.record) {
20714
- const recordPath = path43.resolve(root, entry.record.path);
20944
+ const recordPath = path44.resolve(root, entry.record.path);
20715
20945
  try {
20716
20946
  const recordHash = await sha256File(recordPath);
20717
20947
  if (recordHash !== entry.record.sha256) violations.push("quality record hash changed");
@@ -20753,15 +20983,15 @@ async function runQuality(args) {
20753
20983
  if (!args.out) throw new Error("quality snapshot needs --out <pixelkiln.quality.json>.");
20754
20984
  let raw;
20755
20985
  try {
20756
- raw = JSON.parse(await readFile30(path44.resolve(args.inputs), "utf8"));
20986
+ raw = JSON.parse(await readFile31(path45.resolve(args.inputs), "utf8"));
20757
20987
  } catch (error) {
20758
20988
  throw new Error(
20759
- `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)}`,
20760
20990
  { cause: error }
20761
20991
  );
20762
20992
  }
20763
20993
  const inputs = resolveQualityInputs(raw, args.inputs);
20764
- if (path44.resolve(args.inputs) === path44.resolve(args.out)) {
20994
+ if (path45.resolve(args.inputs) === path45.resolve(args.out)) {
20765
20995
  throw new Error("quality snapshot --out must not overwrite its --inputs file.");
20766
20996
  }
20767
20997
  const result = await snapshotQualityBaseline(inputs, args.out, { force: args.force });
@@ -20774,7 +21004,7 @@ async function runQuality(args) {
20774
21004
  }, null, 2));
20775
21005
  } else {
20776
21006
  log(
20777
- ` ${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))`
20778
21008
  );
20779
21009
  log(` Review the tolerances, commit the baseline, then run quality check in CI.`);
20780
21010
  }
@@ -20800,54 +21030,54 @@ async function runQuality(args) {
20800
21030
  }
20801
21031
 
20802
21032
  // src/cli/commands/recipe.ts
20803
- import path46 from "path";
21033
+ import path47 from "path";
20804
21034
 
20805
21035
  // src/recipes.ts
20806
- import path45 from "path";
20807
- import { existsSync as existsSync34 } from "fs";
20808
- 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";
20809
21039
  import { fileURLToPath as fileURLToPath2 } from "url";
20810
- import { z as z13 } from "zod";
21040
+ import { z as z14 } from "zod";
20811
21041
  var SHA256_RE2 = /^[0-9a-f]{64}$/;
20812
21042
  var RECIPE_ID_RE = /^[a-z0-9][a-z0-9-]*\/[a-z0-9][a-z0-9-]*$/;
20813
21043
  var VERSION_RE = /^\d+\.\d+\.\d+$/;
20814
21044
  var RECIPE_FILE = "pixelkiln.recipe.json";
20815
21045
  var RECIPE_DIR_TOKEN = "{{recipeDir}}";
20816
- var PortablePathSchema = z13.string().min(1).refine((value) => {
20817
- 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;
20818
21048
  const parts = value.split("/");
20819
21049
  return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
20820
21050
  }, "expected a portable relative path without '.', '..', or backslashes");
20821
- var RecipeBindingSchema = z13.object({
20822
- nodeId: z13.string().min(1),
20823
- input: z13.string().min(1)
21051
+ var RecipeBindingSchema = z14.object({
21052
+ nodeId: z14.string().min(1),
21053
+ input: z14.string().min(1)
20824
21054
  }).strict();
20825
- var RecipeSchema = z13.object({
20826
- $schema: z13.string().url().optional(),
20827
- format: z13.literal("pixelkiln-recipe"),
20828
- schemaVersion: z13.literal(1),
20829
- id: z13.string().regex(RECIPE_ID_RE, "expected provider/name"),
20830
- version: z13.string().regex(VERSION_RE, "expected x.y.z"),
20831
- provider: z13.string().regex(/^[a-z0-9][a-z0-9-]*$/),
20832
- summary: z13.string().min(1).max(240),
20833
- 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({
20834
21064
  path: PortablePathSchema,
20835
- role: z13.enum(["workflow", "reference"]),
20836
- sha256: z13.string().regex(SHA256_RE2)
21065
+ role: z14.enum(["workflow", "reference"]),
21066
+ sha256: z14.string().regex(SHA256_RE2)
20837
21067
  }).strict()).default([]),
20838
- models: z13.array(z13.object({
21068
+ models: z14.array(z14.object({
20839
21069
  path: PortablePathSchema,
20840
- sha256: z13.string().regex(SHA256_RE2),
20841
- source: z13.string().url(),
20842
- license: z13.string().min(1)
21070
+ sha256: z14.string().regex(SHA256_RE2),
21071
+ source: z14.string().url(),
21072
+ license: z14.string().min(1)
20843
21073
  }).strict()).default([]),
20844
- styleId: z13.string().regex(/^[a-z0-9][a-z0-9-]*$/),
21074
+ styleId: z14.string().regex(/^[a-z0-9][a-z0-9-]*$/),
20845
21075
  style: StyleSchema,
20846
- workflow: z13.object({
21076
+ workflow: z14.object({
20847
21077
  path: PortablePathSchema,
20848
- outputNodeId: z13.string().min(1),
20849
- numImages: z13.number().int().min(1).max(16),
20850
- bindings: z13.object({
21078
+ outputNodeId: z14.string().min(1),
21079
+ numImages: z14.number().int().min(1).max(16),
21080
+ bindings: z14.object({
20851
21081
  prompt: RecipeBindingSchema,
20852
21082
  width: RecipeBindingSchema.optional(),
20853
21083
  height: RecipeBindingSchema.optional(),
@@ -20858,67 +21088,67 @@ var RecipeSchema = z13.object({
20858
21088
  strength: RecipeBindingSchema.optional()
20859
21089
  }).strict()
20860
21090
  }).strict().optional(),
20861
- quality: z13.object({
20862
- stage: z13.enum(["composition-source", "production-candidate"]),
20863
- workingCanvas: z13.object({
20864
- width: z13.number().int().min(16),
20865
- 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)
20866
21096
  }).strict(),
20867
- recommendedNativeSize: z13.object({
20868
- min: z13.number().int().min(1),
20869
- max: z13.number().int().min(1)
21097
+ recommendedNativeSize: z14.object({
21098
+ min: z14.number().int().min(1),
21099
+ max: z14.number().int().min(1)
20870
21100
  }).strict(),
20871
- paletteColors: z13.object({
20872
- min: z13.number().int().min(1),
20873
- max: z13.number().int().min(1)
21101
+ paletteColors: z14.object({
21102
+ min: z14.number().int().min(1),
21103
+ max: z14.number().int().min(1)
20874
21104
  }).strict(),
20875
- background: z13.enum(["opaque", "transparent", "full-bleed"]),
20876
- checks: z13.array(z13.enum([
21105
+ background: z14.enum(["opaque", "transparent", "full-bleed"]),
21106
+ checks: z14.array(z14.enum([
20877
21107
  "prompt-coverage",
20878
21108
  "native-grid",
20879
21109
  "final-palette",
20880
21110
  "alpha-edge",
20881
21111
  "human-1x"
20882
21112
  ])).min(1),
20883
- notes: z13.array(z13.string().min(1)).default([])
21113
+ notes: z14.array(z14.string().min(1)).default([])
20884
21114
  }).strict(),
20885
- integrity: z13.object({
20886
- algorithm: z13.literal("sha256"),
20887
- digest: z13.string().regex(SHA256_RE2)
21115
+ integrity: z14.object({
21116
+ algorithm: z14.literal("sha256"),
21117
+ digest: z14.string().regex(SHA256_RE2)
20888
21118
  }).strict()
20889
21119
  }).strict().superRefine((recipe, context) => {
20890
21120
  if (recipe.style.provider && recipe.style.provider !== recipe.provider) {
20891
21121
  context.addIssue({
20892
- code: z13.ZodIssueCode.custom,
21122
+ code: z14.ZodIssueCode.custom,
20893
21123
  path: ["style", "provider"],
20894
21124
  message: "must match the recipe provider"
20895
21125
  });
20896
21126
  }
20897
21127
  const filePaths = recipe.files.map((file) => file.path);
20898
21128
  if (new Set(filePaths).size !== filePaths.length) {
20899
- 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" });
20900
21130
  }
20901
21131
  const modelPaths = recipe.models.map((model) => model.path);
20902
21132
  if (new Set(modelPaths).size !== modelPaths.length) {
20903
- 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" });
20904
21134
  }
20905
21135
  if (filePaths.includes(RECIPE_FILE)) {
20906
21136
  context.addIssue({
20907
- code: z13.ZodIssueCode.custom,
21137
+ code: z14.ZodIssueCode.custom,
20908
21138
  path: ["files"],
20909
21139
  message: `${RECIPE_FILE} cannot hash itself`
20910
21140
  });
20911
21141
  }
20912
21142
  if (recipe.quality.recommendedNativeSize.min > recipe.quality.recommendedNativeSize.max) {
20913
21143
  context.addIssue({
20914
- code: z13.ZodIssueCode.custom,
21144
+ code: z14.ZodIssueCode.custom,
20915
21145
  path: ["quality", "recommendedNativeSize"],
20916
21146
  message: "min cannot exceed max"
20917
21147
  });
20918
21148
  }
20919
21149
  if (recipe.quality.paletteColors.min > recipe.quality.paletteColors.max) {
20920
21150
  context.addIssue({
20921
- code: z13.ZodIssueCode.custom,
21151
+ code: z14.ZodIssueCode.custom,
20922
21152
  path: ["quality", "paletteColors"],
20923
21153
  message: "min cannot exceed max"
20924
21154
  });
@@ -20927,7 +21157,7 @@ var RecipeSchema = z13.object({
20927
21157
  (file) => file.path === recipe.workflow.path && file.role === "workflow"
20928
21158
  )) {
20929
21159
  context.addIssue({
20930
- code: z13.ZodIssueCode.custom,
21160
+ code: z14.ZodIssueCode.custom,
20931
21161
  path: ["workflow", "path"],
20932
21162
  message: "must name a file whose role is workflow"
20933
21163
  });
@@ -20935,7 +21165,7 @@ var RecipeSchema = z13.object({
20935
21165
  if (recipe.provider === "comfyui") {
20936
21166
  if (!recipe.workflow) {
20937
21167
  context.addIssue({
20938
- code: z13.ZodIssueCode.custom,
21168
+ code: z14.ZodIssueCode.custom,
20939
21169
  path: ["workflow"],
20940
21170
  message: "ComfyUI recipes require workflow metadata"
20941
21171
  });
@@ -20944,7 +21174,7 @@ var RecipeSchema = z13.object({
20944
21174
  const options = recipe.style.providerOptions.comfyui;
20945
21175
  if (!options || typeof options !== "object" || Array.isArray(options)) {
20946
21176
  context.addIssue({
20947
- code: z13.ZodIssueCode.custom,
21177
+ code: z14.ZodIssueCode.custom,
20948
21178
  path: ["style", "providerOptions", "comfyui"],
20949
21179
  message: "ComfyUI recipes require ComfyUI provider options"
20950
21180
  });
@@ -20958,7 +21188,7 @@ var RecipeSchema = z13.object({
20958
21188
  };
20959
21189
  if (JSON.stringify(canonical3(options)) !== JSON.stringify(canonical3(expected))) {
20960
21190
  context.addIssue({
20961
- code: z13.ZodIssueCode.custom,
21191
+ code: z14.ZodIssueCode.custom,
20962
21192
  path: ["style", "providerOptions", "comfyui"],
20963
21193
  message: "must match workflow metadata and use {{recipeDir}} for workflowFile"
20964
21194
  });
@@ -20990,10 +21220,10 @@ function message5(error) {
20990
21220
  return error instanceof Error ? error.message : String(error);
20991
21221
  }
20992
21222
  async function readRecipeFile(recipePath, bundled) {
20993
- const absolute = path45.resolve(recipePath);
21223
+ const absolute = path46.resolve(recipePath);
20994
21224
  let raw;
20995
21225
  try {
20996
- raw = JSON.parse(await readFile31(absolute, "utf8"));
21226
+ raw = JSON.parse(await readFile32(absolute, "utf8"));
20997
21227
  } catch (error) {
20998
21228
  throw new Error(`Could not read recipe ${absolute}: ${message5(error)}`, { cause: error });
20999
21229
  }
@@ -21004,13 +21234,13 @@ async function readRecipeFile(recipePath, bundled) {
21004
21234
  ` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
21005
21235
  );
21006
21236
  }
21007
- 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 };
21008
21238
  }
21009
21239
  async function walkRecipeFiles(root) {
21010
- if (!existsSync34(root)) return [];
21240
+ if (!existsSync35(root)) return [];
21011
21241
  const out = [];
21012
21242
  for (const entry of await readdir3(root, { withFileTypes: true })) {
21013
- const resolved = path45.join(root, entry.name);
21243
+ const resolved = path46.join(root, entry.name);
21014
21244
  if (entry.isDirectory()) out.push(...await walkRecipeFiles(resolved));
21015
21245
  else if (entry.isFile() && entry.name === RECIPE_FILE) out.push(resolved);
21016
21246
  }
@@ -21021,7 +21251,7 @@ function bundledRecipeRoot() {
21021
21251
  }
21022
21252
  async function listBundledRecipes(root = bundledRecipeRoot()) {
21023
21253
  const recipes = await Promise.all(
21024
- (await walkRecipeFiles(path45.resolve(root))).map((file) => readRecipeFile(file, true))
21254
+ (await walkRecipeFiles(path46.resolve(root))).map((file) => readRecipeFile(file, true))
21025
21255
  );
21026
21256
  return recipes.sort(
21027
21257
  (a, b) => a.recipe.id.localeCompare(b.recipe.id) || compareVersions(b.recipe.version, a.recipe.version)
@@ -21044,9 +21274,9 @@ function parseSelector(selector) {
21044
21274
  return { id: match[1], version: match[2] };
21045
21275
  }
21046
21276
  async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
21047
- const local = path45.resolve(target);
21048
- if (existsSync34(local)) {
21049
- 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);
21050
21280
  return readRecipeFile(recipePath, false);
21051
21281
  }
21052
21282
  if (target.startsWith(".") || target.startsWith("/") || target.includes("\\")) {
@@ -21060,8 +21290,8 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
21060
21290
  return matches[0];
21061
21291
  }
21062
21292
  async function verifyFile(root, file) {
21063
- const absolute = path45.resolve(root, ...file.path.split("/"));
21064
- if (!existsSync34(absolute)) {
21293
+ const absolute = path46.resolve(root, ...file.path.split("/"));
21294
+ if (!existsSync35(absolute)) {
21065
21295
  return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
21066
21296
  }
21067
21297
  const actualSha256 = await sha256File(absolute);
@@ -21076,7 +21306,7 @@ async function verifyRecipe(target, options = {}) {
21076
21306
  const loaded = await resolveRecipe(target, options.bundledRoot);
21077
21307
  const actualIntegrity = recipeDigest(loaded.authored);
21078
21308
  const files = await Promise.all(loaded.recipe.files.map((file) => verifyFile(loaded.dir, file)));
21079
- const modelRoot = options.modelRoot ? path45.resolve(options.modelRoot) : null;
21309
+ const modelRoot = options.modelRoot ? path46.resolve(options.modelRoot) : null;
21080
21310
  const models = modelRoot ? await Promise.all(loaded.recipe.models.map(async (model) => ({
21081
21311
  ...await verifyFile(modelRoot, model),
21082
21312
  source: model.source,
@@ -21110,7 +21340,7 @@ async function verifyRecipe(target, options = {}) {
21110
21340
  };
21111
21341
  }
21112
21342
  function renderStyle(style, destination, cwd) {
21113
- const portableDir = path45.relative(cwd, destination).split(path45.sep).join("/") || ".";
21343
+ const portableDir = path46.relative(cwd, destination).split(path46.sep).join("/") || ".";
21114
21344
  const replace = (value) => {
21115
21345
  if (typeof value === "string") return value.replaceAll(RECIPE_DIR_TOKEN, portableDir);
21116
21346
  if (Array.isArray(value)) return value.map(replace);
@@ -21122,25 +21352,25 @@ function renderStyle(style, destination, cwd) {
21122
21352
  return replace(style);
21123
21353
  }
21124
21354
  async function installRecipe(target, options = {}) {
21125
- const cwd = path45.resolve(options.cwd ?? process.cwd());
21355
+ const cwd = path46.resolve(options.cwd ?? process.cwd());
21126
21356
  const loaded = await resolveRecipe(target, options.bundledRoot);
21127
21357
  const verification = await verifyRecipe(loaded.path);
21128
21358
  if (!verification.ok) {
21129
21359
  throw new Error(`Recipe ${loaded.recipe.id}@${loaded.recipe.version} failed integrity verification.`);
21130
21360
  }
21131
- 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);
21132
21362
  const sources = [
21133
21363
  { path: RECIPE_FILE, source: loaded.path },
21134
- ...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("/")) }))
21135
21365
  ];
21136
21366
  const files = await Promise.all(sources.map(async (file) => ({
21137
- path: path45.join(destination, ...file.path.split("/")),
21138
- data: await readFile31(file.source)
21367
+ path: path46.join(destination, ...file.path.split("/")),
21368
+ data: await readFile32(file.source)
21139
21369
  })));
21140
21370
  if (!options.force) {
21141
21371
  for (const file of files) {
21142
- if (!existsSync34(file.path)) continue;
21143
- const current = await readFile31(file.path);
21372
+ if (!existsSync35(file.path)) continue;
21373
+ const current = await readFile32(file.path);
21144
21374
  if (!current.equals(file.data)) {
21145
21375
  throw new OverwriteRefusedError(
21146
21376
  `Recipe destination has local changes: ${file.path}. Choose another --out or pass --force to replace declared recipe files.`,
@@ -21234,14 +21464,14 @@ async function runRecipe(args) {
21234
21464
  }, null, 2));
21235
21465
  } else {
21236
21466
  log(` installed ${result.recipe.id}@${result.recipe.version}`);
21237
- log(` ${path46.relative(process.cwd(), result.destination) || "."}`);
21467
+ log(` ${path47.relative(process.cwd(), result.destination) || "."}`);
21238
21468
  log(`
21239
21469
  Add this entry under your manifest's styles object:`);
21240
21470
  log(JSON.stringify({ [result.styleId]: result.style }, null, 2));
21241
21471
  if (result.recipe.models.length) {
21242
21472
  log(`
21243
21473
  Models are not downloaded automatically. Verify them with:`);
21244
- 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>`);
21245
21475
  }
21246
21476
  }
21247
21477
  return;
@@ -21249,7 +21479,7 @@ async function runRecipe(args) {
21249
21479
  }
21250
21480
 
21251
21481
  // src/cli/commands/refine.ts
21252
- import path47 from "path";
21482
+ import path48 from "path";
21253
21483
  async function runRefine(args) {
21254
21484
  if (args.from || args.subcommand === "approve") {
21255
21485
  if (!args.from) {
@@ -21279,12 +21509,12 @@ async function runRefine(args) {
21279
21509
  ` recovered ${result3.detection.columns}x${result3.detection.rows} native grid (${result3.detection.confidence}: ${result3.detection.consensus})`
21280
21510
  );
21281
21511
  log(` enforced ${result3.palette.length}-color palette without dithering`);
21282
- log(` wrote ${path47.relative(process.cwd(), result3.output)}`);
21283
- 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)}`);
21284
21514
  log(`
21285
21515
  Automated checks passed. Human 1\xD7 review is still required:`);
21286
21516
  log(
21287
- ` 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"`
21288
21518
  );
21289
21519
  }
21290
21520
  return;
@@ -21312,7 +21542,7 @@ async function runRefine(args) {
21312
21542
  if (args.json) log(JSON.stringify(result3, null, 2));
21313
21543
  else {
21314
21544
  log(
21315
- 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()}`
21316
21546
  );
21317
21547
  }
21318
21548
  return;
@@ -21321,7 +21551,7 @@ async function runRefine(args) {
21321
21551
  if (args.json) {
21322
21552
  log(JSON.stringify(result2, null, 2));
21323
21553
  } else {
21324
- 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)}`);
21325
21555
  for (const reason of result2.reasons) log(` ${reason}`);
21326
21556
  }
21327
21557
  if (!result2.safe) process.exitCode = 1;
@@ -21367,7 +21597,7 @@ async function runRefine(args) {
21367
21597
  log(` ${item.state.padEnd(16)} ${item.key} ${item.reason}`);
21368
21598
  if (item.state === "needs-approval") {
21369
21599
  log(
21370
- ` 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"`
21371
21601
  );
21372
21602
  }
21373
21603
  }
@@ -21378,9 +21608,9 @@ async function runRefine(args) {
21378
21608
  }
21379
21609
 
21380
21610
  // src/cli/commands/skeleton.ts
21381
- import path48 from "path";
21382
- import { existsSync as existsSync35 } from "fs";
21383
- 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";
21384
21614
 
21385
21615
  // src/skeleton-preview.ts
21386
21616
  var RIGHT = [255, 140, 40, 255];
@@ -21491,12 +21721,12 @@ var Canvas = class {
21491
21721
  async function runEstimateSkeleton(args) {
21492
21722
  const image = args.target;
21493
21723
  if (!image) throw new UsageError("estimate-skeleton needs an image path: pixelkiln estimate-skeleton <image> [--out keypoints.json]");
21494
- if (args.out && existsSync35(path48.resolve(args.out)) && !args.force) {
21724
+ if (args.out && existsSync36(path49.resolve(args.out)) && !args.force) {
21495
21725
  throw new UsageError(`${args.out} already exists; pass --force to replace it`);
21496
21726
  }
21497
- loadEnvFiles(path48.dirname(path48.resolve(args.manifest)));
21727
+ loadEnvFiles(path49.dirname(path49.resolve(args.manifest)));
21498
21728
  loadEnvFiles(process.cwd());
21499
- const bytes = await readFile32(path48.resolve(image));
21729
+ const bytes = await readFile33(path49.resolve(image));
21500
21730
  const metadata = imageMetadata(bytes);
21501
21731
  if (!metadata) throw new UsageError(`${image} is not a readable PNG or JPEG`);
21502
21732
  const sizeProblem = estimateSkeletonSizeProblem(metadata.width, metadata.height);
@@ -21508,7 +21738,7 @@ async function runEstimateSkeleton(args) {
21508
21738
  const frames = args.frames ?? DEFAULT_SCAFFOLD_FRAMES;
21509
21739
  const json = JSON.stringify(scaffoldSkeletonSet(res.keypoints, frames), null, 2);
21510
21740
  if (args.out) {
21511
- await writeFile18(path48.resolve(args.out), json + "\n");
21741
+ await writeFile19(path49.resolve(args.out), json + "\n");
21512
21742
  log(` wrote ${args.out}: the estimated pose and ${frames} frames to edit`);
21513
21743
  log(` preview it with: pixelkiln skeleton-preview ${args.out} --from ${image}`);
21514
21744
  } else {
@@ -21521,8 +21751,8 @@ async function runSkeletonPreview(args) {
21521
21751
  let sourceFile;
21522
21752
  let label;
21523
21753
  if (target.endsWith(".json")) {
21524
- keypointsFile = path48.resolve(target);
21525
- sourceFile = args.from ? path48.resolve(args.from) : void 0;
21754
+ keypointsFile = path49.resolve(target);
21755
+ sourceFile = args.from ? path49.resolve(args.from) : void 0;
21526
21756
  label = target;
21527
21757
  } else {
21528
21758
  const loaded = await loadManifest(args.manifest);
@@ -21534,31 +21764,31 @@ async function runSkeletonPreview(args) {
21534
21764
  );
21535
21765
  }
21536
21766
  keypointsFile = spec.revision.keypointsFile;
21537
- sourceFile = args.from ? path48.resolve(args.from) : spec.revision.sourceFile;
21767
+ sourceFile = args.from ? path49.resolve(args.from) : spec.revision.sourceFile;
21538
21768
  label = `${spec.styleId}/${spec.assetId}`;
21539
21769
  }
21540
- 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}`);
21541
21771
  let json;
21542
21772
  try {
21543
- json = JSON.parse(await readFile32(keypointsFile, "utf8"));
21773
+ json = JSON.parse(await readFile33(keypointsFile, "utf8"));
21544
21774
  } catch (error) {
21545
21775
  throw new UsageError(`${keypointsFile} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
21546
21776
  }
21547
21777
  const set = parseSkeletonSet(json, keypointsFile);
21548
21778
  let source;
21549
21779
  if (sourceFile) {
21550
- if (!existsSync35(sourceFile)) {
21780
+ if (!existsSync36(sourceFile)) {
21551
21781
  log(` source image does not exist yet, drawing on a plain ground: ${sourceFile}`);
21552
21782
  } else {
21553
- const bytes = await readFile32(sourceFile);
21783
+ const bytes = await readFile33(sourceFile);
21554
21784
  if (imageMetadata(bytes)?.format !== "png") throw new UsageError(`${sourceFile} must be a PNG to preview over`);
21555
21785
  source = decodePng(bytes);
21556
21786
  }
21557
21787
  }
21558
- const out = path48.resolve(args.out ?? keypointsFile.replace(/\.json$/, "") + ".preview.png");
21559
- 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 }));
21560
21790
  log(` ${label}: ${set.frames.length} frames after the starting pose`);
21561
- const shown = path48.relative(process.cwd(), out);
21791
+ const shown = path49.relative(process.cwd(), out);
21562
21792
  log(` wrote ${shown.startsWith("..") ? out : shown}`);
21563
21793
  }
21564
21794
 
@@ -21604,15 +21834,15 @@ async function runTools(args) {
21604
21834
  }
21605
21835
 
21606
21836
  // src/cli/commands/workspace.ts
21607
- import path49 from "path";
21608
- import { existsSync as existsSync36 } from "fs";
21837
+ import path50 from "path";
21838
+ import { existsSync as existsSync37 } from "fs";
21609
21839
  async function runWorkspace(args) {
21610
- const workspacePath = path49.resolve(args.workspace ?? "pixelkiln.workspace.json");
21611
- const dir = path49.dirname(workspacePath);
21840
+ const workspacePath = path50.resolve(args.workspace ?? "pixelkiln.workspace.json");
21841
+ const dir = path50.dirname(workspacePath);
21612
21842
  if (args.subcommand === "add") {
21613
- const manifestPath = path49.resolve(args.target);
21843
+ const manifestPath = path50.resolve(args.target);
21614
21844
  const loadedTarget = await loadManifest(manifestPath);
21615
- 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");
21616
21846
  const ws = await loadWorkspace(workspacePath);
21617
21847
  const id = args.name ?? loadedTarget.manifest.name;
21618
21848
  if (ws.projects.some((p) => p.id === id)) {
@@ -21632,10 +21862,10 @@ async function runWorkspace(args) {
21632
21862
  ...args.account ? { account: args.account } : {}
21633
21863
  };
21634
21864
  await saveWorkspace(workspacePath, { version: 1, projects: [...ws.projects, project] });
21635
- log(` registered "${id}" in ${path49.relative(process.cwd(), workspacePath)}`);
21865
+ log(` registered "${id}" in ${path50.relative(process.cwd(), workspacePath)}`);
21636
21866
  log(` manifest: ${project.manifest}`);
21637
21867
  log(` lock: ${project.lock}`);
21638
- if (!existsSync36(lockPath)) {
21868
+ if (!existsSync37(lockPath)) {
21639
21869
  log(
21640
21870
  ` warning: no lockfile there yet; this project contributes no claims until one is generated`
21641
21871
  );
@@ -21644,7 +21874,7 @@ async function runWorkspace(args) {
21644
21874
  }
21645
21875
  if (args.subcommand === "remove") {
21646
21876
  const ws = await loadWorkspace(workspacePath);
21647
- const resolvedTarget = path49.resolve(args.target);
21877
+ const resolvedTarget = path50.resolve(args.target);
21648
21878
  const match = ws.projects.find(
21649
21879
  (p) => p.id === args.target || resolveProject(dir, p).manifestPath === resolvedTarget
21650
21880
  );
@@ -21655,11 +21885,11 @@ async function runWorkspace(args) {
21655
21885
  version: 1,
21656
21886
  projects: ws.projects.filter((p) => p !== match)
21657
21887
  });
21658
- log(` removed "${match.id}" from ${path49.relative(process.cwd(), workspacePath)}`);
21888
+ log(` removed "${match.id}" from ${path50.relative(process.cwd(), workspacePath)}`);
21659
21889
  return;
21660
21890
  }
21661
21891
  if (args.subcommand === "list") {
21662
- if (!existsSync36(workspacePath)) {
21892
+ if (!existsSync37(workspacePath)) {
21663
21893
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
21664
21894
  }
21665
21895
  const ws = await loadWorkspace(workspacePath);
@@ -21667,9 +21897,9 @@ async function runWorkspace(args) {
21667
21897
  if (args.json) {
21668
21898
  log(JSON.stringify({ version: 1, workspace: workspacePath, projects: ws.projects, diagnostics }, null, 2));
21669
21899
  } else if (!ws.projects.length) {
21670
- log(` no projects registered in ${path49.relative(process.cwd(), workspacePath)}`);
21900
+ log(` no projects registered in ${path50.relative(process.cwd(), workspacePath)}`);
21671
21901
  } else {
21672
- 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)}:`);
21673
21903
  for (const p of ws.projects) {
21674
21904
  log(` ${p.id.padEnd(24)} ${p.manifest.padEnd(40)} (${p.provider}${p.account ? `, ${p.account}` : ""})`);
21675
21905
  }
@@ -21679,7 +21909,7 @@ async function runWorkspace(args) {
21679
21909
  return;
21680
21910
  }
21681
21911
  if (args.subcommand === "status") {
21682
- if (!existsSync36(workspacePath)) {
21912
+ if (!existsSync37(workspacePath)) {
21683
21913
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
21684
21914
  }
21685
21915
  const ws = await loadWorkspace(workspacePath);
@@ -21687,7 +21917,7 @@ async function runWorkspace(args) {
21687
21917
  if (args.json) {
21688
21918
  log(JSON.stringify({ ...report, workspace: workspacePath }, null, 2));
21689
21919
  } else {
21690
- log(` workspace: ${path49.relative(process.cwd(), workspacePath)}`);
21920
+ log(` workspace: ${path50.relative(process.cwd(), workspacePath)}`);
21691
21921
  for (const p of report.projects) {
21692
21922
  if (p.error) {
21693
21923
  log(`
@@ -21741,8 +21971,8 @@ async function runHelp() {
21741
21971
  async function runVersion() {
21742
21972
  for (const up of ["../package.json", "../../package.json"]) {
21743
21973
  const url = new URL(up, import.meta.url);
21744
- if (!existsSync37(url)) continue;
21745
- const pkg = JSON.parse(await readFile33(url, "utf8"));
21974
+ if (!existsSync38(url)) continue;
21975
+ const pkg = JSON.parse(await readFile34(url, "utf8"));
21746
21976
  if (pkg.name === "pixelkiln") {
21747
21977
  log(`${pkg.name} ${pkg.version}`);
21748
21978
  return;