pixelkiln 0.83.0 → 0.85.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(path52) {
669
669
  return new Promise((resolve, reject) => {
670
670
  const hash = createHash("sha256");
671
- const stream = createReadStream(path50);
671
+ const stream = createReadStream(path52);
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. */
@@ -4754,7 +4818,7 @@ async function confirm(question, auto) {
4754
4818
  }
4755
4819
 
4756
4820
  // src/cli/commands/account.ts
4757
- import path22 from "path";
4821
+ import path23 from "path";
4758
4822
  import { existsSync as existsSync16 } from "fs";
4759
4823
 
4760
4824
  // src/env.ts
@@ -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(path52, 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}${path52}`, {
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"} ${path52} \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"} ${path52} 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 path52;
6613
6678
  let body;
6614
6679
  if (args.mode === "v3") {
6615
- path50 = "/create-character-v3";
6680
+ path52 = "/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
+ path52 = "/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
+ path52 = "/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
+ path52 = 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(path52, { method: "POST", body: JSON.stringify(body) });
6750
+ return validateResponse(CharacterSubmitSchema, raw, path52.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(path52, 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}${path52}`, {
9607
9731
  ...init,
9608
9732
  headers: {
9609
9733
  "Content-Type": "application/json",
@@ -9912,8 +10036,13 @@ function retroError(value) {
9912
10036
  }
9913
10037
 
9914
10038
  // src/providers/scenario.ts
10039
+ import { createHash as createHash3 } from "crypto";
10040
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, renameSync, writeFileSync as writeFileSync2 } from "fs";
10041
+ import os2 from "os";
10042
+ import path13 from "path";
9915
10043
  var DEFAULT_BASE_URL3 = "https://api.cloud.scenario.com/v1";
9916
10044
  var SOURCE_PROTOCOL2 = "scenario:";
10045
+ var SUBMIT_RETRIES = 8;
9917
10046
  var PROTECTED_PARAMETERS = /* @__PURE__ */ new Set([
9918
10047
  "dryRun",
9919
10048
  "height",
@@ -9960,7 +10089,11 @@ var ScenarioClient = class {
9960
10089
  async submit(modelId, body, projectId) {
9961
10090
  const response = await this.call(
9962
10091
  `/generate/custom/${encodeURIComponent(modelId)}`,
9963
- { method: "POST", body: JSON.stringify(body) },
10092
+ // A 429 here means this account's concurrent-job limit refused the
10093
+ // request, so nothing was accepted or billed; wait it out instead of
10094
+ // failing the asset. The default four retries give up after about half
10095
+ // a minute, before a running job has finished.
10096
+ { method: "POST", body: JSON.stringify(body), retries: SUBMIT_RETRIES },
9964
10097
  { projectId }
9965
10098
  );
9966
10099
  const job = response?.job;
@@ -9968,6 +10101,29 @@ var ScenarioClient = class {
9968
10101
  if (!jobId) throw new Error("Scenario did not return an async job id");
9969
10102
  return jobId;
9970
10103
  }
10104
+ /**
10105
+ * Uploads one image (POST /assets) and returns its asset id, which a model's
10106
+ * reference input takes. Free, and idempotent per image: the id is kept by
10107
+ * content hash so a rerun does not upload the same bytes again.
10108
+ */
10109
+ async uploadImage(image, projectId) {
10110
+ const bytes = Buffer.from(image.base64, "base64");
10111
+ const digest2 = sha256(bytes);
10112
+ const cached = readUploadCache()[uploadCacheKey(this.baseUrl, projectId, digest2)];
10113
+ if (cached) return cached;
10114
+ const response = await this.call(
10115
+ "/assets",
10116
+ {
10117
+ method: "POST",
10118
+ body: JSON.stringify({ image: image.base64, name: `pixelkiln-${digest2.slice(0, 12)}.${image.format === "png" ? "png" : "jpg"}` })
10119
+ },
10120
+ { projectId }
10121
+ );
10122
+ const id = response?.asset?.id;
10123
+ if (typeof id !== "string" || !id) throw new Error("Scenario did not return an uploaded asset id");
10124
+ writeUploadCache(uploadCacheKey(this.baseUrl, projectId, digest2), id);
10125
+ return id;
10126
+ }
9971
10127
  async job(id, projectId) {
9972
10128
  const response = await this.call(`/jobs/${encodeURIComponent(id)}`, {}, { projectId });
9973
10129
  const raw = response?.job;
@@ -10094,6 +10250,14 @@ var ScenarioProvider = class _ScenarioProvider {
10094
10250
  supports(generator) {
10095
10251
  return generator === "map";
10096
10252
  }
10253
+ /**
10254
+ * An edit is a generation with the parent as a reference image, which the
10255
+ * editing models (GPT Image, Gemini, FLUX) take. Masked inpainting and the
10256
+ * PixelLab-specific modes have no common Scenario input.
10257
+ */
10258
+ supportsRevision(mode2) {
10259
+ return mode2 === "image-to-image";
10260
+ }
10097
10261
  estimate(spec) {
10098
10262
  const options = scenarioOptions(spec);
10099
10263
  return {
@@ -10114,11 +10278,31 @@ var ScenarioProvider = class _ScenarioProvider {
10114
10278
  if (!Number.isInteger(count) || count < 1 || count > 4) {
10115
10279
  throw new Error("Scenario numOutputs must be a whole number from 1 to 4");
10116
10280
  }
10117
- if (spec.width < 128 || spec.width > 2048 || spec.width % 16 !== 0 || spec.height < 128 || spec.height > 2048 || spec.height % 16 !== 0) {
10118
- throw new Error("Scenario width and height must be multiples of 16 from 128 to 2048");
10281
+ if (!Number.isInteger(spec.width) || spec.width < 16 || spec.width > 4096 || !Number.isInteger(spec.height) || spec.height < 16 || spec.height > 4096) {
10282
+ throw new Error("Scenario width and height must be whole numbers from 16 to 4096");
10119
10283
  }
10120
- if (styleImages.length) {
10121
- throw new Error("Scenario styleImages are not supported yet; use a reusable Scenario model");
10284
+ const revision = spec.revision;
10285
+ if (revision) {
10286
+ if (revision.sourceMembers?.length) {
10287
+ throw new Error(
10288
+ `Scenario edits one image; ${revision.sourceAssetId} is a ${revision.sourceMembers.length}-member set`
10289
+ );
10290
+ }
10291
+ if (revision.strength != null) {
10292
+ throw new Error("Scenario image-to-image takes no strength; use the model's own parameters");
10293
+ }
10294
+ }
10295
+ const parameter = referenceParameter(options);
10296
+ if ((styleImages.length || revision) && Object.hasOwn(options.parameters ?? {}, parameter)) {
10297
+ throw new Error(
10298
+ `Scenario parameters.${parameter} is the reference-image input and is filled from styleImages and revisions`
10299
+ );
10300
+ }
10301
+ if (styleImages.length + (revision ? 1 : 0) > 1 && options.referenceArray === false) {
10302
+ throw new Error(`Scenario ${parameter} takes one image; use one style image or a revision, not both`);
10303
+ }
10304
+ if (options.referenceParameter != null && (typeof options.referenceParameter !== "string" || !options.referenceParameter.trim())) {
10305
+ throw new Error("Scenario referenceParameter must be a non-empty string");
10122
10306
  }
10123
10307
  if (options.projectId != null && (typeof options.projectId !== "string" || !options.projectId.trim())) {
10124
10308
  throw new Error("Scenario projectId must be a non-empty string");
@@ -10131,7 +10315,12 @@ var ScenarioProvider = class _ScenarioProvider {
10131
10315
  async submit(spec, styleImages) {
10132
10316
  this.validate(spec, styleImages);
10133
10317
  const options = scenarioOptions(spec);
10134
- const body = requestBody(spec, options);
10318
+ const references = [];
10319
+ if (spec.revision) references.push(readRevisionSource(spec));
10320
+ for (const image of styleImages) references.push({ base64: image.base64, format: image.format });
10321
+ const referenceAssetIds = [];
10322
+ for (const image of references) referenceAssetIds.push(await this.client.uploadImage(image, options.projectId));
10323
+ const body = requestBody(spec, options, referenceAssetIds);
10135
10324
  const quote = await this.client.quote(options.modelId, body, options.projectId);
10136
10325
  if (quote.total > options.maxComputeUnits + Number.EPSILON) {
10137
10326
  throw new Error(
@@ -10148,7 +10337,8 @@ var ScenarioProvider = class _ScenarioProvider {
10148
10337
  quotedComputeUnits: quote.total,
10149
10338
  creativeUnitsCost: quote.creativeUnitsCost,
10150
10339
  costDetails: quote.costDetails,
10151
- ipDetectionCost: quote.ipDetectionCost
10340
+ ipDetectionCost: quote.ipDetectionCost,
10341
+ ...referenceAssetIds.length ? { referenceAssetIds } : {}
10152
10342
  }
10153
10343
  };
10154
10344
  }
@@ -10170,7 +10360,13 @@ var ScenarioProvider = class _ScenarioProvider {
10170
10360
  }
10171
10361
  const assetIds = scenarioAssetIds(job);
10172
10362
  if (!assetIds.length) return { status: "failed", error: "Scenario job returned no asset IDs" };
10173
- const assets = await this.assets(assetIds, reference.projectId);
10363
+ let assets;
10364
+ try {
10365
+ assets = await this.assets(assetIds, reference.projectId);
10366
+ } catch (error) {
10367
+ if (error instanceof UnsupportedScenarioOutput) return { status: "failed", error: error.message };
10368
+ throw error;
10369
+ }
10174
10370
  if (assets.length > 1) {
10175
10371
  return {
10176
10372
  status: "review",
@@ -10225,6 +10421,32 @@ var ScenarioProvider = class _ScenarioProvider {
10225
10421
  return assets.sort((left, right) => left.outputIndex - right.outputIndex || (jobOrder.get(left.id) ?? Number.MAX_SAFE_INTEGER) - (jobOrder.get(right.id) ?? Number.MAX_SAFE_INTEGER) || left.id.localeCompare(right.id));
10226
10422
  }
10227
10423
  };
10424
+ function uploadCachePath() {
10425
+ return process.env.SCENARIO_UPLOAD_CACHE ?? path13.join(os2.tmpdir(), "pixelkiln-scenario", "uploads.json");
10426
+ }
10427
+ function uploadCacheKey(baseUrl, projectId, digest2) {
10428
+ return createHash3("sha256").update(`${baseUrl}
10429
+ ${projectId ?? ""}
10430
+ ${digest2}`).digest("hex");
10431
+ }
10432
+ function readUploadCache() {
10433
+ try {
10434
+ const value = JSON.parse(readFileSync3(uploadCachePath(), "utf8"));
10435
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
10436
+ } catch {
10437
+ return {};
10438
+ }
10439
+ }
10440
+ function writeUploadCache(key, assetId) {
10441
+ try {
10442
+ const file = uploadCachePath();
10443
+ mkdirSync2(path13.dirname(file), { recursive: true });
10444
+ const tmp = `${file}.${process.pid}.tmp`;
10445
+ writeFileSync2(tmp, JSON.stringify({ ...readUploadCache(), [key]: assetId }));
10446
+ renameSync(tmp, file);
10447
+ } catch {
10448
+ }
10449
+ }
10228
10450
  function scenarioOptions(spec) {
10229
10451
  return spec.providerOptions;
10230
10452
  }
@@ -10240,9 +10462,25 @@ function scenarioBaseUrl(value) {
10240
10462
  }
10241
10463
  return url.href.replace(/\/$/, "");
10242
10464
  }
10243
- function requestBody(spec, options) {
10465
+ function referenceParameter(options) {
10466
+ return options.referenceParameter?.trim() || "referenceImages";
10467
+ }
10468
+ function readRevisionSource(spec) {
10469
+ const revision = spec.revision;
10470
+ if (!revision.sourceFile || !revision.sourceSha256 || !revision.sourceFormat) {
10471
+ throw new Error(`${spec.styleId}/${spec.assetId}: revision source is not ready`);
10472
+ }
10473
+ const bytes = readFileSync3(revision.sourceFile);
10474
+ if (sha256(bytes) !== revision.sourceSha256) {
10475
+ throw new Error(`${spec.styleId}/${spec.assetId}: revision source changed after the manifest was resolved`);
10476
+ }
10477
+ return { base64: bytes.toString("base64"), format: revision.sourceFormat };
10478
+ }
10479
+ function requestBody(spec, options, referenceAssetIds = []) {
10480
+ const parameter = referenceParameter(options);
10244
10481
  return {
10245
10482
  ...options.parameters ?? {},
10483
+ ...referenceAssetIds.length ? { [parameter]: options.referenceArray === false ? referenceAssetIds[0] : referenceAssetIds } : {},
10246
10484
  prompt: spec.prompt,
10247
10485
  width: spec.width,
10248
10486
  height: spec.height,
@@ -10332,9 +10570,11 @@ function scenarioAssetIds(job) {
10332
10570
  function assetDownloadUrl(asset) {
10333
10571
  return asset.originalFileUrl || asset.url;
10334
10572
  }
10573
+ var UnsupportedScenarioOutput = class extends Error {
10574
+ };
10335
10575
  function assertPngAsset(asset) {
10336
10576
  if (asset.originalMimeType && asset.originalMimeType !== MediaType.PNG) {
10337
- throw new Error(
10577
+ throw new UnsupportedScenarioOutput(
10338
10578
  `Scenario asset ${asset.id} is ${asset.originalMimeType}; PixelKiln's Scenario MVP requires PNG output`
10339
10579
  );
10340
10580
  }
@@ -10462,7 +10702,7 @@ registerProvider({
10462
10702
  // src/manifest.ts
10463
10703
  import { readFile as readFile6 } from "fs/promises";
10464
10704
  import { existsSync as existsSync8 } from "fs";
10465
- import path13 from "path";
10705
+ import path14 from "path";
10466
10706
 
10467
10707
  // src/loop-directions.ts
10468
10708
  var SINGLE_OUTPUT_FIELDS = ["file", "cell", "remoteId", "source", "sourceByStyle", "outputRole"];
@@ -10551,7 +10791,7 @@ function isObject2(value) {
10551
10791
 
10552
10792
  // src/manifest.ts
10553
10793
  async function loadManifest(manifestPath) {
10554
- const abs = path13.resolve(manifestPath);
10794
+ const abs = path14.resolve(manifestPath);
10555
10795
  if (!existsSync8(abs)) throw new ProjectError(`No manifest at ${abs}`);
10556
10796
  const expansion = expandLoopDirections(JSON.parse(await readFile6(abs, "utf8")));
10557
10797
  if (expansion.issues.length) {
@@ -10644,7 +10884,7 @@ ${issues}`);
10644
10884
  ${unknownReferences.map((i) => ` ${i}`).join("\n")}`);
10645
10885
  }
10646
10886
  const loopFamilies = Object.keys(expansion.families).length ? { loopFamilies: expansion.families } : {};
10647
- return { manifest: parsed.data, root: path13.dirname(abs), path: abs, ...loopFamilies };
10887
+ return { manifest: parsed.data, root: path14.dirname(abs), path: abs, ...loopFamilies };
10648
10888
  }
10649
10889
  function parentAssetId(asset) {
10650
10890
  return asset?.revision?.from ?? asset?.state?.of ?? asset?.animation?.of ?? asset?.portrait?.of ?? asset?.outfit?.of;
@@ -10745,7 +10985,7 @@ async function resolveSpecs(loaded, filter) {
10745
10985
  }
10746
10986
  const styleImageCache = /* @__PURE__ */ new Map();
10747
10987
  async function loadStyleImage(rel, what = "Style image") {
10748
- const abs = path13.resolve(root, rel);
10988
+ const abs = path14.resolve(root, rel);
10749
10989
  let hit = styleImageCache.get(abs);
10750
10990
  if (!hit) {
10751
10991
  if (!existsSync8(abs)) throw new Error(`${what} not found: ${abs}`);
@@ -10775,7 +11015,7 @@ async function resolveSpecs(loaded, filter) {
10775
11015
  styleImageDimensions.push(loadedImage);
10776
11016
  }
10777
11017
  const resolvedImages = style.styleImages.map((image) => {
10778
- const hit = styleImageCache.get(path13.resolve(root, image.path));
11018
+ const hit = styleImageCache.get(path14.resolve(root, image.path));
10779
11019
  return { base64: hit.base64, width: hit.width, height: hit.height, format: hit.format };
10780
11020
  });
10781
11021
  const styleSpecs = /* @__PURE__ */ new Map();
@@ -10876,9 +11116,9 @@ async function resolveSpecs(loaded, filter) {
10876
11116
  } else {
10877
11117
  prompt = [style.promptPrefix, subject, style.promptSuffix].map((p) => p.trim()).filter(Boolean).join(", ");
10878
11118
  }
10879
- const relFile = asset.file ?? path13.join(asset.category ?? "", `${assetId}.png`);
10880
- const outFile = path13.resolve(root, style.outDir, relFile);
10881
- const qualityOutFile = style.quality ? path13.resolve(root, style.quality.outDir, pngPath(relFile)) : void 0;
11119
+ const relFile = asset.file ?? path14.join(asset.category ?? "", `${assetId}.png`);
11120
+ const outFile = path14.resolve(root, style.outDir, relFile);
11121
+ const qualityOutFile = style.quality ? path14.resolve(root, style.quality.outDir, pngPath(relFile)) : void 0;
10882
11122
  const tileSize = generator === "tiles" ? size : style.tileSize ?? 32;
10883
11123
  const tileVariations = tileFeatureOutputCount(generator === "tiles" ? style.tileFeature : void 0) ?? tileVariationCount(countNumberedDescriptions(prompt));
10884
11124
  const terrainTiles = generator === "terrain" ? terrainTileCount(style.terrainTransitionSize) : 0;
@@ -10973,7 +11213,7 @@ async function resolveSpecs(loaded, filter) {
10973
11213
  minGridConfidence: style.quality.minGridConfidence,
10974
11214
  ...style.quality.minTransparency == null ? {} : { minTransparency: style.quality.minTransparency },
10975
11215
  ...style.quality.fixerRevision ? { fixerRevision: style.quality.fixerRevision } : {},
10976
- ...style.quality.fixerPython ? { fixerPython: path13.resolve(root, style.quality.fixerPython) } : {},
11216
+ ...style.quality.fixerPython ? { fixerPython: path14.resolve(root, style.quality.fixerPython) } : {},
10977
11217
  ...style.quality.fps == null ? {} : { fps: style.quality.fps }
10978
11218
  }
10979
11219
  } : {},
@@ -11109,7 +11349,7 @@ async function resolveSpecs(loaded, filter) {
11109
11349
  animation: sourceSpec.character.animation,
11110
11350
  outfit: {
11111
11351
  reference: {
11112
- path: path13.resolve(root, asset.outfit.reference),
11352
+ path: path14.resolve(root, asset.outfit.reference),
11113
11353
  sha256: referenceImage.hash,
11114
11354
  width: referenceImage.width,
11115
11355
  height: referenceImage.height,
@@ -11131,23 +11371,23 @@ async function resolveSpecs(loaded, filter) {
11131
11371
  );
11132
11372
  }
11133
11373
  const sourceSpec = await finalize(asset.revision.from);
11134
- const sourceStem = sourceSpec.quality?.outFile ?? (sourceSpec.source ? path13.resolve(root, sourceSpec.source) : sourceSpec.outFile);
11374
+ const sourceStem = sourceSpec.quality?.outFile ?? (sourceSpec.source ? path14.resolve(root, sourceSpec.source) : sourceSpec.outFile);
11135
11375
  const sourceMembers = await revisionSourceMembers(sourceStem, `revision source ${asset.revision.from}`);
11136
11376
  const sourceFile = sourceMembers?.[0]?.file ?? sourceStem;
11137
11377
  const sourceImage = sourceMembers ? { ...sourceMembers[0], hash: sha256(JSON.stringify(sourceMembers.map((member) => member.sha256))) } : await optionalRevisionImage(sourceFile, "revision source");
11138
11378
  const sourceFps = sourceSpec.character?.animation?.fps ?? sourceSpec.objectPro?.animation?.fps ?? sourceSpec.revision?.fps ?? sourceSpec.revision?.sourceFps;
11139
- const maskFile = asset.revision.mask ? path13.resolve(root, asset.revision.mask) : void 0;
11379
+ const maskFile = asset.revision.mask ? path14.resolve(root, asset.revision.mask) : void 0;
11140
11380
  const maskImage = maskFile ? await optionalRevisionImage(maskFile, "revision mask", "png") : null;
11141
11381
  if (sourceImage && maskImage && (sourceImage.width !== maskImage.width || sourceImage.height !== maskImage.height)) {
11142
11382
  throw new Error(
11143
11383
  `Revision mask for ${styleId}/${assetId} is ${maskImage.width}x${maskImage.height}; source ${asset.revision.from} is ${sourceImage.width}x${sourceImage.height}`
11144
11384
  );
11145
11385
  }
11146
- const paletteImageFile = asset.revision.paletteImage ? path13.resolve(root, asset.revision.paletteImage) : void 0;
11386
+ const paletteImageFile = asset.revision.paletteImage ? path14.resolve(root, asset.revision.paletteImage) : void 0;
11147
11387
  const paletteImage = paletteImageFile ? await optionalRevisionImage(paletteImageFile, "revision palette image", "png") : null;
11148
- const lastFrameFile = asset.revision.lastFrame ? path13.resolve(root, asset.revision.lastFrame) : void 0;
11388
+ const lastFrameFile = asset.revision.lastFrame ? path14.resolve(root, asset.revision.lastFrame) : void 0;
11149
11389
  const lastFrame = lastFrameFile ? await optionalRevisionImage(lastFrameFile, "revision last frame") : null;
11150
- const keypointsFile = asset.revision.keypointsFile ? path13.resolve(root, asset.revision.keypointsFile) : void 0;
11390
+ const keypointsFile = asset.revision.keypointsFile ? path14.resolve(root, asset.revision.keypointsFile) : void 0;
11151
11391
  const keypoints = keypointsFile ? await optionalSkeletonSet(keypointsFile) : null;
11152
11392
  resolved.revision = {
11153
11393
  mode: asset.revision.mode,
@@ -11201,6 +11441,10 @@ async function resolveSpecs(loaded, filter) {
11201
11441
  } : {},
11202
11442
  ...asset.revision.skeletonTemplate ? { skeletonTemplate: asset.revision.skeletonTemplate } : {},
11203
11443
  ...asset.revision.description ? { description: asset.revision.description } : {},
11444
+ ...asset.revision.removalTask ? { removalTask: asset.revision.removalTask } : {},
11445
+ ...asset.revision.driftThreshold == null ? {} : { driftThreshold: asset.revision.driftThreshold },
11446
+ ...asset.revision.subjectDescription ? { subjectDescription: asset.revision.subjectDescription } : {},
11447
+ ...asset.revision.initialPose ? { initialPose: asset.revision.initialPose } : {},
11204
11448
  ...asset.revision.engine ? { engine: asset.revision.engine } : {}
11205
11449
  };
11206
11450
  }
@@ -11310,7 +11554,7 @@ async function resolveSpecs(loaded, filter) {
11310
11554
  return specs;
11311
11555
  }
11312
11556
  function pngPath(file) {
11313
- const extension = path13.extname(file);
11557
+ const extension = path14.extname(file);
11314
11558
  return extension ? `${file.slice(0, -extension.length)}.png` : `${file}.png`;
11315
11559
  }
11316
11560
  async function resolveMapScene(root, styleId, assetId, generator, scene, provider) {
@@ -11320,12 +11564,12 @@ async function resolveMapScene(root, styleId, assetId, generator, scene, provide
11320
11564
  if (!provider.supportsMapScene) {
11321
11565
  throw new Error(`assets.${assetId}.scene: provider "${provider.id}" cannot draw into a scene; PixelLab's /map-objects can`);
11322
11566
  }
11323
- const file = path13.resolve(root, scene.image);
11567
+ const file = path14.resolve(root, scene.image);
11324
11568
  const image = await optionalRevisionImage(file, "scene image", "png");
11325
11569
  const base = { file, sha256: image?.hash ?? null, width: image?.width ?? null, height: image?.height ?? null };
11326
11570
  const placement = scene.placement;
11327
11571
  if ("mask" in placement) {
11328
- const maskFile = path13.resolve(root, placement.mask);
11572
+ const maskFile = path14.resolve(root, placement.mask);
11329
11573
  const mask = await optionalRevisionImage(maskFile, "scene mask", "png");
11330
11574
  if (image && mask && (image.width !== mask.width || image.height !== mask.height)) {
11331
11575
  throw new Error(
@@ -11397,7 +11641,7 @@ async function resolveStyleImages(loaded, styleId) {
11397
11641
  if (!style) throw new Error(`Unknown style "${styleId}"`);
11398
11642
  const out = [];
11399
11643
  for (const img of style.styleImages) {
11400
- const file = path13.resolve(loaded.root, img.path);
11644
+ const file = path14.resolve(loaded.root, img.path);
11401
11645
  const buf = await readFile6(file);
11402
11646
  const metadata = imageMetadata(buf);
11403
11647
  if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${file}`);
@@ -11425,7 +11669,7 @@ async function resolveCharacterShape(asset, style, kind, files) {
11425
11669
  };
11426
11670
  if (asset.concept) {
11427
11671
  const image = await files.load(asset.concept, "Concept image");
11428
- shape.concept = { path: path13.resolve(files.root, asset.concept), sha256: image.hash, width: image.width, height: image.height, format: image.format };
11672
+ shape.concept = { path: path14.resolve(files.root, asset.concept), sha256: image.hash, width: image.width, height: image.height, format: image.format };
11429
11673
  }
11430
11674
  if (asset.reference) {
11431
11675
  const byDirection = typeof asset.reference === "string" ? { south: asset.reference } : asset.reference;
@@ -11433,7 +11677,7 @@ async function resolveCharacterShape(asset, style, kind, files) {
11433
11677
  for (const [direction, rel] of Object.entries(byDirection)) {
11434
11678
  const image = await files.load(rel, `Reference sprite (${direction})`);
11435
11679
  shape.reference[direction] = {
11436
- path: path13.resolve(files.root, rel),
11680
+ path: path14.resolve(files.root, rel),
11437
11681
  sha256: image.hash,
11438
11682
  width: image.width,
11439
11683
  height: image.height,
@@ -11454,7 +11698,7 @@ async function resolveCharacterShape(asset, style, kind, files) {
11454
11698
  const animationMode = asset.animation.mode ?? (asset.animation.template ? "template" : "v3");
11455
11699
  const pose = async (rel, what) => {
11456
11700
  const image = await files.load(rel, what);
11457
- return { path: path13.resolve(files.root, rel), sha256: image.hash, width: image.width, height: image.height, format: image.format };
11701
+ return { path: path14.resolve(files.root, rel), sha256: image.hash, width: image.width, height: image.height, format: image.format };
11458
11702
  };
11459
11703
  shape.animation = {
11460
11704
  mode: animationMode,
@@ -11485,7 +11729,7 @@ async function resolveObjectProShape(assetId, asset, style, kind, files) {
11485
11729
  const south = typeof asset.reference === "string" ? asset.reference : asset.reference.south;
11486
11730
  if (south) {
11487
11731
  const image = await files.load(south, "Reference sprite");
11488
- shape.reference = { path: path13.resolve(files.root, south), sha256: image.hash, width: image.width, height: image.height, format: image.format };
11732
+ shape.reference = { path: path14.resolve(files.root, south), sha256: image.hash, width: image.width, height: image.height, format: image.format };
11489
11733
  }
11490
11734
  }
11491
11735
  if (asset.state) {
@@ -11513,7 +11757,7 @@ async function resolveObjectProShape(assetId, asset, style, kind, files) {
11513
11757
  }
11514
11758
  const pose = async (rel, what) => {
11515
11759
  const image = await files.load(rel, what);
11516
- return { path: path13.resolve(files.root, rel), sha256: image.hash, width: image.width, height: image.height, format: image.format };
11760
+ return { path: path14.resolve(files.root, rel), sha256: image.hash, width: image.width, height: image.height, format: image.format };
11517
11761
  };
11518
11762
  shape.animation = {
11519
11763
  mode: asset.animation.mode ?? "v3",
@@ -11530,9 +11774,9 @@ async function resolveObjectProShape(assetId, asset, style, kind, files) {
11530
11774
  }
11531
11775
 
11532
11776
  // src/pipeline/fetch.ts
11533
- import { existsSync as existsSync9, readFileSync as readFileSync3 } from "fs";
11777
+ import { existsSync as existsSync9, readFileSync as readFileSync4 } from "fs";
11534
11778
  import { mkdir as mkdir4, readFile as readFile7, rename as rename4, rm as rm4, writeFile as writeFile4 } from "fs/promises";
11535
- import path14 from "path";
11779
+ import path15 from "path";
11536
11780
 
11537
11781
  // src/source-url.ts
11538
11782
  var SENSITIVE_QUERY_KEYS = /* @__PURE__ */ new Set([
@@ -11578,7 +11822,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
11578
11822
  const result = { downloaded: 0, skipped: 0, failed: 0, ...opts.refresh ? { unchanged: 0 } : {} };
11579
11823
  const specByKey = new Map(specs.map((s) => [lockKey(s.styleId, s.assetId), s]));
11580
11824
  normalizeLockOutputPaths(lock, specs);
11581
- const cacheDir = opts.cacheDir === false ? null : path14.resolve(opts.cacheDir ?? path14.join(path14.dirname(lockPath), ".pixelkiln", "cache"));
11825
+ const cacheDir = opts.cacheDir === false ? null : path15.resolve(opts.cacheDir ?? path15.join(path15.dirname(lockPath), ".pixelkiln", "cache"));
11582
11826
  const pending = Object.entries(lock.entries).filter(([key, e]) => {
11583
11827
  if (e.provider !== provider.id || !specByKey.has(key)) return false;
11584
11828
  if (specByKey.get(key)?.mirror) return false;
@@ -11589,7 +11833,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
11589
11833
  if (spec && e.specHash === spec.specHash && e.outputs.length && !postprocessCurrent(e, spec)) {
11590
11834
  return Boolean(opts.force) || e.outputs.every((output) => {
11591
11835
  const file = resolveOutputPath(output.path, spec.root);
11592
- return existsSync9(file) && sha256(readFileSync3(file)) === output.sha256;
11836
+ return existsSync9(file) && sha256(readFileSync4(file)) === output.sha256;
11593
11837
  });
11594
11838
  }
11595
11839
  if (!opts.repair) return false;
@@ -11597,7 +11841,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
11597
11841
  return e.outputs.some((output) => {
11598
11842
  const file = resolveOutputPath(output.path, spec.root);
11599
11843
  if (!existsSync9(file)) return true;
11600
- const current = sha256(readFileSync3(file));
11844
+ const current = sha256(readFileSync4(file));
11601
11845
  if (current === output.sha256) return false;
11602
11846
  return Boolean(opts.force) || (e.supersededOutputs ?? []).some((old) => old.sha256 === current);
11603
11847
  });
@@ -11680,7 +11924,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
11680
11924
  if (!source.url) throw new Error(`no durable source URL to refresh ${source.role ?? "asset"} from`);
11681
11925
  fresh = await provider.download(source.url);
11682
11926
  if (recorded && sha256(fresh) === recordedRaw && !reapply) {
11683
- log2(` current ${path14.relative(process.cwd(), target)}`);
11927
+ log2(` current ${path15.relative(process.cwd(), target)}`);
11684
11928
  outputs.push({ ...recorded, path: portableOutputPath(target, spec.root) });
11685
11929
  continue;
11686
11930
  }
@@ -11704,9 +11948,9 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
11704
11948
  }
11705
11949
  if (reapply && !fresh) {
11706
11950
  if (!recorded.raw) raw = await readFile7(target);
11707
- log2(` reapply ${path14.relative(process.cwd(), target)}`);
11951
+ log2(` reapply ${path15.relative(process.cwd(), target)}`);
11708
11952
  } else {
11709
- log2(` changed ${path14.relative(process.cwd(), target)} (upstream)`);
11953
+ log2(` changed ${path15.relative(process.cwd(), target)} (upstream)`);
11710
11954
  }
11711
11955
  } else {
11712
11956
  const superseded = (entry.supersededOutputs ?? []).find(
@@ -11734,7 +11978,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
11734
11978
  }
11735
11979
  }
11736
11980
  log2(
11737
- ` replace ${path14.relative(process.cwd(), target)}` + (opts.force ? " (--force)" : " (previous tracked generation)")
11981
+ ` replace ${path15.relative(process.cwd(), target)}` + (opts.force ? " (--force)" : " (previous tracked generation)")
11738
11982
  );
11739
11983
  }
11740
11984
  }
@@ -11743,7 +11987,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
11743
11987
  } else if (!raw && recorded && cacheDir) {
11744
11988
  if (!reapply) final = await readCachedMedia(cacheDir, recorded.sha256, expectedMediaType);
11745
11989
  if (!final && recordedRaw) raw = await readCachedMedia(cacheDir, recordedRaw, expectedMediaType);
11746
- if (final || raw) log2(` cached ${path14.relative(process.cwd(), target)}`);
11990
+ if (final || raw) log2(` cached ${path15.relative(process.cwd(), target)}`);
11747
11991
  }
11748
11992
  if (!final && !raw) {
11749
11993
  if (!source.url) {
@@ -11770,11 +12014,11 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
11770
12014
  buf = processed.bytes;
11771
12015
  if (processed.changed) {
11772
12016
  rawHash = sha256(raw);
11773
- log2(` snapped ${path14.relative(process.cwd(), target)} to ${wanted.palette.colors.length} colours`);
12017
+ log2(` snapped ${path15.relative(process.cwd(), target)} to ${wanted.palette.colors.length} colours`);
11774
12018
  }
11775
12019
  }
11776
12020
  if (cacheDir) await cacheMedia(cacheDir, buf, mediaType);
11777
- await mkdir4(path14.dirname(target), { recursive: true });
12021
+ await mkdir4(path15.dirname(target), { recursive: true });
11778
12022
  const tmp = `${target}.pixelkiln.tmp`;
11779
12023
  await writeFile4(tmp, buf);
11780
12024
  outputs.push({
@@ -11788,7 +12032,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
11788
12032
  await saveLock(lockPath, lock);
11789
12033
  await rename4(tmp, target);
11790
12034
  wrote = true;
11791
- log2(` wrote ${path14.relative(process.cwd(), target)}`);
12035
+ log2(` wrote ${path15.relative(process.cwd(), target)}`);
11792
12036
  }
11793
12037
  if (opts.refresh && !wrote) {
11794
12038
  if (rotated) {
@@ -11832,7 +12076,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
11832
12076
  return result;
11833
12077
  }
11834
12078
  async function readCachedMedia(cacheDir, hash, mediaType) {
11835
- const file = path14.join(cacheDir, cacheFileName(hash, mediaType));
12079
+ const file = path15.join(cacheDir, cacheFileName(hash, mediaType));
11836
12080
  if (!existsSync9(file)) return null;
11837
12081
  try {
11838
12082
  const buf = await readFile7(file);
@@ -11846,7 +12090,7 @@ async function readCachedMedia(cacheDir, hash, mediaType) {
11846
12090
  async function cacheMedia(cacheDir, buf, mediaType, knownHash) {
11847
12091
  const hash = knownHash ?? sha256(buf);
11848
12092
  validateMedia(buf, mediaType);
11849
- const file = path14.join(cacheDir, cacheFileName(hash, mediaType));
12093
+ const file = path15.join(cacheDir, cacheFileName(hash, mediaType));
11850
12094
  if (await readCachedMedia(cacheDir, hash, mediaType)) return;
11851
12095
  await mkdir4(cacheDir, { recursive: true });
11852
12096
  const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
@@ -11889,7 +12133,7 @@ async function pushTags(provider, specs, lock, opts = {}) {
11889
12133
  // src/pipeline/adopt.ts
11890
12134
  import { existsSync as existsSync11 } from "fs";
11891
12135
  import { readFile as readFile9, writeFile as writeFile6 } from "fs/promises";
11892
- import path15 from "path";
12136
+ import path16 from "path";
11893
12137
 
11894
12138
  // src/cache.ts
11895
12139
  import { mkdir as mkdir5, readFile as readFile8, rename as rename5, rm as rm5, writeFile as writeFile5 } from "fs/promises";
@@ -11908,10 +12152,10 @@ function isSha256Hash(value) {
11908
12152
  function parseCache(value) {
11909
12153
  return HashCacheSchema.parse(value);
11910
12154
  }
11911
- async function loadCache(path50) {
11912
- if (!existsSync10(path50)) return { version: 1, hashes: {} };
12155
+ async function loadCache(path52) {
12156
+ if (!existsSync10(path52)) return { version: 1, hashes: {} };
11913
12157
  try {
11914
- const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile8(path50, "utf8")));
12158
+ const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile8(path52, "utf8")));
11915
12159
  if (!parsed.success) return { version: 1, hashes: {} };
11916
12160
  return {
11917
12161
  version: 1,
@@ -11923,18 +12167,18 @@ async function loadCache(path50) {
11923
12167
  return { version: 1, hashes: {} };
11924
12168
  }
11925
12169
  }
11926
- async function saveCache(path50, cache) {
12170
+ async function saveCache(path52, cache) {
11927
12171
  const sorted = {};
11928
12172
  for (const key of Object.keys(cache.hashes).sort()) {
11929
12173
  const hash = cache.hashes[key];
11930
12174
  if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
11931
12175
  sorted[key] = hash;
11932
12176
  }
11933
- await mkdir5(pathModule.dirname(pathModule.resolve(path50)), { recursive: true });
11934
- const tmp = `${path50}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
12177
+ await mkdir5(pathModule.dirname(pathModule.resolve(path52)), { recursive: true });
12178
+ const tmp = `${path52}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
11935
12179
  try {
11936
12180
  await writeFile5(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
11937
- await rename5(tmp, path50);
12181
+ await rename5(tmp, path52);
11938
12182
  } finally {
11939
12183
  await rm5(tmp, { force: true });
11940
12184
  }
@@ -12127,7 +12371,7 @@ function formatUnmatchedRemote(objects, limit = 15) {
12127
12371
  // src/pipeline/adopt-characters.ts
12128
12372
  import { existsSync as existsSync12 } from "fs";
12129
12373
  import { mkdir as mkdir6, readFile as readFile10, writeFile as writeFile7 } from "fs/promises";
12130
- import path16 from "path";
12374
+ import path17 from "path";
12131
12375
  async function adoptCharacters(provider, specs, lock, lockPath, opts) {
12132
12376
  const log2 = opts.onProgress ?? (() => {
12133
12377
  });
@@ -12179,11 +12423,11 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
12179
12423
  const hash = sha256(bytes);
12180
12424
  const file = expectedOutputPath(spec, source.role, index, sources.length);
12181
12425
  if (existsSync12(file)) {
12182
- if (sha256(await readFile10(file)) !== hash) result.differing.push(path16.relative(process.cwd(), file));
12426
+ if (sha256(await readFile10(file)) !== hash) result.differing.push(path17.relative(process.cwd(), file));
12183
12427
  } else {
12184
- await mkdir6(path16.dirname(file), { recursive: true });
12428
+ await mkdir6(path17.dirname(file), { recursive: true });
12185
12429
  await writeFile7(file, bytes);
12186
- result.written.push(path16.relative(process.cwd(), file));
12430
+ result.written.push(path17.relative(process.cwd(), file));
12187
12431
  }
12188
12432
  outputs.push({ path: portableOutputPath(file, spec.root), sha256: hash, ...source.role ? { role: source.role } : {} });
12189
12433
  }
@@ -12222,7 +12466,7 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
12222
12466
  } else {
12223
12467
  const south = expectedOutputPath(spec, "south", 0, spec.character.directions);
12224
12468
  if (!existsSync12(south)) {
12225
- result.unmatched.push(`${key}: no remoteId and no ${path16.relative(process.cwd(), south)} to match by`);
12469
+ result.unmatched.push(`${key}: no remoteId and no ${path17.relative(process.cwd(), south)} to match by`);
12226
12470
  return false;
12227
12471
  }
12228
12472
  const localHash = sha256(await readFile10(south));
@@ -12234,7 +12478,7 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
12234
12478
  }
12235
12479
  }
12236
12480
  if (!match) {
12237
- result.unmatched.push(`${key}: no character's south rotation has the bytes of ${path16.relative(process.cwd(), south)}`);
12481
+ result.unmatched.push(`${key}: no character's south rotation has the bytes of ${path17.relative(process.cwd(), south)}`);
12238
12482
  return false;
12239
12483
  }
12240
12484
  }
@@ -12304,7 +12548,7 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
12304
12548
  if (!existsSync12(first)) {
12305
12549
  const byName = candidates.find((a) => a.name === characterAnimationName(spec));
12306
12550
  if (!byName) {
12307
- result.unmatched.push(`${key}: no remoteId and no ${path16.relative(process.cwd(), first)} to match by`);
12551
+ result.unmatched.push(`${key}: no remoteId and no ${path17.relative(process.cwd(), first)} to match by`);
12308
12552
  return false;
12309
12553
  }
12310
12554
  match = byName;
@@ -12317,7 +12561,7 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
12317
12561
  }
12318
12562
  }
12319
12563
  if (!match) {
12320
- result.unmatched.push(`${key}: no ${animation.direction} animation of character ${parentId} starts with the bytes of ${path16.relative(process.cwd(), first)}`);
12564
+ result.unmatched.push(`${key}: no ${animation.direction} animation of character ${parentId} starts with the bytes of ${path17.relative(process.cwd(), first)}`);
12321
12565
  return false;
12322
12566
  }
12323
12567
  }
@@ -12379,7 +12623,7 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
12379
12623
  // src/pipeline/salvage.ts
12380
12624
  import { readFile as readFile11 } from "fs/promises";
12381
12625
  import { existsSync as existsSync13 } from "fs";
12382
- import path17 from "path";
12626
+ import path18 from "path";
12383
12627
  function providerClaimId(provider, objectId) {
12384
12628
  return `${encodeURIComponent(provider)}:${encodeURIComponent(objectId)}`;
12385
12629
  }
@@ -12556,20 +12800,20 @@ function matchStyleByPattern(prompt, manifest) {
12556
12800
  return null;
12557
12801
  }
12558
12802
  async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, claimPaths) {
12559
- const own = path17.resolve(ownManifestPath);
12803
+ const own = path18.resolve(ownManifestPath);
12560
12804
  const siblingManifestPaths = [
12561
12805
  .../* @__PURE__ */ new Set([
12562
12806
  ...workspaceManifestPaths,
12563
- ...claimPaths.map((c) => path17.join(path17.dirname(path17.resolve(c)), "pixelkiln.manifest.json"))
12807
+ ...claimPaths.map((c) => path18.join(path18.dirname(path18.resolve(c)), "pixelkiln.manifest.json"))
12564
12808
  ])
12565
12809
  ];
12566
12810
  const siblings = [];
12567
12811
  for (const siblingManifestPath of siblingManifestPaths) {
12568
- if (path17.resolve(siblingManifestPath) === own) continue;
12812
+ if (path18.resolve(siblingManifestPath) === own) continue;
12569
12813
  if (!existsSync13(siblingManifestPath)) continue;
12570
12814
  try {
12571
12815
  const { manifest } = await loadManifest(siblingManifestPath);
12572
- siblings.push({ label: path17.basename(path17.dirname(siblingManifestPath)), manifest });
12816
+ siblings.push({ label: path18.basename(path18.dirname(siblingManifestPath)), manifest });
12573
12817
  } catch {
12574
12818
  }
12575
12819
  }
@@ -12691,7 +12935,7 @@ async function applyTags(provider, decisions, existing, opts = {}) {
12691
12935
  // src/workspace.ts
12692
12936
  import { mkdir as mkdir7, readFile as readFile12, rename as rename6, rm as rm6, writeFile as writeFile8 } from "fs/promises";
12693
12937
  import { existsSync as existsSync14 } from "fs";
12694
- import path18 from "path";
12938
+ import path19 from "path";
12695
12939
  import { z as z5 } from "zod";
12696
12940
  var WorkspaceProjectSchema = z5.object({
12697
12941
  id: z5.string().min(1),
@@ -12733,7 +12977,7 @@ async function saveWorkspace(workspacePath, ws) {
12733
12977
  version: 1,
12734
12978
  projects: [...ws.projects].sort((a, b) => a.id.localeCompare(b.id))
12735
12979
  };
12736
- await mkdir7(path18.dirname(path18.resolve(workspacePath)), { recursive: true });
12980
+ await mkdir7(path19.dirname(path19.resolve(workspacePath)), { recursive: true });
12737
12981
  const tmp = `${workspacePath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
12738
12982
  try {
12739
12983
  await writeFile8(tmp, JSON.stringify(sorted, null, 2) + "\n");
@@ -12743,12 +12987,12 @@ async function saveWorkspace(workspacePath, ws) {
12743
12987
  }
12744
12988
  }
12745
12989
  function toPortablePath(dir, absolute) {
12746
- return path18.relative(dir, absolute).split(path18.sep).join("/");
12990
+ return path19.relative(dir, absolute).split(path19.sep).join("/");
12747
12991
  }
12748
12992
  function resolveProject(dir, project) {
12749
12993
  return {
12750
- manifestPath: path18.resolve(dir, project.manifest.split("/").join(path18.sep)),
12751
- lockPath: path18.resolve(dir, project.lock.split("/").join(path18.sep))
12994
+ manifestPath: path19.resolve(dir, project.manifest.split("/").join(path19.sep)),
12995
+ lockPath: path19.resolve(dir, project.lock.split("/").join(path19.sep))
12752
12996
  };
12753
12997
  }
12754
12998
  function validateWorkspace(ws, dir) {
@@ -12761,7 +13005,7 @@ function validateWorkspace(ws, dir) {
12761
13005
  const { manifestPath, lockPath } = resolveProject(dir, project);
12762
13006
  lockOwners.set(lockPath, [...lockOwners.get(lockPath) ?? [], project.id]);
12763
13007
  manifestOwners.set(manifestPath, [...manifestOwners.get(manifestPath) ?? [], project.id]);
12764
- if (path18.isAbsolute(project.manifest) || path18.isAbsolute(project.lock)) {
13008
+ if (path19.isAbsolute(project.manifest) || path19.isAbsolute(project.lock)) {
12765
13009
  diagnostics.push({
12766
13010
  id: "absolute-path",
12767
13011
  level: "warning",
@@ -12823,7 +13067,7 @@ function validateWorkspace(ws, dir) {
12823
13067
 
12824
13068
  // src/pick/salvage-server.ts
12825
13069
  import { mkdir as mkdir8, writeFile as writeFile9, readFile as readFile14 } from "fs/promises";
12826
- import path19 from "path";
13070
+ import path20 from "path";
12827
13071
 
12828
13072
  // src/pick/salvage-sheet.ts
12829
13073
  var escapeHtml = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
@@ -13088,7 +13332,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
13088
13332
  });
13089
13333
  const html = renderSalvageSheet(orphans, {
13090
13334
  styleId: ctx.styleId,
13091
- importDir: path19.relative(process.cwd(), ctx.importDir) || "."
13335
+ importDir: path20.relative(process.cwd(), ctx.importDir) || "."
13092
13336
  });
13093
13337
  const byId = new Map(orphans.map((o) => [o.id, o]));
13094
13338
  const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
@@ -13144,9 +13388,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
13144
13388
  if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
13145
13389
  decodePng(buf);
13146
13390
  const assetId = idFromPrompt(orphan.prompt, taken);
13147
- const rel = path19.join("_salvaged", `${assetId}.png`);
13148
- const outFile = path19.resolve(ctx.importDir, rel);
13149
- await mkdir8(path19.dirname(outFile), { recursive: true });
13391
+ const rel = path20.join("_salvaged", `${assetId}.png`);
13392
+ const outFile = path20.resolve(ctx.importDir, rel);
13393
+ await mkdir8(path20.dirname(outFile), { recursive: true });
13150
13394
  await writeFile9(outFile, buf);
13151
13395
  ctx.manifest.assets[assetId] = {
13152
13396
  prompt: orphan.prompt,
@@ -13179,7 +13423,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
13179
13423
  sourceUrl: durableSource,
13180
13424
  sourceUrls: durableSource ? [{ url: durableSource }] : [],
13181
13425
  outputs: [{
13182
- path: portableOutputPath(outFile, path19.dirname(ctx.manifestPath)),
13426
+ path: portableOutputPath(outFile, path20.dirname(ctx.manifestPath)),
13183
13427
  sha256: sha256(buf)
13184
13428
  }],
13185
13429
  submittedAt: orphan.createdAt,
@@ -13214,19 +13458,19 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
13214
13458
  }
13215
13459
 
13216
13460
  // src/cli/project.ts
13217
- import path21 from "path";
13461
+ import path22 from "path";
13218
13462
  import { existsSync as existsSync15 } from "fs";
13219
13463
 
13220
13464
  // src/project.ts
13221
- import path20 from "path";
13465
+ import path21 from "path";
13222
13466
  async function openProject(manifestPath = "pixelkiln.manifest.json", opts = {}) {
13223
- const absolute = path20.resolve(manifestPath);
13224
- const root = path20.dirname(absolute);
13467
+ const absolute = path21.resolve(manifestPath);
13468
+ const root = path21.dirname(absolute);
13225
13469
  if (opts.env !== false) {
13226
13470
  loadEnvFiles(root);
13227
- if (path20.resolve(process.cwd()) !== root) loadEnvFiles(process.cwd());
13471
+ if (path21.resolve(process.cwd()) !== root) loadEnvFiles(process.cwd());
13228
13472
  }
13229
- const lockPath = path20.resolve(opts.lockPath ?? path20.join(root, "pixelkiln.lock.json"));
13473
+ const lockPath = path21.resolve(opts.lockPath ?? path21.join(root, "pixelkiln.lock.json"));
13230
13474
  const loaded = await loadManifest(absolute);
13231
13475
  const specs = await resolveSpecs(loaded, { styles: opts.styles ?? [], assets: opts.assets ?? [] });
13232
13476
  const lock = await loadLock(lockPath);
@@ -13357,7 +13601,7 @@ async function workspaceStatus(ws, dir) {
13357
13601
 
13358
13602
  // src/cli/project.ts
13359
13603
  async function provenanceFile(id, file) {
13360
- const absolute = path21.resolve(file);
13604
+ const absolute = path22.resolve(file);
13361
13605
  return {
13362
13606
  id,
13363
13607
  path: absolute,
@@ -13436,7 +13680,7 @@ async function requireCompleteWorkspaceClaims(workspacePath) {
13436
13680
  if (!existsSync15(workspacePath)) {
13437
13681
  throw new ProjectError(`Workspace catalog not found: ${workspacePath}`);
13438
13682
  }
13439
- const dir = path21.dirname(path21.resolve(workspacePath));
13683
+ const dir = path22.dirname(path22.resolve(workspacePath));
13440
13684
  const ws = await loadWorkspace(workspacePath);
13441
13685
  const diagnostics = validateWorkspace(ws, dir);
13442
13686
  const errors = diagnostics.filter((d) => d.level === "error");
@@ -13459,9 +13703,9 @@ async function openAccountProject(args) {
13459
13703
  return project;
13460
13704
  }
13461
13705
  async function open(args) {
13462
- if (!existsSync15(path21.resolve(args.manifest))) {
13706
+ if (!existsSync15(path22.resolve(args.manifest))) {
13463
13707
  throw new ProjectError(
13464
- `No manifest at ${path21.resolve(args.manifest)}. Pass --manifest, or run \`pixelkiln init --from <dir>\`.`
13708
+ `No manifest at ${path22.resolve(args.manifest)}. Pass --manifest, or run \`pixelkiln init --from <dir>\`.`
13465
13709
  );
13466
13710
  }
13467
13711
  const project = await openProject(args.manifest, { lockPath: args.lock, styles: args.styles, assets: args.assets });
@@ -13503,7 +13747,7 @@ function providerCache(mode2) {
13503
13747
 
13504
13748
  // src/cli/commands/account.ts
13505
13749
  async function runBalance(args) {
13506
- loadEnvFiles(path22.dirname(path22.resolve(args.manifest)));
13750
+ loadEnvFiles(path23.dirname(path23.resolve(args.manifest)));
13507
13751
  loadEnvFiles(process.cwd());
13508
13752
  const loaded = await loadManifest(args.manifest);
13509
13753
  const selectedProvider = accountProviderId(loaded.manifest, args.provider, "balance");
@@ -13576,12 +13820,12 @@ async function runAdopt(args) {
13576
13820
  tagged ${n} object(s) upstream`);
13577
13821
  }
13578
13822
  if (args.writePrompts) {
13579
- const { filled, stillEmpty } = await writePromptsBack(path22.resolve(args.manifest), lock, {
13823
+ const { filled, stillEmpty } = await writePromptsBack(path23.resolve(args.manifest), lock, {
13580
13824
  onProgress: log,
13581
13825
  provider: provider.id,
13582
13826
  assetIds: specs.map((spec) => spec.assetId)
13583
13827
  });
13584
- log(` recovered ${filled} prompt(s) into ${path22.relative(process.cwd(), args.manifest)}`);
13828
+ log(` recovered ${filled} prompt(s) into ${path23.relative(process.cwd(), args.manifest)}`);
13585
13829
  const reloaded = await loadManifest(args.manifest);
13586
13830
  const rebased = (await resolveSpecs(reloaded, {
13587
13831
  styles: args.styles,
@@ -13611,12 +13855,12 @@ async function runSalvage2(args) {
13611
13855
  const provider = providerFor(accountProvider);
13612
13856
  const jsonMode = args.dryRun && args.json;
13613
13857
  const diag = jsonMode ? (msg = "") => console.error(msg) : log;
13614
- const ownLock = path22.resolve(args.lock);
13858
+ const ownLock = path23.resolve(args.lock);
13615
13859
  let workspaceProjects = [];
13616
13860
  let workspaceDir = "";
13617
13861
  if (args.workspace) {
13618
- const workspacePath = path22.resolve(args.workspace);
13619
- workspaceDir = path22.dirname(workspacePath);
13862
+ const workspacePath = path23.resolve(args.workspace);
13863
+ workspaceDir = path23.dirname(workspacePath);
13620
13864
  const complete = await requireCompleteWorkspaceClaims(workspacePath);
13621
13865
  workspaceProjects = complete.ws.projects;
13622
13866
  for (const d of complete.diagnostics) diag(` WARN ${d.id}: ${d.message}`);
@@ -13626,11 +13870,11 @@ async function runSalvage2(args) {
13626
13870
  .../* @__PURE__ */ new Set([
13627
13871
  ...workspaceLockPaths,
13628
13872
  ...existsSync16(ownLock) ? [ownLock] : [],
13629
- ...args.claims.map((c) => path22.resolve(c))
13873
+ ...args.claims.map((c) => path23.resolve(c))
13630
13874
  ])
13631
13875
  ];
13632
13876
  diag(` claim set (${lockPaths.length} lockfile(s)):`);
13633
- for (const p of lockPaths) diag(` ${path22.relative(process.cwd(), p)}`);
13877
+ for (const p of lockPaths) diag(` ${path23.relative(process.cwd(), p)}`);
13634
13878
  if (!args.claims.length && !args.workspace) {
13635
13879
  diag(
13636
13880
  `
@@ -13638,7 +13882,7 @@ async function runSalvage2(args) {
13638
13882
  pass every other project's lockfile via --claims a.json,b.json, or
13639
13883
  register every project in a workspace catalog and pass --workspace.`
13640
13884
  );
13641
- } else if (args.workspace && !workspaceProjects.some((p) => resolveProject(workspaceDir, p).manifestPath === path22.resolve(args.manifest))) {
13885
+ } else if (args.workspace && !workspaceProjects.some((p) => resolveProject(workspaceDir, p).manifestPath === path23.resolve(args.manifest))) {
13642
13886
  diag(
13643
13887
  `
13644
13888
  This project's manifest is not registered in the workspace catalog. Its own
@@ -13757,7 +14001,7 @@ async function runSalvage2(args) {
13757
14001
  manifestPath: loaded.path,
13758
14002
  manifest: accountManifest,
13759
14003
  styleId,
13760
- importDir: path22.resolve(loaded.root, style.outDir),
14004
+ importDir: path23.resolve(loaded.root, style.outDir),
13761
14005
  lock,
13762
14006
  lockPath: args.lock
13763
14007
  },
@@ -13884,19 +14128,20 @@ async function runTag(args) {
13884
14128
  }
13885
14129
 
13886
14130
  // src/cli/commands/edit.ts
13887
- import path25 from "path";
14131
+ import path26 from "path";
13888
14132
 
13889
14133
  // src/pipeline/hand-edit.ts
13890
14134
  import { existsSync as existsSync17 } from "fs";
13891
14135
  import { copyFile, mkdir as mkdir9, readFile as readFile16, rename as rename8, rm as rm8, writeFile as writeFile11 } from "fs/promises";
13892
- import path24 from "path";
14136
+ import path25 from "path";
13893
14137
  import { z as z7 } from "zod";
13894
14138
 
13895
14139
  // src/manifest-edit.ts
13896
14140
  import { readFile as readFile15, rename as rename7, rm as rm7, writeFile as writeFile10 } from "fs/promises";
13897
- import path23 from "path";
14141
+ import path24 from "path";
13898
14142
  import { z as z6 } from "zod";
13899
14143
  var HexSha = z6.string().regex(/^[0-9a-f]{64}$/);
14144
+ var ProjectFileSchema = z6.string().min(1).refine((file) => !path24.isAbsolute(file) && !file.split(/[\\/]/).includes(".."), "must be a path inside the project");
13900
14145
  var AssetPatchSchema = z6.object({
13901
14146
  prompt: z6.string().optional(),
13902
14147
  /** Per-style override for the style being viewed; null removes it. */
@@ -13945,10 +14190,11 @@ var NewAssetSchema = z6.object({
13945
14190
  styles: z6.array(z6.string().min(1)).optional(),
13946
14191
  /**
13947
14192
  * 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).
14193
+ * instead of a fresh generation. Every mode but `outpaint` can be
14194
+ * created here: `inpaint` names a mask file the gallery's mask editor
14195
+ * wrote first, `interpolate` names an existing image as its ending
14196
+ * keyframe, and `animate-skeleton` names the keypoints file the gallery
14197
+ * wrote first.
13952
14198
  */
13953
14199
  revision: z6.discriminatedUnion("mode", [
13954
14200
  z6.object({
@@ -13965,15 +14211,38 @@ var NewAssetSchema = z6.object({
13965
14211
  dithering: RevisionDitheringSchema.optional(),
13966
14212
  ditheringStrength: z6.number().min(0).max(10).optional()
13967
14213
  }).strict(),
14214
+ z6.object({
14215
+ mode: z6.literal("inpaint"),
14216
+ from: z6.string().min(1),
14217
+ /** Black-and-white PNG the same size as the parent; white is repainted. */
14218
+ mask: ProjectFileSchema
14219
+ }).strict(),
14220
+ z6.object({
14221
+ mode: z6.literal("interpolate"),
14222
+ from: z6.string().min(1),
14223
+ /** The ending keyframe: an image already in the project, the parent's size. */
14224
+ lastFrame: ProjectFileSchema,
14225
+ fps: z6.number().int().min(1).max(60).optional()
14226
+ }).strict(),
13968
14227
  z6.object({ mode: z6.literal("correct-pixelart"), from: z6.string().min(1), strength: z6.number().min(0).max(1).optional() }).strict(),
14228
+ z6.object({
14229
+ mode: z6.literal("remove-background"),
14230
+ from: z6.string().min(1),
14231
+ removalTask: z6.enum(["simple", "complex"]).optional(),
14232
+ /** A short description of the foreground, sent as PixelLab's text hint. */
14233
+ description: z6.string().min(1).optional()
14234
+ }).strict(),
13969
14235
  z6.object({
13970
14236
  mode: z6.enum(["animate", "animate-pixminimax"]),
13971
14237
  from: z6.string().min(1),
13972
14238
  frames: z6.number().int().min(4).max(40).refine((n) => n % 2 === 0, "frames must be even").optional(),
13973
14239
  fps: z6.number().int().min(1).max(60).optional(),
13974
14240
  enhancePrompt: z6.boolean().optional(),
13975
- /** `animate-pixminimax` only; the loader refuses it on `animate`. */
13976
- direction: CharacterDirectionSchema.optional()
14241
+ driftThreshold: z6.number().min(0).optional(),
14242
+ /** `animate-pixminimax` only; the loader refuses these on `animate`. */
14243
+ direction: CharacterDirectionSchema.optional(),
14244
+ subjectDescription: z6.string().min(1).max(300).optional(),
14245
+ initialPose: z6.string().min(1).max(300).optional()
13977
14246
  }).strict(),
13978
14247
  z6.object({
13979
14248
  mode: z6.literal("edit-animation"),
@@ -14291,7 +14560,7 @@ function applyEdit(raw, edit) {
14291
14560
  }
14292
14561
  }
14293
14562
  async function withCandidate(manifestPath, edit, use, unchanged) {
14294
- const absolute = path23.resolve(manifestPath);
14563
+ const absolute = path24.resolve(manifestPath);
14295
14564
  const original = await readFile15(absolute, "utf8");
14296
14565
  if (sha256(original) !== edit.expectedSha256) throw new ManifestDriftError(absolute);
14297
14566
  let raw;
@@ -14305,9 +14574,9 @@ async function withCandidate(manifestPath, edit, use, unchanged) {
14305
14574
  applyEdit(raw, edit);
14306
14575
  const next = serializeLike(original, raw);
14307
14576
  if (next === original) return unchanged(absolute, original);
14308
- const tmp = path23.join(
14309
- path23.dirname(absolute),
14310
- `.${path23.basename(absolute)}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`
14577
+ const tmp = path24.join(
14578
+ path24.dirname(absolute),
14579
+ `.${path24.basename(absolute)}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`
14311
14580
  );
14312
14581
  try {
14313
14582
  await writeFile10(tmp, next);
@@ -14380,11 +14649,11 @@ async function priceManifestEdit(manifestPath, edit, opts = {}) {
14380
14649
  var HAND_EDIT_DIR = "edits";
14381
14650
  function handEditPath(loaded, spec) {
14382
14651
  const style = loaded.manifest.styles[spec.styleId];
14383
- const outDir = path24.resolve(loaded.root, style?.outDir ?? ".");
14384
- const relative = path24.relative(outDir, spec.outFile);
14385
- const inside = relative && !relative.startsWith("..") && !path24.isAbsolute(relative);
14386
- const file = inside ? relative : path24.basename(spec.outFile);
14387
- return path24.join(outDir, HAND_EDIT_DIR, file.replace(/\.gif$/i, ".png"));
14652
+ const outDir = path25.resolve(loaded.root, style?.outDir ?? ".");
14653
+ const relative = path25.relative(outDir, spec.outFile);
14654
+ const inside = relative && !relative.startsWith("..") && !path25.isAbsolute(relative);
14655
+ const file = inside ? relative : path25.basename(spec.outFile);
14656
+ return path25.join(outDir, HAND_EDIT_DIR, file.replace(/\.gif$/i, ".png"));
14388
14657
  }
14389
14658
  function handEditBases(spec, lock) {
14390
14659
  const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
@@ -14426,7 +14695,7 @@ async function startHandEdit(loaded, lock, spec, opts = {}) {
14426
14695
  `${spec.styleId}/${spec.assetId} declares ${spec.source}, one committed file placed for the whole set; clear that source (pixelkiln edit detach) to hand-edit the members one by one`
14427
14696
  );
14428
14697
  }
14429
- const existing = spec.source ? path24.resolve(loaded.root, spec.source) : null;
14698
+ const existing = spec.source ? path25.resolve(loaded.root, spec.source) : null;
14430
14699
  const editPath = existing ?? handEditPath(loaded, spec);
14431
14700
  const members = handEditMembers(loaded, spec, lock, editPath);
14432
14701
  let created = false;
@@ -14437,7 +14706,7 @@ async function startHandEdit(loaded, lock, spec, opts = {}) {
14437
14706
  `${spec.styleId}/${spec.assetId} has no generated PNG on disk to start from; generate or restore it first`
14438
14707
  );
14439
14708
  }
14440
- await mkdir9(path24.dirname(member.path), { recursive: true });
14709
+ await mkdir9(path25.dirname(member.path), { recursive: true });
14441
14710
  await copyFile(member.base, member.path);
14442
14711
  created = true;
14443
14712
  }
@@ -14575,7 +14844,7 @@ async function saveHandEdit(loaded, lock, spec, input) {
14575
14844
  editor: input.editor,
14576
14845
  protocol: input.protocol,
14577
14846
  savedAt: (input.now ?? /* @__PURE__ */ new Date()).toISOString(),
14578
- project: projectPath ? path24.basename(projectPath) : null,
14847
+ project: projectPath ? path25.basename(projectPath) : null,
14579
14848
  outputs: incoming.map((frame, index) => ({
14580
14849
  role: frame.role,
14581
14850
  basedOn: basedOnHash(entry, index),
@@ -14635,7 +14904,7 @@ async function runEdit(args) {
14635
14904
  const set = started.members.length > 1;
14636
14905
  const kind = spec.generator === "frames" ? "frames" : "members";
14637
14906
  log(` ${started.created ? "created" : "found"} ${started.source}${set ? ` (${started.members.length} ${kind})` : ""}${started.declared ? " and declared it as the asset's source" : ""}`);
14638
- if (set) for (const member of started.members) log(` ${member.role ?? ""} ${path25.relative(process.cwd(), member.path)}`);
14907
+ if (set) for (const member of started.members) log(` ${member.role ?? ""} ${path26.relative(process.cwd(), member.path)}`);
14639
14908
  if (args.noOpen) {
14640
14909
  log(` edit ${set ? "them" : "it"} with your own tool, then run pixelkiln plan; mount and pack place ${set ? "them" : "it"} in place of the generated art`);
14641
14910
  } else {
@@ -14645,24 +14914,24 @@ async function runEdit(args) {
14645
14914
  }
14646
14915
 
14647
14916
  // src/cli/commands/gallery.ts
14648
- import path34 from "path";
14649
- import { existsSync as existsSync26 } from "fs";
14917
+ import path36 from "path";
14918
+ import { existsSync as existsSync27 } from "fs";
14650
14919
 
14651
14920
  // src/gallery/snapshot.ts
14652
- 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";
14921
+ import { createHash as createHash4 } from "crypto";
14922
+ import { existsSync as existsSync22 } from "fs";
14923
+ import { readFile as readFile20, stat as stat2 } from "fs/promises";
14924
+ import path32 from "path";
14656
14925
 
14657
14926
  // 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";
14927
+ import { readFile as readFile19, rename as rename9, writeFile as writeFile15 } from "fs/promises";
14928
+ import path30 from "path";
14929
+ import { z as z11 } from "zod";
14661
14930
 
14662
14931
  // src/gallery/upload.ts
14663
14932
  import { existsSync as existsSync18 } from "fs";
14664
14933
  import { mkdir as mkdir10, writeFile as writeFile12 } from "fs/promises";
14665
- import path26 from "path";
14934
+ import path27 from "path";
14666
14935
  import { z as z8 } from "zod";
14667
14936
  var MAX_UPLOAD_BYTES = 8 * 1024 * 1024;
14668
14937
  var UploadImageSchema = z8.object({
@@ -14675,12 +14944,12 @@ var UploadImageSchema = z8.object({
14675
14944
  overwrite: z8.boolean().optional()
14676
14945
  }).strict();
14677
14946
  async function saveProjectImage(root, request) {
14678
- const file = path26.resolve(root, request.path);
14679
- const relative = path26.relative(root, file);
14680
- if (relative.startsWith("..") || path26.isAbsolute(relative)) {
14947
+ const file = path27.resolve(root, request.path);
14948
+ const relative = path27.relative(root, file);
14949
+ if (relative.startsWith("..") || path27.isAbsolute(relative)) {
14681
14950
  throw new ManifestEditError("an upload must stay inside the project");
14682
14951
  }
14683
- const extension = path26.extname(file).toLowerCase();
14952
+ const extension = path27.extname(file).toLowerCase();
14684
14953
  const expected = extension === ".png" ? "png" : extension === ".jpg" || extension === ".jpeg" ? "jpeg" : null;
14685
14954
  if (!expected) throw new ManifestEditError("an upload must be a .png, .jpg, or .jpeg path");
14686
14955
  const bytes = Buffer.from(request.base64, "base64");
@@ -14693,57 +14962,113 @@ async function saveProjectImage(root, request) {
14693
14962
  throw new ManifestEditError(`the upload is a ${metadata.format.toUpperCase()}; name it ${metadata.format === "png" ? ".png" : ".jpg"}`);
14694
14963
  }
14695
14964
  if (existsSync18(file) && !request.overwrite) {
14696
- throw new ManifestEditError(`${relative.split(path26.sep).join("/")} already exists; choose another name, or replace it`);
14965
+ throw new ManifestEditError(`${relative.split(path27.sep).join("/")} already exists; choose another name, or replace it`);
14697
14966
  }
14698
- await mkdir10(path26.dirname(file), { recursive: true });
14967
+ await mkdir10(path27.dirname(file), { recursive: true });
14699
14968
  await writeFile12(file, bytes);
14700
- return relative.split(path26.sep).join("/");
14969
+ return relative.split(path27.sep).join("/");
14701
14970
  }
14702
14971
 
14703
- // src/gallery/skeleton.ts
14972
+ // src/gallery/image-revision.ts
14704
14973
  import { existsSync as existsSync19 } from "fs";
14705
- import { mkdir as mkdir11, readFile as readFile17, rm as rm9, writeFile as writeFile13 } from "fs/promises";
14706
- import path27 from "path";
14974
+ import { readFile as readFile17, rm as rm9, writeFile as writeFile13 } from "fs/promises";
14975
+ import path28 from "path";
14707
14976
  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"),
14977
+ var InpaintRevisionRequestSchema = z9.object({
14978
+ action: z9.literal("create-inpaint-revision"),
14716
14979
  project: z9.string().min(1).optional(),
14717
14980
  expectedSha256: z9.string().regex(/^[0-9a-f]{64}$/),
14718
14981
  styleId: z9.string().min(1),
14719
- /** The sprite the animation poses. */
14982
+ /** The sprite being repainted. */
14720
14983
  from: z9.string().min(1),
14721
14984
  assetId: z9.string().min(1).regex(/^[^/\\]+$/, "asset ids cannot contain slashes"),
14722
- /** The motion, sent as PixelLab's `action`. */
14985
+ /** What to paint into the masked area. */
14723
14986
  prompt: z9.string().min(1),
14987
+ /** Manifest-relative `.png` destination for the mask. */
14988
+ maskPath: z9.string().min(1),
14989
+ maskBase64: z9.string().regex(/^[A-Za-z0-9+/]*={0,2}$/),
14990
+ /** Replace a file already at `maskPath`. */
14991
+ overwrite: z9.boolean().optional()
14992
+ }).strict();
14993
+ async function createInpaintRevision(manifestPath, request) {
14994
+ const root = path28.dirname(path28.resolve(manifestPath));
14995
+ if (path28.extname(request.maskPath).toLowerCase() !== ".png") {
14996
+ throw new ManifestEditError("the mask must be a .png path inside the project");
14997
+ }
14998
+ const bytes = Buffer.from(request.maskBase64, "base64");
14999
+ const metadata = imageMetadata(bytes);
15000
+ if (!metadata || metadata.format !== "png") throw new ManifestEditError("the mask is not a readable PNG");
15001
+ const file = path28.resolve(root, request.maskPath);
15002
+ const previous = existsSync19(file) ? await readFile17(file) : null;
15003
+ const mask = await saveProjectImage(root, {
15004
+ action: "upload-image",
15005
+ path: request.maskPath,
15006
+ base64: request.maskBase64,
15007
+ ...request.overwrite ? { overwrite: true } : {}
15008
+ });
15009
+ try {
15010
+ await applyManifestEdit(manifestPath, {
15011
+ action: "add-asset",
15012
+ assetId: request.assetId,
15013
+ expectedSha256: request.expectedSha256,
15014
+ asset: {
15015
+ prompt: request.prompt,
15016
+ styles: [request.styleId],
15017
+ revision: { mode: "inpaint", from: request.from, mask }
15018
+ }
15019
+ });
15020
+ } catch (error) {
15021
+ if (previous) await writeFile13(file, previous);
15022
+ else await rm9(file, { force: true });
15023
+ throw error;
15024
+ }
15025
+ return mask;
15026
+ }
15027
+
15028
+ // src/gallery/skeleton.ts
15029
+ import { existsSync as existsSync20 } from "fs";
15030
+ import { mkdir as mkdir11, readFile as readFile18, rm as rm10, writeFile as writeFile14 } from "fs/promises";
15031
+ import path29 from "path";
15032
+ import { z as z10 } from "zod";
15033
+ var SkeletonEstimateRequestSchema = z10.object({
15034
+ project: z10.string().min(1).optional(),
15035
+ styleId: z10.string().min(1),
15036
+ assetId: z10.string().min(1),
15037
+ frames: z10.number().int().min(3).max(15).optional()
15038
+ }).strict();
15039
+ var SkeletonAnimationRequestSchema = z10.object({
15040
+ action: z10.literal("create-skeleton-animation"),
15041
+ project: z10.string().min(1).optional(),
15042
+ expectedSha256: z10.string().regex(/^[0-9a-f]{64}$/),
15043
+ styleId: z10.string().min(1),
15044
+ /** The sprite the animation poses. */
15045
+ from: z10.string().min(1),
15046
+ assetId: z10.string().min(1).regex(/^[^/\\]+$/, "asset ids cannot contain slashes"),
15047
+ /** The motion, sent as PixelLab's `action`. */
15048
+ prompt: z10.string().min(1),
14724
15049
  direction: CharacterDirectionSchema,
14725
- description: z9.string().min(1).optional(),
14726
- skeletonTemplate: z9.string().min(1).optional(),
15050
+ description: z10.string().min(1).optional(),
15051
+ skeletonTemplate: z10.string().min(1).optional(),
14727
15052
  /** Manifest-relative JSON path inside the project. */
14728
- keypointsFile: z9.string().min(1),
15053
+ keypointsFile: z10.string().min(1),
14729
15054
  /** Poses to write to `keypointsFile`; omitted to use a file that already exists. */
14730
15055
  set: SkeletonSetSchema.optional(),
14731
15056
  /** Replace an existing `keypointsFile` with `set`. */
14732
- overwrite: z9.boolean().optional()
15057
+ overwrite: z10.boolean().optional()
14733
15058
  }).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),
15059
+ var SkeletonKeypointsUpdateSchema = z10.object({
15060
+ action: z10.literal("update-skeleton-keypoints"),
15061
+ project: z10.string().min(1).optional(),
15062
+ styleId: z10.string().min(1),
15063
+ assetId: z10.string().min(1),
14739
15064
  /** 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}$/),
15065
+ expectedSha256: z10.string().regex(/^[0-9a-f]{64}$/),
14741
15066
  set: SkeletonSetSchema
14742
15067
  }).strict();
14743
15068
  var DEFAULT_ESTIMATE_LIMIT = 10;
14744
15069
  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;
15070
+ if (spec.source) return path29.resolve(spec.root, spec.source);
15071
+ if (spec.quality && existsSync20(spec.quality.outFile)) return spec.quality.outFile;
14747
15072
  return spec.outFile;
14748
15073
  }
14749
15074
  function createGallerySkeletonHandlers(opts) {
@@ -14764,10 +15089,10 @@ function createGallerySkeletonHandlers(opts) {
14764
15089
  const spec = ctx.specs.find((candidate) => lockKey(candidate.styleId, candidate.assetId) === key);
14765
15090
  if (!spec) throw new ManifestEditError(`"${key}" is not declared by the manifest`);
14766
15091
  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);
15092
+ if (!existsSync20(file)) throw new ManifestEditError(`${key} has no image on disk to estimate from`);
15093
+ const bytes = await readFile18(file);
14769
15094
  const metadata = imageMetadata(bytes);
14770
- if (!metadata) throw new ManifestEditError(`${path27.basename(file)} is not a readable PNG or JPEG`);
15095
+ if (!metadata) throw new ManifestEditError(`${path29.basename(file)} is not a readable PNG or JPEG`);
14771
15096
  const problem = estimateSkeletonSizeProblem(metadata.width, metadata.height);
14772
15097
  if (problem) throw new ManifestEditError(`${key} is ${problem}`);
14773
15098
  if (used >= limit) {
@@ -14784,7 +15109,7 @@ function createGallerySkeletonHandlers(opts) {
14784
15109
  log2(` estimated a skeleton for ${key} (PixelLab estimate-skeleton, ${used}/${limit} this session${billed ? `, billed ${billed.amount} ${billed.unit}` : ""})`);
14785
15110
  return {
14786
15111
  set: scaffoldSkeletonSet(res.keypoints, request.frames ?? DEFAULT_SCAFFOLD_FRAMES),
14787
- source: path27.relative(ctx.loaded.root, file).split(path27.sep).join("/"),
15112
+ source: path29.relative(ctx.loaded.root, file).split(path29.sep).join("/"),
14788
15113
  session: status()
14789
15114
  };
14790
15115
  }
@@ -14798,21 +15123,21 @@ function billedAmount(usage) {
14798
15123
  return typeof amount === "number" && Number.isFinite(amount) ? { unit: type, amount } : null;
14799
15124
  }
14800
15125
  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)) {
15126
+ const root = path29.dirname(path29.resolve(manifestPath));
15127
+ const file = path29.resolve(root, request.keypointsFile);
15128
+ const relative = path29.relative(root, file);
15129
+ if (!request.keypointsFile.endsWith(".json") || relative.startsWith("..") || path29.isAbsolute(relative)) {
14805
15130
  throw new ManifestEditError("the keypoints file must be a .json path inside the project");
14806
15131
  }
14807
- const existed = existsSync19(file);
15132
+ const existed = existsSync20(file);
14808
15133
  let previous = null;
14809
15134
  if (request.set) {
14810
15135
  if (existed && !request.overwrite) {
14811
15136
  throw new ManifestEditError(`${request.keypointsFile} already exists; choose another path, or replace it`);
14812
15137
  }
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");
15138
+ if (existed) previous = await readFile18(file);
15139
+ await mkdir11(path29.dirname(file), { recursive: true });
15140
+ await writeFile14(file, JSON.stringify(request.set, null, 2) + "\n");
14816
15141
  } else if (!existed) {
14817
15142
  throw new ManifestEditError(`${request.keypointsFile} does not exist; paste poses or estimate them from the sprite`);
14818
15143
  }
@@ -14827,7 +15152,7 @@ async function createSkeletonAnimation(manifestPath, request) {
14827
15152
  revision: {
14828
15153
  mode: "animate-skeleton",
14829
15154
  from: request.from,
14830
- keypointsFile: relative.split(path27.sep).join("/"),
15155
+ keypointsFile: relative.split(path29.sep).join("/"),
14831
15156
  direction: request.direction,
14832
15157
  ...request.description ? { description: request.description } : {},
14833
15158
  ...request.skeletonTemplate ? { skeletonTemplate: request.skeletonTemplate } : {}
@@ -14836,8 +15161,8 @@ async function createSkeletonAnimation(manifestPath, request) {
14836
15161
  });
14837
15162
  } catch (error) {
14838
15163
  if (request.set) {
14839
- if (previous) await writeFile13(file, previous);
14840
- else await rm9(file, { force: true });
15164
+ if (previous) await writeFile14(file, previous);
15165
+ else await rm10(file, { force: true });
14841
15166
  }
14842
15167
  throw error;
14843
15168
  }
@@ -14848,64 +15173,64 @@ async function updateSkeletonKeypoints(ctx, request) {
14848
15173
  if (!spec) throw new ManifestEditError(`"${key}" is not declared by the manifest`);
14849
15174
  const file = spec.revision?.mode === "animate-skeleton" ? spec.revision.keypointsFile : void 0;
14850
15175
  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)) {
15176
+ const relative = path29.relative(ctx.loaded.root, file);
15177
+ if (relative.startsWith("..") || path29.isAbsolute(relative)) {
14853
15178
  throw new ManifestEditError(`${key}'s keypoints file is outside the project; edit it by hand`);
14854
15179
  }
14855
- const current = existsSync19(file) ? sha256(await readFile17(file)) : null;
15180
+ const current = existsSync20(file) ? sha256(await readFile18(file)) : null;
14856
15181
  if (current !== request.expectedSha256) {
14857
15182
  throw Object.assign(
14858
- new Error(`${relative.split(path27.sep).join("/")} changed on disk since this page loaded it; press Refresh and edit again`),
15183
+ new Error(`${relative.split(path29.sep).join("/")} changed on disk since this page loaded it; press Refresh and edit again`),
14859
15184
  { status: 409 }
14860
15185
  );
14861
15186
  }
14862
- await writeFile13(file, JSON.stringify(request.set, null, 2) + "\n");
14863
- return relative.split(path27.sep).join("/");
15187
+ await writeFile14(file, JSON.stringify(request.set, null, 2) + "\n");
15188
+ return relative.split(path29.sep).join("/");
14864
15189
  }
14865
15190
 
14866
15191
  // 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}$/),
15192
+ var Base64 = z11.string().regex(/^[A-Za-z0-9+/]*={0,2}$/).max(Math.ceil(MAX_HAND_EDIT_BYTES / 3) * 4);
15193
+ var HandEditRequestSchema = z11.discriminatedUnion("action", [
15194
+ z11.object({
15195
+ action: z11.literal("start-edit"),
15196
+ project: z11.string().min(1).optional(),
15197
+ assetId: z11.string().min(1),
15198
+ styleId: z11.string().min(1),
15199
+ expectedSha256: z11.string().regex(/^[0-9a-f]{64}$/),
14875
15200
  /** Also launch the author's editor on the file. */
14876
- open: z10.boolean().optional()
15201
+ open: z11.boolean().optional()
14877
15202
  }).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}$/)
15203
+ z11.object({
15204
+ action: z11.literal("detach-edit"),
15205
+ project: z11.string().min(1).optional(),
15206
+ assetId: z11.string().min(1),
15207
+ styleId: z11.string().min(1),
15208
+ expectedSha256: z11.string().regex(/^[0-9a-f]{64}$/)
14884
15209
  }).strict(),
14885
15210
  /** 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}$/),
15211
+ z11.object({
15212
+ action: z11.literal("save-edit"),
15213
+ project: z11.string().min(1).optional(),
15214
+ assetId: z11.string().min(1),
15215
+ styleId: z11.string().min(1),
15216
+ expectedSha256: z11.string().regex(/^[0-9a-f]{64}$/),
14892
15217
  /** The flattened image; for a frame set, `frames` instead. */
14893
15218
  png: Base64.min(1).optional(),
14894
15219
  /** 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(),
15220
+ frames: z11.array(z11.object({ role: z11.string().min(1).nullable(), png: Base64.min(1) }).strict()).min(1).max(256).optional(),
14896
15221
  /** The editor's layered project file, kept beside the edit. */
14897
15222
  pxo: Base64.optional(),
14898
15223
  /** Editor identity from the bridge's ready message. */
14899
- editor: z10.string().min(1).max(200),
14900
- protocol: z10.number().int().positive()
15224
+ editor: z11.string().min(1).max(200),
15225
+ protocol: z11.number().int().positive()
14901
15226
  }).strict()
14902
15227
  ]);
14903
15228
  var UNDO_DEPTH = 20;
14904
- var UndoRequestSchema = z10.object({
14905
- action: z10.literal("undo-edit"),
14906
- project: z10.string().min(1).optional(),
15229
+ var UndoRequestSchema = z11.object({
15230
+ action: z11.literal("undo-edit"),
15231
+ project: z11.string().min(1).optional(),
14907
15232
  /** 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}$/)
15233
+ expectedSha256: z11.string().regex(/^[0-9a-f]{64}$/)
14909
15234
  }).strict();
14910
15235
  function createGalleryEditHandler(opts) {
14911
15236
  const log2 = opts.onProgress ?? (() => {
@@ -14914,7 +15239,7 @@ function createGalleryEditHandler(opts) {
14914
15239
  const history = /* @__PURE__ */ new Map();
14915
15240
  const manifestOf = async (project) => {
14916
15241
  try {
14917
- return path28.resolve(await opts.manifestFor(typeof project === "string" ? project : void 0));
15242
+ return path30.resolve(await opts.manifestFor(typeof project === "string" ? project : void 0));
14918
15243
  } catch {
14919
15244
  return null;
14920
15245
  }
@@ -14924,7 +15249,7 @@ function createGalleryEditHandler(opts) {
14924
15249
  if (undo.success) {
14925
15250
  const manifestPath2 = await manifestOf(undo.data.project);
14926
15251
  if (!manifestPath2) throw new ManifestEditError("no manifest to undo an edit in");
14927
- const current = await readFile18(manifestPath2, "utf8");
15252
+ const current = await readFile19(manifestPath2, "utf8");
14928
15253
  if (sha256(current) !== undo.data.expectedSha256) throw new ManifestDriftError(manifestPath2);
14929
15254
  const stack = history.get(manifestPath2) ?? [];
14930
15255
  const last = stack[stack.length - 1];
@@ -14934,17 +15259,17 @@ function createGalleryEditHandler(opts) {
14934
15259
  throw Object.assign(new Error("the manifest changed outside the gallery since its last edit; undo would discard that change"), { status: 409 });
14935
15260
  }
14936
15261
  const tmp = manifestPath2 + ".pixelkiln-undo";
14937
- await writeFile14(tmp, last.before);
15262
+ await writeFile15(tmp, last.before);
14938
15263
  await rename9(tmp, manifestPath2);
14939
15264
  stack.pop();
14940
- log2(` manifest edit undone in ${path28.relative(process.cwd(), manifestPath2) || manifestPath2}`);
15265
+ log2(` manifest edit undone in ${path30.relative(process.cwd(), manifestPath2) || manifestPath2}`);
14941
15266
  return opts.reload();
14942
15267
  }
14943
15268
  const manifestPath = await manifestOf(body?.project);
14944
- const before = manifestPath ? await readFile18(manifestPath, "utf8").catch(() => null) : null;
15269
+ const before = manifestPath ? await readFile19(manifestPath, "utf8").catch(() => null) : null;
14945
15270
  const build = await apply(body);
14946
15271
  if (manifestPath && before !== null) {
14947
- const after = await readFile18(manifestPath, "utf8").catch(() => null);
15272
+ const after = await readFile19(manifestPath, "utf8").catch(() => null);
14948
15273
  if (after !== null && after !== before) {
14949
15274
  const stack = history.get(manifestPath) ?? [];
14950
15275
  stack.push({ before, afterSha: sha256(after) });
@@ -14999,7 +15324,7 @@ function createEditApplier(opts, log2) {
14999
15324
  } catch (error) {
15000
15325
  throw new ManifestEditError(error instanceof Error ? error.message : String(error));
15001
15326
  }
15002
- const written = await saveProjectImage(path28.dirname(path28.resolve(manifestPath2)), upload.data);
15327
+ const written = await saveProjectImage(path30.dirname(path30.resolve(manifestPath2)), upload.data);
15003
15328
  log2(` image added: ${written}`);
15004
15329
  return opts.reload();
15005
15330
  }
@@ -15035,6 +15360,23 @@ function createEditApplier(opts, log2) {
15035
15360
  "invalid skeleton animation: " + skeleton.error.issues.slice(0, 3).map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")
15036
15361
  );
15037
15362
  }
15363
+ const inpaint = InpaintRevisionRequestSchema.safeParse(body);
15364
+ if (inpaint.success) {
15365
+ let manifestPath2;
15366
+ try {
15367
+ manifestPath2 = await opts.manifestFor(inpaint.data.project);
15368
+ } catch (error) {
15369
+ throw new ManifestEditError(error instanceof Error ? error.message : String(error));
15370
+ }
15371
+ const mask = await createInpaintRevision(manifestPath2, inpaint.data);
15372
+ log2(` manifest edited: added inpaint revision ${inpaint.data.assetId} (mask ${mask})`);
15373
+ return opts.reload();
15374
+ }
15375
+ if (body?.action === "create-inpaint-revision") {
15376
+ throw new ManifestEditError(
15377
+ "invalid inpaint revision: " + inpaint.error.issues.slice(0, 3).map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")
15378
+ );
15379
+ }
15038
15380
  const parsed = ManifestEditSchema.safeParse(body);
15039
15381
  if (!parsed.success) {
15040
15382
  throw new ManifestEditError(
@@ -15049,7 +15391,7 @@ function createEditApplier(opts, log2) {
15049
15391
  }
15050
15392
  const result = await applyManifestEdit(manifestPath, parsed.data);
15051
15393
  if (result.changed) {
15052
- const relative = path28.relative(process.cwd(), result.manifestPath);
15394
+ const relative = path30.relative(process.cwd(), result.manifestPath);
15053
15395
  const shown = !relative || relative.startsWith("..") ? result.manifestPath : relative;
15054
15396
  log2(` manifest edited: ${describeEdit(parsed.data)} in ${shown}`);
15055
15397
  }
@@ -15100,8 +15442,8 @@ function createGalleryPriceHandler(opts) {
15100
15442
  }
15101
15443
 
15102
15444
  // src/pipeline/history.ts
15103
- import { existsSync as existsSync20 } from "fs";
15104
- import path29 from "path";
15445
+ import { existsSync as existsSync21 } from "fs";
15446
+ import path31 from "path";
15105
15447
  var HISTORY_ENV = "PIXELKILN_HISTORY";
15106
15448
  function historyLimit(manifest, env = process.env) {
15107
15449
  if (manifest?.history !== void 0) return manifest.history;
@@ -15188,10 +15530,10 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
15188
15530
  }
15189
15531
  for (let i = 0; i < entry.outputs.length; i++) {
15190
15532
  const file = currentEntryOutputPath(entry, spec, i);
15191
- if (!existsSync20(file)) continue;
15533
+ if (!existsSync21(file)) continue;
15192
15534
  if (await sha256File(file) !== entry.outputs[i].sha256 && !opts.force) {
15193
15535
  throw new OverwriteRefusedError(
15194
- `${path29.relative(process.cwd(), file)} was changed after download; keep it with pixelkiln edit, or pass --force to replace it`,
15536
+ `${path31.relative(process.cwd(), file)} was changed after download; keep it with pixelkiln edit, or pass --force to replace it`,
15195
15537
  [file]
15196
15538
  );
15197
15539
  }
@@ -15244,7 +15586,7 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
15244
15586
  // src/gallery/snapshot.ts
15245
15587
  var PROJECT_FILE_TYPE = "application/octet-stream";
15246
15588
  function galleryMediaId(absolutePath) {
15247
- return createHash3("sha256").update(path30.resolve(absolutePath)).digest("hex").slice(0, 24);
15589
+ return createHash4("sha256").update(path32.resolve(absolutePath)).digest("hex").slice(0, 24);
15248
15590
  }
15249
15591
  function galleryMediaRoute(id) {
15250
15592
  return `/media/${id}`;
@@ -15263,9 +15605,9 @@ function describeHistory(media, entry, cacheDir) {
15263
15605
  return (entry.history ?? []).map((generation, i) => {
15264
15606
  const outputs = generation.outputs.map((output) => {
15265
15607
  const mediaType = output.mediaType ?? MediaType.PNG;
15266
- const file = path30.join(cacheDir, cacheFileName(output.sha256, mediaType));
15608
+ const file = path32.join(cacheDir, cacheFileName(output.sha256, mediaType));
15267
15609
  let url = null;
15268
- if (existsSync21(file)) {
15610
+ if (existsSync22(file)) {
15269
15611
  const id = galleryMediaId(file);
15270
15612
  media.set(id, { path: file, contentType: mediaType });
15271
15613
  url = `${galleryMediaRoute(id)}?v=${output.sha256.slice(0, 24)}`;
@@ -15297,7 +15639,7 @@ function describeHistory(media, entry, cacheDir) {
15297
15639
  var MAX_COMPARED_BYTES = 8 * 1024 * 1024;
15298
15640
  async function samePixels(a, b) {
15299
15641
  try {
15300
- const [left, right] = await Promise.all([readFile19(a), readFile19(b)]);
15642
+ const [left, right] = await Promise.all([readFile20(a), readFile20(b)]);
15301
15643
  if (left.length > MAX_COMPARED_BYTES || right.length > MAX_COMPARED_BYTES) return false;
15302
15644
  const x = decodePng(left);
15303
15645
  const y = decodePng(right);
@@ -15395,10 +15737,10 @@ async function describeOutput(media, root, absolutePath, recorded = {}) {
15395
15737
  async function describeQuality(media, root, inspection) {
15396
15738
  const outputPaths = inspection.outputs ?? [inspection.output];
15397
15739
  const outputs = await Promise.all(
15398
- outputPaths.map((file) => describeOutput(media, root, path30.resolve(root, file)))
15740
+ outputPaths.map((file) => describeOutput(media, root, path32.resolve(root, file)))
15399
15741
  );
15400
- const recordPath = path30.resolve(root, inspection.record);
15401
- const recordExists = existsSync21(recordPath);
15742
+ const recordPath = path32.resolve(root, inspection.record);
15743
+ const recordExists = existsSync22(recordPath);
15402
15744
  let options = null;
15403
15745
  let check = null;
15404
15746
  if (recordExists) {
@@ -15417,7 +15759,7 @@ async function describeQuality(media, root, inspection) {
15417
15759
  return {
15418
15760
  state: inspection.state,
15419
15761
  reason: inspection.reason,
15420
- output: portableOutputPath(path30.resolve(root, inspection.output), root),
15762
+ output: portableOutputPath(path32.resolve(root, inspection.output), root),
15421
15763
  record: portableOutputPath(recordPath, root),
15422
15764
  recordExists,
15423
15765
  outputs,
@@ -15455,9 +15797,9 @@ async function buildGallerySnapshot(opts) {
15455
15797
  const media = /* @__PURE__ */ new Map();
15456
15798
  const plan = await buildPlan(specs, lock);
15457
15799
  const items = [];
15458
- const manifestText = await readFile19(loaded.path, "utf8");
15800
+ const manifestText = await readFile20(loaded.path, "utf8");
15459
15801
  const manifestSha256 = sha256(manifestText);
15460
- const cacheDir = path30.resolve(path30.dirname(opts.lockPath), ".pixelkiln", "cache");
15802
+ const cacheDir = path32.resolve(path32.dirname(opts.lockPath), ".pixelkiln", "cache");
15461
15803
  let rawStyles = {};
15462
15804
  try {
15463
15805
  const raw = JSON.parse(manifestText);
@@ -15481,12 +15823,12 @@ async function buildGallerySnapshot(opts) {
15481
15823
  )
15482
15824
  );
15483
15825
  if (spec.source) {
15484
- const editPath = path30.resolve(root, spec.source);
15826
+ const editPath = path32.resolve(root, spec.source);
15485
15827
  const memberCount = sourceIsStem(spec.source, entry, root) ? entry.outputs.length : 1;
15486
15828
  const editShas = await Promise.all(
15487
15829
  Array.from({ length: memberCount }, async (_, index) => {
15488
15830
  const file = sourceOutputPath(spec.source, entry, index, root);
15489
- return { file, sha256: existsSync21(file) ? await sha256File(file) : null };
15831
+ return { file, sha256: existsSync22(file) ? await sha256File(file) : null };
15490
15832
  })
15491
15833
  );
15492
15834
  edits = await Promise.all(editShas.map(({ file, sha256: editSha }) => describeOutput(media, root, file, { sha256: editSha })));
@@ -15494,7 +15836,7 @@ async function buildGallerySnapshot(opts) {
15494
15836
  const companion = await readHandEditCompanion(editPath);
15495
15837
  if (companion) {
15496
15838
  const projectPath = handEditProjectPath(editPath);
15497
- const project = companion.project && existsSync21(projectPath) ? await fileInfo(projectPath) : null;
15839
+ const project = companion.project && existsSync22(projectPath) ? await fileInfo(projectPath) : null;
15498
15840
  let projectUrl = null;
15499
15841
  if (project) {
15500
15842
  const id = galleryMediaId(projectPath);
@@ -15530,8 +15872,8 @@ async function buildGallerySnapshot(opts) {
15530
15872
  editStatus = edits.some((member) => !member.exists) ? "missing" : editChanged.every((changed) => !changed) ? "same" : regenerated ? "regenerated-since" : "edited";
15531
15873
  }
15532
15874
  } else if (spec.source) {
15533
- outputs = [await describeOutput(media, root, path30.resolve(root, spec.source))];
15534
- } else if (!entry && existsSync21(spec.outFile)) {
15875
+ outputs = [await describeOutput(media, root, path32.resolve(root, spec.source))];
15876
+ } else if (!entry && existsSync22(spec.outFile)) {
15535
15877
  outputs = [await describeOutput(media, root, spec.outFile)];
15536
15878
  } else {
15537
15879
  outputs = [];
@@ -15734,7 +16076,7 @@ async function buildGallerySnapshot(opts) {
15734
16076
  id: loaded.manifest.name,
15735
16077
  name: loaded.manifest.name,
15736
16078
  manifest: loaded.path,
15737
- lock: path30.resolve(opts.lockPath),
16079
+ lock: path32.resolve(opts.lockPath),
15738
16080
  root,
15739
16081
  provider: loaded.manifest.provider,
15740
16082
  account: null,
@@ -15760,7 +16102,7 @@ async function buildGallerySnapshot(opts) {
15760
16102
  return { snapshot, media };
15761
16103
  }
15762
16104
  async function buildWorkspaceGallerySnapshot(opts) {
15763
- const dir = path30.dirname(path30.resolve(opts.workspacePath));
16105
+ const dir = path32.dirname(path32.resolve(opts.workspacePath));
15764
16106
  const media = /* @__PURE__ */ new Map();
15765
16107
  const projects = [];
15766
16108
  const items = [];
@@ -15803,11 +16145,11 @@ async function buildWorkspaceGallerySnapshot(opts) {
15803
16145
  id: project.id,
15804
16146
  name: loaded.manifest.name,
15805
16147
  manifest: manifestPath,
15806
- lock: path30.resolve(lockPath),
16148
+ lock: path32.resolve(lockPath),
15807
16149
  root: loaded.root,
15808
16150
  provider: loaded.manifest.provider,
15809
16151
  account: project.account ?? null,
15810
- manifestSha256: manifestSha256 ?? sha256(await readFile19(manifestPath, "utf8")),
16152
+ manifestSha256: manifestSha256 ?? sha256(await readFile20(manifestPath, "utf8")),
15811
16153
  historyLimit: historyLimit(loaded.manifest),
15812
16154
  entries: Object.keys(lock.entries).length,
15813
16155
  items: projectItems.length,
@@ -15819,8 +16161,8 @@ async function buildWorkspaceGallerySnapshot(opts) {
15819
16161
  id: project.id,
15820
16162
  name: project.id,
15821
16163
  manifest: manifestPath,
15822
- lock: path30.resolve(lockPath),
15823
- root: path30.dirname(manifestPath),
16164
+ lock: path32.resolve(lockPath),
16165
+ root: path32.dirname(manifestPath),
15824
16166
  provider: project.provider,
15825
16167
  account: project.account ?? null,
15826
16168
  manifestSha256: null,
@@ -15847,7 +16189,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
15847
16189
  version: 1,
15848
16190
  generatedAt: (opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
15849
16191
  project: null,
15850
- workspace: { path: path30.resolve(opts.workspacePath), dir, projects },
16192
+ workspace: { path: path32.resolve(opts.workspacePath), dir, projects },
15851
16193
  filter,
15852
16194
  totals: {
15853
16195
  entries: projects.reduce((sum, project) => sum + project.entries, 0),
@@ -15866,10 +16208,10 @@ async function buildWorkspaceGallerySnapshot(opts) {
15866
16208
  // src/gallery/server.ts
15867
16209
  import { randomBytes } from "crypto";
15868
16210
  import { createServer as createServer2 } from "http";
15869
- import { readFile as readFile20 } from "fs/promises";
16211
+ import { readFile as readFile21 } from "fs/promises";
15870
16212
 
15871
16213
  // src/gallery/page.ts
15872
- import { existsSync as existsSync22, readFileSync as readFileSync4 } from "fs";
16214
+ import { existsSync as existsSync23, readFileSync as readFileSync5 } from "fs";
15873
16215
 
15874
16216
  // src/gallery/client-bundle.ts
15875
16217
  import { createRequire } from "module";
@@ -15909,8 +16251,8 @@ var client;
15909
16251
  function clientAssets() {
15910
16252
  const prebuilt = new URL("gallery.js", CLIENT_DIR);
15911
16253
  client ??= {
15912
- css: readFileSync4(new URL("gallery.css", CLIENT_DIR), "utf8"),
15913
- js: existsSync22(prebuilt) ? readFileSync4(prebuilt, "utf8") : bundleGalleryClient()
16254
+ css: readFileSync5(new URL("gallery.css", CLIENT_DIR), "utf8"),
16255
+ js: existsSync23(prebuilt) ? readFileSync5(prebuilt, "utf8") : bundleGalleryClient()
15914
16256
  };
15915
16257
  return client;
15916
16258
  }
@@ -16181,7 +16523,7 @@ async function serveGallery(opts) {
16181
16523
  const asset = reviewAssets.get(jobId)?.get(url2.pathname);
16182
16524
  if (!asset) return fail(404, "no such review asset");
16183
16525
  try {
16184
- const bytes = await readFile20(asset.path);
16526
+ const bytes = await readFile21(asset.path);
16185
16527
  res.writeHead(200, {
16186
16528
  "Content-Type": asset.contentType,
16187
16529
  "Cache-Control": "no-store",
@@ -16264,7 +16606,7 @@ async function serveGallery(opts) {
16264
16606
  if (!asset) return fail(404, "no such gallery media");
16265
16607
  let bytes;
16266
16608
  try {
16267
- bytes = await readFile20(asset.path);
16609
+ bytes = await readFile21(asset.path);
16268
16610
  } catch {
16269
16611
  return fail(404, "gallery media is no longer on disk; refresh the page");
16270
16612
  }
@@ -16308,7 +16650,7 @@ async function serveGallery(opts) {
16308
16650
 
16309
16651
  // src/gallery/generate.ts
16310
16652
  import { randomBytes as randomBytes2 } from "crypto";
16311
- import { z as z11 } from "zod";
16653
+ import { z as z12 } from "zod";
16312
16654
 
16313
16655
  // src/pipeline/poll.ts
16314
16656
  var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -16592,6 +16934,10 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
16592
16934
  ...spec.revision.keypointsSha256 ? { keypointsSha256: spec.revision.keypointsSha256 } : {},
16593
16935
  ...spec.revision.skeletonTemplate ? { skeletonTemplate: spec.revision.skeletonTemplate } : {},
16594
16936
  ...spec.revision.description ? { description: spec.revision.description } : {},
16937
+ ...spec.revision.removalTask ? { removalTask: spec.revision.removalTask } : {},
16938
+ ...spec.revision.driftThreshold == null ? {} : { driftThreshold: spec.revision.driftThreshold },
16939
+ ...spec.revision.subjectDescription ? { subjectDescription: spec.revision.subjectDescription } : {},
16940
+ ...spec.revision.initialPose ? { initialPose: spec.revision.initialPose } : {},
16595
16941
  ...spec.revision.engine ? { engine: spec.revision.engine } : {}
16596
16942
  } : null,
16597
16943
  outfit: spec.character?.kind === "outfit" && outfitParentEntry ? {
@@ -16719,13 +17065,13 @@ function ownedReferenceImageId(spec, lock, providerId) {
16719
17065
  }
16720
17066
 
16721
17067
  // src/pick/server.ts
16722
- import { existsSync as existsSync24 } from "fs";
16723
- import { readFile as readFile22 } from "fs/promises";
17068
+ import { existsSync as existsSync25 } from "fs";
17069
+ import { readFile as readFile23 } from "fs/promises";
16724
17070
 
16725
17071
  // 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";
17072
+ import { readdir, readFile as readFile22, writeFile as writeFile16 } from "fs/promises";
17073
+ import { existsSync as existsSync24 } from "fs";
17074
+ import path33 from "path";
16729
17075
  function pngSize(buf) {
16730
17076
  if (buf.length < 24) return null;
16731
17077
  const isPng = buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834;
@@ -16739,7 +17085,7 @@ function slugify(value) {
16739
17085
  async function walk(dir, exclude) {
16740
17086
  const out = [];
16741
17087
  for (const entry of await readdir(dir, { withFileTypes: true })) {
16742
- const full = path31.join(dir, entry.name);
17088
+ const full = path33.join(dir, entry.name);
16743
17089
  if (exclude.some((x) => full.includes(x))) continue;
16744
17090
  if (entry.isDirectory()) out.push(...await walk(full, exclude));
16745
17091
  else if (entry.name.toLowerCase().endsWith(".png")) out.push(full);
@@ -16753,14 +17099,14 @@ async function scanAssets(root, opts = {}) {
16753
17099
  const skipped = [];
16754
17100
  const seen = /* @__PURE__ */ new Map();
16755
17101
  for (const file of files.sort()) {
16756
- const size = pngSize(await readFile21(file));
17102
+ const size = pngSize(await readFile22(file));
16757
17103
  if (!size) {
16758
17104
  skipped.push(`${file} (not a readable PNG)`);
16759
17105
  continue;
16760
17106
  }
16761
- const rel = path31.relative(root, file);
16762
- const category = path31.dirname(rel) === "." ? "" : path31.dirname(rel);
16763
- let id = slugify(path31.basename(rel));
17107
+ const rel = path33.relative(root, file);
17108
+ const category = path33.dirname(rel) === "." ? "" : path33.dirname(rel);
17109
+ let id = slugify(path33.basename(rel));
16764
17110
  if (seen.has(id)) {
16765
17111
  const n = seen.get(id) + 1;
16766
17112
  seen.set(id, n);
@@ -16818,10 +17164,10 @@ function mode(values) {
16818
17164
  return [...counts.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? 64;
16819
17165
  }
16820
17166
  async function writeManifestFile(target, manifest) {
16821
- if (existsSync23(target)) {
17167
+ if (existsSync24(target)) {
16822
17168
  throw new Error(`${target} already exists; refusing to overwrite an existing manifest.`);
16823
17169
  }
16824
- await writeFile15(target, JSON.stringify(manifest, null, 2) + "\n");
17170
+ await writeFile16(target, JSON.stringify(manifest, null, 2) + "\n");
16825
17171
  }
16826
17172
 
16827
17173
  // src/pick/sheet.ts
@@ -17234,13 +17580,13 @@ async function currentArt(entry, spec) {
17234
17580
  const previous = primaryOutput({ ...entry, outputs: entry.supersededOutputs });
17235
17581
  if (!previous) return null;
17236
17582
  const file = resolveOutputPath(previous.path, spec.root);
17237
- if (!existsSync24(file)) return null;
17583
+ if (!existsSync25(file)) return null;
17238
17584
  const contentType = previous.mediaType ?? mediaTypeFromExtension(file);
17239
17585
  if (!contentType) return null;
17240
17586
  let width = entry.width;
17241
17587
  let height = entry.height;
17242
17588
  if (contentType === "image/png") {
17243
- const size = pngSize(await readFile22(file));
17589
+ const size = pngSize(await readFile23(file));
17244
17590
  if (size) ({ width, height } = size);
17245
17591
  }
17246
17592
  return { path: file, contentType, width, height };
@@ -17408,26 +17754,26 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
17408
17754
  }
17409
17755
 
17410
17756
  // 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),
17757
+ var GenerateRequestSchema = z12.object({
17758
+ project: z12.string().min(1).optional(),
17759
+ keys: z12.array(z12.string().min(1)).min(1).max(500),
17414
17760
  /**
17415
17761
  * Regenerate work that is up to date, as `gen --force` does, and let the
17416
17762
  * fetch replace a locally modified or untracked file. With `restore`, put
17417
17763
  * the recorded bytes back over a modified file.
17418
17764
  */
17419
- force: z11.boolean().optional(),
17765
+ force: z12.boolean().optional(),
17420
17766
  /** Advance in-flight, review, or selected work without submitting anything. */
17421
- resume: z11.boolean().optional(),
17767
+ resume: z12.boolean().optional(),
17422
17768
  /** Re-download downloaded outputs and replace files whose object changed upstream. */
17423
- refresh: z11.boolean().optional(),
17769
+ refresh: z12.boolean().optional(),
17424
17770
  /** Put recorded outputs back on disk from the cache or the provider, without generating. */
17425
- restore: z11.boolean().optional(),
17771
+ restore: z12.boolean().optional(),
17426
17772
  /**
17427
17773
  * Bring a replaced generation back: 1-based position in the record's
17428
17774
  * history (newest first) or an output hash prefix. One key at a time.
17429
17775
  */
17430
- revert: z11.string().min(1).max(64).optional()
17776
+ revert: z12.string().min(1).max(64).optional()
17431
17777
  }).strict().refine((request) => [request.resume, request.refresh, request.restore, request.revert !== void 0].filter(Boolean).length <= 1, {
17432
17778
  message: "resume, refresh, restore, and revert are exclusive"
17433
17779
  }).refine((request) => request.revert === void 0 || request.keys.length === 1, { message: "revert takes one key" });
@@ -17773,15 +18119,15 @@ function createGenerateHandlers(opts) {
17773
18119
  }
17774
18120
 
17775
18121
  // src/gallery/editor.ts
17776
- import { readFile as readFile24 } from "fs/promises";
17777
- import path33 from "path";
18122
+ import { readFile as readFile25 } from "fs/promises";
18123
+ import path35 from "path";
17778
18124
 
17779
18125
  // src/editor/install.ts
17780
- 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";
17783
- import os2 from "os";
17784
- import path32 from "path";
18126
+ import { createHash as createHash5 } from "crypto";
18127
+ import { existsSync as existsSync26 } from "fs";
18128
+ import { mkdir as mkdir12, readFile as readFile24, rename as rename10, rm as rm11, stat as stat3, writeFile as writeFile17 } from "fs/promises";
18129
+ import os3 from "os";
18130
+ import path34 from "path";
17785
18131
 
17786
18132
  // tools/pixelorama-bridge/pin.json
17787
18133
  var pin_default = {
@@ -17845,15 +18191,15 @@ var EditorInstallError = class extends Error {
17845
18191
  };
17846
18192
  function editorToolsRoot() {
17847
18193
  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");
18194
+ if (override) return path34.resolve(override);
18195
+ if (process.platform === "darwin") return path34.join(os3.homedir(), "Library", "Caches", "pixelkiln", "tools");
17850
18196
  if (process.platform === "win32") {
17851
- return path32.join(process.env.LOCALAPPDATA ?? path32.join(os2.homedir(), "AppData", "Local"), "pixelkiln", "tools");
18197
+ return path34.join(process.env.LOCALAPPDATA ?? path34.join(os3.homedir(), "AppData", "Local"), "pixelkiln", "tools");
17852
18198
  }
17853
- return path32.join(process.env.XDG_CACHE_HOME ?? path32.join(os2.homedir(), ".cache"), "pixelkiln", "tools");
18199
+ return path34.join(process.env.XDG_CACHE_HOME ?? path34.join(os3.homedir(), ".cache"), "pixelkiln", "tools");
17854
18200
  }
17855
18201
  function editorDir(pin = EDITOR_PIN, root = editorToolsRoot()) {
17856
- return path32.join(root, "pixelorama", pin.release ?? "unpublished");
18202
+ return path34.join(root, "pixelorama", pin.release ?? "unpublished");
17857
18203
  }
17858
18204
  function editorBaseUrl(pin = EDITOR_PIN) {
17859
18205
  const override = process.env.PIXELKILN_EDITOR_URL?.trim();
@@ -17863,7 +18209,7 @@ function editorBaseUrl(pin = EDITOR_PIN) {
17863
18209
  }
17864
18210
  async function sha256Of(file) {
17865
18211
  try {
17866
- return createHash4("sha256").update(await readFile23(file)).digest("hex");
18212
+ return createHash5("sha256").update(await readFile24(file)).digest("hex");
17867
18213
  } catch {
17868
18214
  return null;
17869
18215
  }
@@ -17876,8 +18222,8 @@ async function editorStatus(opts = {}) {
17876
18222
  let totalBytes = 0;
17877
18223
  for (const [name, expected] of Object.entries(pin.files)) {
17878
18224
  totalBytes += expected.bytes;
17879
- const file = path32.join(dir, name);
17880
- const info = existsSync25(file) ? await stat3(file).catch(() => null) : null;
18225
+ const file = path34.join(dir, name);
18226
+ const info = existsSync26(file) ? await stat3(file).catch(() => null) : null;
17881
18227
  if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
17882
18228
  installedBytes += expected.bytes;
17883
18229
  } else {
@@ -17930,17 +18276,17 @@ async function installEditor(opts = {}) {
17930
18276
  if (bytes.length !== expected.bytes) {
17931
18277
  throw new EditorInstallError(`${name}: expected ${expected.bytes} bytes, got ${bytes.length}`, name);
17932
18278
  }
17933
- const digest2 = createHash4("sha256").update(bytes).digest("hex");
18279
+ const digest2 = createHash5("sha256").update(bytes).digest("hex");
17934
18280
  if (digest2 !== expected.sha256) {
17935
18281
  throw new EditorInstallError(`${name}: sha256 ${digest2.slice(0, 12)}\u2026 does not match the pinned ${expected.sha256.slice(0, 12)}\u2026`, name);
17936
18282
  }
17937
- const final = path32.join(dir, name);
18283
+ const final = path34.join(dir, name);
17938
18284
  const tmp = `${final}.${process.pid}.part`;
17939
18285
  try {
17940
- await writeFile16(tmp, bytes);
18286
+ await writeFile17(tmp, bytes);
17941
18287
  await rename10(tmp, final);
17942
18288
  } finally {
17943
- await rm10(tmp, { force: true });
18289
+ await rm11(tmp, { force: true });
17944
18290
  }
17945
18291
  fetchedBytes += bytes.length;
17946
18292
  downloaded.push(name);
@@ -17980,7 +18326,7 @@ var EDITOR_CONTENT_TYPES = {
17980
18326
  ".json": "application/json; charset=utf-8"
17981
18327
  };
17982
18328
  function editorContentType(name) {
17983
- return EDITOR_CONTENT_TYPES[path32.extname(name).toLowerCase()] ?? null;
18329
+ return EDITOR_CONTENT_TYPES[path34.extname(name).toLowerCase()] ?? null;
17984
18330
  }
17985
18331
 
17986
18332
  // src/gallery/editor.ts
@@ -18056,7 +18402,7 @@ function createGalleryEditorHandlers(opts = {}) {
18056
18402
  if (!status.installed) return null;
18057
18403
  let bytes;
18058
18404
  try {
18059
- bytes = await readFile24(path33.join(dir, name));
18405
+ bytes = await readFile25(path35.join(dir, name));
18060
18406
  } catch {
18061
18407
  verified = null;
18062
18408
  return null;
@@ -18096,7 +18442,7 @@ async function serveUntilStopped(initial, reload, args, access2) {
18096
18442
  };
18097
18443
  const loadProject = async (project) => {
18098
18444
  const ctx = await access2.loadProject(project);
18099
- loadProjectEnv(path34.dirname(ctx.loaded.path));
18445
+ loadProjectEnv(path36.dirname(ctx.loaded.path));
18100
18446
  return ctx;
18101
18447
  };
18102
18448
  const loadProjectEnv = (dir) => {
@@ -18105,7 +18451,7 @@ async function serveUntilStopped(initial, reload, args, access2) {
18105
18451
  for (const [name, value] of Object.entries(declared)) {
18106
18452
  if (credentialNames.has(name) && name in process.env && process.env[name] !== value) {
18107
18453
  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.`
18454
+ `${path36.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
18455
  );
18110
18456
  }
18111
18457
  }
@@ -18113,7 +18459,7 @@ async function serveUntilStopped(initial, reload, args, access2) {
18113
18459
  };
18114
18460
  const quoters = /* @__PURE__ */ new Map();
18115
18461
  const quoteFor = async (project) => {
18116
- loadProjectEnv(path34.dirname(path34.resolve(await access2.manifestFor(project))));
18462
+ loadProjectEnv(path36.dirname(path36.resolve(await access2.manifestFor(project))));
18117
18463
  const key = process.env.PIXELLAB_API_KEY;
18118
18464
  if (!key) return null;
18119
18465
  let quoter = quoters.get(key);
@@ -18164,8 +18510,8 @@ async function serveUntilStopped(initial, reload, args, access2) {
18164
18510
  }
18165
18511
  async function runGallery(args) {
18166
18512
  if (args.workspace) {
18167
- const workspacePath = path34.resolve(args.workspace);
18168
- if (!existsSync26(workspacePath)) {
18513
+ const workspacePath = path36.resolve(args.workspace);
18514
+ if (!existsSync27(workspacePath)) {
18169
18515
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
18170
18516
  }
18171
18517
  const filter2 = { styles: args.styles, assets: args.assets };
@@ -18185,7 +18531,7 @@ async function runGallery(args) {
18185
18531
  const registered = async (projectId) => {
18186
18532
  const project = (await loadWorkspace(workspacePath)).projects.find((candidate) => candidate.id === projectId);
18187
18533
  if (!projectId || !project) throw new Error(`unknown workspace project "${projectId ?? ""}"`);
18188
- return resolveProject(path34.dirname(workspacePath), project);
18534
+ return resolveProject(path36.dirname(workspacePath), project);
18189
18535
  };
18190
18536
  await serveUntilStopped(initial, build2, args, {
18191
18537
  manifestFor: async (projectId) => (await registered(projectId)).manifestPath,
@@ -18210,7 +18556,7 @@ async function runGallery(args) {
18210
18556
  return buildGallerySnapshot({ ...fresh, filter });
18211
18557
  };
18212
18558
  await serveUntilStopped(await build(), reload, args, {
18213
- manifestFor: () => path34.resolve(args.manifest),
18559
+ manifestFor: () => path36.resolve(args.manifest),
18214
18560
  loadProject: () => openProject(args.manifest, { lockPath: args.lock, env: false })
18215
18561
  });
18216
18562
  }
@@ -18519,18 +18865,18 @@ async function runRestoreGeneration(args) {
18519
18865
  }
18520
18866
 
18521
18867
  // src/cli/commands/housekeeping.ts
18522
- import { existsSync as existsSync28 } from "fs";
18868
+ import { existsSync as existsSync29 } from "fs";
18523
18869
 
18524
18870
  // 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";
18871
+ import { existsSync as existsSync28 } from "fs";
18872
+ import { readFile as readFile26, readdir as readdir2, rm as rm12 } from "fs/promises";
18873
+ import path37 from "path";
18528
18874
  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)}`);
18875
+ if (options.prune && !existsSync28(lockPath)) {
18876
+ throw new Error(`Refusing to prune without an existing lockfile at ${path37.resolve(lockPath)}`);
18531
18877
  }
18532
- const contentDir = path35.resolve(path35.dirname(lockPath), ".pixelkiln", "cache");
18533
- const remotePath = path35.resolve(cachePathFor(lockPath));
18878
+ const contentDir = path37.resolve(path37.dirname(lockPath), ".pixelkiln", "cache");
18879
+ const remotePath = path37.resolve(cachePathFor(lockPath));
18534
18880
  const referenced = referencedHashes(lock);
18535
18881
  let content = await inspectContentCache(contentDir, referenced);
18536
18882
  let remoteHashes = await inspectRemoteHashCache(remotePath);
@@ -18542,7 +18888,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
18542
18888
  ]);
18543
18889
  for (const name of names) {
18544
18890
  try {
18545
- await rm11(path35.join(contentDir, name), { force: true });
18891
+ await rm12(path37.join(contentDir, name), { force: true });
18546
18892
  removed.contentFiles++;
18547
18893
  } catch {
18548
18894
  }
@@ -18551,7 +18897,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
18551
18897
  await saveCache(remotePath, { version: 1, hashes: {} });
18552
18898
  removed.resetRemoteHashCache = true;
18553
18899
  } else if (remoteHashes.invalidIds.length) {
18554
- const cache = parseCache(JSON.parse(await readFile25(remotePath, "utf8")));
18900
+ const cache = parseCache(JSON.parse(await readFile26(remotePath, "utf8")));
18555
18901
  for (const id of remoteHashes.invalidIds) delete cache.hashes[id];
18556
18902
  removed.remoteHashEntries = remoteHashes.invalidIds.length;
18557
18903
  await saveCache(remotePath, cache);
@@ -18569,7 +18915,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
18569
18915
  async function inspectContentCache(contentDir, referenced) {
18570
18916
  const report = {
18571
18917
  path: contentDir,
18572
- exists: existsSync27(contentDir),
18918
+ exists: existsSync28(contentDir),
18573
18919
  files: 0,
18574
18920
  bytes: 0,
18575
18921
  valid: 0,
@@ -18590,12 +18936,12 @@ async function inspectContentCache(contentDir, referenced) {
18590
18936
  continue;
18591
18937
  }
18592
18938
  report.files++;
18593
- const file = path35.join(contentDir, entry.name);
18939
+ const file = path37.join(contentDir, entry.name);
18594
18940
  const mediaType = mediaTypeFromExtension(entry.name);
18595
18941
  const expected = mediaType ? entry.name.slice(0, -4) : "";
18596
18942
  let bytes;
18597
18943
  try {
18598
- bytes = await readFile25(file);
18944
+ bytes = await readFile26(file);
18599
18945
  report.bytes += bytes.length;
18600
18946
  } catch (err) {
18601
18947
  report.invalid.push({
@@ -18633,7 +18979,7 @@ async function inspectContentCache(contentDir, referenced) {
18633
18979
  async function inspectRemoteHashCache(remotePath) {
18634
18980
  const report = {
18635
18981
  path: remotePath,
18636
- exists: existsSync27(remotePath),
18982
+ exists: existsSync28(remotePath),
18637
18983
  entries: 0,
18638
18984
  valid: 0,
18639
18985
  invalidIds: [],
@@ -18642,7 +18988,7 @@ async function inspectRemoteHashCache(remotePath) {
18642
18988
  if (!report.exists) return report;
18643
18989
  let cache;
18644
18990
  try {
18645
- cache = parseCache(JSON.parse(await readFile25(remotePath, "utf8")));
18991
+ cache = parseCache(JSON.parse(await readFile26(remotePath, "utf8")));
18646
18992
  } catch (err) {
18647
18993
  report.error = err instanceof Error ? err.message : String(err);
18648
18994
  return report;
@@ -18706,7 +19052,7 @@ async function runAccept(args) {
18706
19052
  let intact = true;
18707
19053
  for (const output of entry.outputs) {
18708
19054
  const file = resolveOutputPath(output.path, item.spec.root);
18709
- if (!existsSync28(file) || await sha256File(file) !== output.sha256) {
19055
+ if (!existsSync29(file) || await sha256File(file) !== output.sha256) {
18710
19056
  intact = false;
18711
19057
  break;
18712
19058
  }
@@ -18761,42 +19107,42 @@ This drops their provenance from the lockfile. The art on disk is`);
18761
19107
  }
18762
19108
 
18763
19109
  // src/cli/commands/init.ts
18764
- import path36 from "path";
18765
- import { existsSync as existsSync29 } from "fs";
19110
+ import path38 from "path";
19111
+ import { existsSync as existsSync30 } from "fs";
18766
19112
  async function runInit(args) {
18767
19113
  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}`);
19114
+ const root = path38.resolve(args.from);
19115
+ if (!existsSync30(root)) throw new Error(`No directory at ${root}`);
18770
19116
  const generator = args.generator ?? "map";
18771
19117
  if (generator !== "1dir" && generator !== "map") {
18772
19118
  throw new Error(`--generator must be "1dir" or "map", got "${args.generator}".`);
18773
19119
  }
18774
- const target = path36.resolve(args.out ?? "pixelkiln.manifest.json");
19120
+ const target = path38.resolve(args.out ?? "pixelkiln.manifest.json");
18775
19121
  const { assets, skipped } = await scanAssets(root, { exclude: args.exclude });
18776
19122
  if (!assets.length) throw new Error(`No PNGs found under ${root}`);
18777
19123
  const manifest = buildManifest(
18778
- args.name ?? path36.basename(path36.dirname(target)),
19124
+ args.name ?? path38.basename(path38.dirname(target)),
18779
19125
  args.styles[0] ?? "base",
18780
19126
  generator,
18781
- path36.relative(path36.dirname(target), root) || ".",
19127
+ path38.relative(path38.dirname(target), root) || ".",
18782
19128
  assets
18783
19129
  );
18784
19130
  await writeManifestFile(target, manifest);
18785
- log(` scanned ${assets.length} PNG(s) under ${path36.relative(process.cwd(), root)}`);
19131
+ log(` scanned ${assets.length} PNG(s) under ${path38.relative(process.cwd(), root)}`);
18786
19132
  if (skipped.length) log(` skipped ${skipped.length} unreadable file(s)`);
18787
- log(` wrote ${path36.relative(process.cwd(), target)}`);
19133
+ log(` wrote ${path38.relative(process.cwd(), target)}`);
18788
19134
  log(`
18789
19135
  Prompts are intentionally empty. To recover the real ones from your`);
18790
19136
  log(` PixelLab account instead of inventing them:`);
18791
19137
  log(`
18792
- pixelkiln adopt --manifest ${path36.relative(process.cwd(), target)} --write-prompts
19138
+ pixelkiln adopt --manifest ${path38.relative(process.cwd(), target)} --write-prompts
18793
19139
  `);
18794
19140
  }
18795
19141
 
18796
19142
  // src/pipeline/doctor.ts
18797
- import { constants, existsSync as existsSync30 } from "fs";
19143
+ import { constants, existsSync as existsSync31 } from "fs";
18798
19144
  import { access, stat as stat4 } from "fs/promises";
18799
- import path37 from "path";
19145
+ import path39 from "path";
18800
19146
  async function doctor(loaded, specs, lock, lockPath, opts = {}) {
18801
19147
  const checks = [];
18802
19148
  const add = (id, level, message6) => checks.push({ id, level, message: message6 });
@@ -18807,8 +19153,8 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
18807
19153
  );
18808
19154
  add("lock", "ok", `${Object.keys(lock.entries).length} valid v2 lock entr(ies)`);
18809
19155
  const dirs = [...new Set(specs.flatMap((spec) => [
18810
- path37.dirname(spec.outFile),
18811
- ...spec.quality ? [path37.dirname(spec.quality.outFile)] : []
19156
+ path39.dirname(spec.outFile),
19157
+ ...spec.quality ? [path39.dirname(spec.quality.outFile)] : []
18812
19158
  ]))];
18813
19159
  const unwritable = [];
18814
19160
  for (const dir of dirs) {
@@ -18831,7 +19177,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
18831
19177
  for (let index = 0; index < entry.outputs.length; index++) {
18832
19178
  const output = entry.outputs[index];
18833
19179
  const spec = specByKey.get(key);
18834
- const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, path37.dirname(lockPath));
19180
+ const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, path39.dirname(lockPath));
18835
19181
  const owner = outputOwners.get(absolute);
18836
19182
  if (owner && owner !== key) duplicateOutputs.push(`${output.path} (${owner}, ${key})`);
18837
19183
  outputOwners.set(absolute, key);
@@ -18846,7 +19192,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
18846
19192
  } else {
18847
19193
  add("lock-outputs", "ok", `${outputOwners.size} recorded output path(s) are uniquely owned`);
18848
19194
  }
18849
- const cacheDir = path37.join(path37.dirname(lockPath), ".pixelkiln", "cache");
19195
+ const cacheDir = path39.join(path39.dirname(lockPath), ".pixelkiln", "cache");
18850
19196
  const stranded = [];
18851
19197
  const invalidState = [];
18852
19198
  const staleMapUrls = [];
@@ -18862,11 +19208,11 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
18862
19208
  const hasSource = Boolean(entry.sourceUrl || entry.sourceUrls?.length);
18863
19209
  let hasCache = false;
18864
19210
  for (const output of entry.outputs) {
18865
- const cached = path37.join(
19211
+ const cached = path39.join(
18866
19212
  cacheDir,
18867
19213
  cacheFileName(output.sha256, output.mediaType ?? MediaType.PNG)
18868
19214
  );
18869
- if (existsSync30(cached) && await sha256File(cached) === output.sha256) {
19215
+ if (existsSync31(cached) && await sha256File(cached) === output.sha256) {
18870
19216
  hasCache = true;
18871
19217
  break;
18872
19218
  }
@@ -18962,16 +19308,16 @@ async function nearestExistingDirectory(start) {
18962
19308
  if ((await stat4(current)).isDirectory()) return current;
18963
19309
  } catch {
18964
19310
  }
18965
- const parent = path37.dirname(current);
19311
+ const parent = path39.dirname(current);
18966
19312
  if (parent === current) return current;
18967
19313
  current = parent;
18968
19314
  }
18969
19315
  }
18970
19316
 
18971
19317
  // src/pipeline/audit.ts
18972
- import { readFile as readFile26 } from "fs/promises";
18973
- import { existsSync as existsSync31 } from "fs";
18974
- import path38 from "path";
19318
+ import { readFile as readFile27 } from "fs/promises";
19319
+ import { existsSync as existsSync32 } from "fs";
19320
+ import path40 from "path";
18975
19321
  function paletteDistance(asset, reference) {
18976
19322
  if (!asset.length || !reference.length) return 0;
18977
19323
  let total = 0;
@@ -19007,12 +19353,12 @@ async function auditStyle(loaded, specs, styleId, lock) {
19007
19353
  const unreadable = [];
19008
19354
  for (const spec of mine) {
19009
19355
  for (const output of resolveSpecOutputs(spec, lock, loaded.root)) {
19010
- if (!existsSync31(output.absolutePath)) {
19356
+ if (!existsSync32(output.absolutePath)) {
19011
19357
  missing.push(output.id);
19012
19358
  continue;
19013
19359
  }
19014
19360
  try {
19015
- const png = decodePng(await readFile26(output.absolutePath));
19361
+ const png = decodePng(await readFile27(output.absolutePath));
19016
19362
  const palette = extractPalette(png, 12);
19017
19363
  assets.push({
19018
19364
  assetId: spec.assetId,
@@ -19036,10 +19382,10 @@ async function auditStyle(loaded, specs, styleId, lock) {
19036
19382
  let referenceFromStyleImages = false;
19037
19383
  const refPalettes = [];
19038
19384
  for (const img of style.styleImages) {
19039
- const abs = path38.resolve(loaded.root, img.path);
19040
- if (!existsSync31(abs)) continue;
19385
+ const abs = path40.resolve(loaded.root, img.path);
19386
+ if (!existsSync32(abs)) continue;
19041
19387
  try {
19042
- refPalettes.push(extractPalette(decodePng(await readFile26(abs)), 12));
19388
+ refPalettes.push(extractPalette(decodePng(await readFile27(abs)), 12));
19043
19389
  } catch {
19044
19390
  }
19045
19391
  }
@@ -19267,24 +19613,24 @@ async function runAudit(args) {
19267
19613
  }
19268
19614
 
19269
19615
  // src/cli/commands/pack.ts
19270
- import path40 from "path";
19271
- import { readFile as readFile27 } from "fs/promises";
19616
+ import path42 from "path";
19617
+ import { readFile as readFile28 } from "fs/promises";
19272
19618
 
19273
19619
  // src/pipeline/pack.ts
19274
- import { readFileSync as readFileSync5 } from "fs";
19275
- import path39 from "path";
19620
+ import { readFileSync as readFileSync6 } from "fs";
19621
+ import path41 from "path";
19276
19622
  var DEFAULT_FRAME_SET_FPS = 12;
19277
19623
  function resolvePackInputs(raw, inputsFilePath) {
19278
19624
  if (!Array.isArray(raw) || !raw.length) {
19279
19625
  throw new Error("--inputs must be a non-empty JSON array of { id, path }");
19280
19626
  }
19281
- const baseDir = path39.dirname(path39.resolve(inputsFilePath));
19627
+ const baseDir = path41.dirname(path41.resolve(inputsFilePath));
19282
19628
  return raw.map((entry, i) => {
19283
19629
  const e = entry;
19284
19630
  if (typeof e.id !== "string" || typeof e.path !== "string") {
19285
19631
  throw new Error(`--inputs[${i}] needs string "id" and "path"`);
19286
19632
  }
19287
- return { id: e.id, path: path39.resolve(baseDir, e.path) };
19633
+ return { id: e.id, path: path41.resolve(baseDir, e.path) };
19288
19634
  });
19289
19635
  }
19290
19636
  function packSprites(inputs, options = {}) {
@@ -19305,7 +19651,7 @@ function packSprites(inputs, options = {}) {
19305
19651
  for (const input of ordered) {
19306
19652
  let sourceHash = null;
19307
19653
  try {
19308
- const bytes = readFileSync5(input.path);
19654
+ const bytes = readFileSync6(input.path);
19309
19655
  sourceHash = sha256(bytes);
19310
19656
  const png = decodePng(bytes);
19311
19657
  sprites.push({ id: input.id, width: png.width, height: png.height, pixels: png.pixels });
@@ -19368,7 +19714,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
19368
19714
  }
19369
19715
  const inputs2 = Object.entries(options.sourceOverrides).map(([id, source]) => ({
19370
19716
  id,
19371
- path: path39.resolve(source)
19717
+ path: path41.resolve(source)
19372
19718
  }));
19373
19719
  if (!inputs2.length) throw new Error(`No approved quality outputs for style "${styleId}".`);
19374
19720
  const packed2 = packSprites(inputs2, options);
@@ -19392,7 +19738,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
19392
19738
  let outputs = resolveEntryOutputs(entry, id, manifestDir);
19393
19739
  const source = sources[id];
19394
19740
  if (source && outputs.length <= 1) {
19395
- inputs.push({ id, path: path39.resolve(manifestDir, source) });
19741
+ inputs.push({ id, path: path41.resolve(manifestDir, source) });
19396
19742
  continue;
19397
19743
  }
19398
19744
  if (source && sourceIsStem(source, entry, manifestDir)) {
@@ -19426,7 +19772,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
19426
19772
  }
19427
19773
  }
19428
19774
  for (const [id, source] of Object.entries(sources)) {
19429
- if (!locked.has(id)) inputs.push({ id, path: path39.resolve(manifestDir, source) });
19775
+ if (!locked.has(id)) inputs.push({ id, path: path41.resolve(manifestDir, source) });
19430
19776
  }
19431
19777
  if (!inputs.length) {
19432
19778
  throw new Error(`No readable sprites for style "${styleId}"; ${noOutput.length} skipped.`);
@@ -19463,7 +19809,7 @@ function mountSprites(placements, options) {
19463
19809
  for (const p of placements) {
19464
19810
  let sourceHash = null;
19465
19811
  try {
19466
- const bytes = readFileSync5(p.path);
19812
+ const bytes = readFileSync6(p.path);
19467
19813
  sourceHash = sha256(bytes);
19468
19814
  const png = decodePng(bytes);
19469
19815
  if (png.width > cellWidth || png.height > cellHeight) {
@@ -19534,7 +19880,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
19534
19880
  const source = sources[id];
19535
19881
  const entry = lock.entries[`${styleId}/${id}`];
19536
19882
  if (source && !(entry && sourceIsStem(source, entry, manifestDir))) {
19537
- placements.push({ id, path: path39.resolve(manifestDir, source), cell });
19883
+ placements.push({ id, path: path41.resolve(manifestDir, source), cell });
19538
19884
  continue;
19539
19885
  }
19540
19886
  if (source && entry) {
@@ -19567,7 +19913,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
19567
19913
  `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
19914
  );
19569
19915
  }
19570
- const basePng = mount.base ? readFileSync5(path39.resolve(manifestDir, mount.base)) : void 0;
19916
+ const basePng = mount.base ? readFileSync6(path41.resolve(manifestDir, mount.base)) : void 0;
19571
19917
  const mounted = mountSprites(placements, {
19572
19918
  cellWidth: mount.cellWidth,
19573
19919
  cellHeight: mount.cellHeight,
@@ -19577,7 +19923,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
19577
19923
  ...mounted,
19578
19924
  sources: mount.base ? [{
19579
19925
  id: "$base",
19580
- path: path39.resolve(manifestDir, mount.base),
19926
+ path: path41.resolve(manifestDir, mount.base),
19581
19927
  sha256: sha256(basePng),
19582
19928
  included: true
19583
19929
  }, ...mounted.sources] : mounted.sources,
@@ -19981,12 +20327,12 @@ async function runPack(args) {
19981
20327
  if (args.primaryOnly || args.outputRoles.length) {
19982
20328
  throw new Error("--primary-only and --output-role require manifest-driven pack");
19983
20329
  }
19984
- const raw = JSON.parse(await readFile27(path40.resolve(args.inputs), "utf8"));
20330
+ const raw = JSON.parse(await readFile28(path42.resolve(args.inputs), "utf8"));
19985
20331
  const inputs = resolvePackInputs(raw, args.inputs);
19986
20332
  const { png, atlas, skipped, sources } = packSprites(inputs, { columns: args.columns });
19987
20333
  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`) });
20334
+ const base = path42.resolve(args.out.replace(/\.(?:png|json|tres)$/i, ""));
20335
+ const { extension, document } = renderSheetDocument(format, atlas, { imageName: path42.basename(`${base}.png`) });
19990
20336
  const outputs = [
19991
20337
  { path: `${base}.png`, data: png },
19992
20338
  { path: `${base}${extension}`, data: document }
@@ -19999,7 +20345,7 @@ async function runPack(args) {
19999
20345
  log(
20000
20346
  ` ${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
20347
  );
20002
- log(` ${path40.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
20348
+ log(` ${path42.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
20003
20349
  for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
20004
20350
  return;
20005
20351
  }
@@ -20007,7 +20353,7 @@ async function runPack(args) {
20007
20353
  if (args.primaryOnly && args.outputRoles.length) {
20008
20354
  throw new Error("pack accepts either --primary-only or --output-role, not both");
20009
20355
  }
20010
- const manifestDir = path40.dirname(path40.resolve(args.manifest));
20356
+ const manifestDir = path42.dirname(path42.resolve(args.manifest));
20011
20357
  const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
20012
20358
  for (const styleId of styleIds) {
20013
20359
  const style = loaded.manifest.styles[styleId];
@@ -20022,9 +20368,9 @@ async function runPack(args) {
20022
20368
  sourceOverrides: qualitySources,
20023
20369
  sources: manifestSources(loaded.manifest, styleId)
20024
20370
  });
20025
- const base = args.out ? path40.resolve(args.out.replace(/\.(?:png|json|tres)$/i, "")) : path40.resolve(manifestDir, style.outDir, `${styleId}-sheet`);
20371
+ const base = args.out ? path42.resolve(args.out.replace(/\.(?:png|json|tres)$/i, "")) : path42.resolve(manifestDir, style.outDir, `${styleId}-sheet`);
20026
20372
  const format = sheetFormat(args);
20027
- const { extension, document } = renderSheetDocument(format, atlas, { imageName: path40.basename(`${base}.png`) });
20373
+ const { extension, document } = renderSheetDocument(format, atlas, { imageName: path42.basename(`${base}.png`) });
20028
20374
  const outputs = [
20029
20375
  { path: `${base}.png`, data: png },
20030
20376
  { path: `${base}${extension}`, data: document }
@@ -20056,13 +20402,13 @@ async function runPack(args) {
20056
20402
  log(
20057
20403
  ` ${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
20404
  );
20059
- log(` ${path40.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
20405
+ log(` ${path42.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
20060
20406
  for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
20061
20407
  }
20062
20408
  }
20063
20409
  async function runMount(args) {
20064
20410
  const { loaded, lock } = await openProject2(args);
20065
- const manifestDir = path40.dirname(path40.resolve(args.manifest));
20411
+ const manifestDir = path42.dirname(path42.resolve(args.manifest));
20066
20412
  const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
20067
20413
  for (const styleId of styleIds) {
20068
20414
  const style = loaded.manifest.styles[styleId];
@@ -20101,9 +20447,9 @@ async function runMount(args) {
20101
20447
  sources,
20102
20448
  outputRoles
20103
20449
  );
20104
- const out = path40.resolve(manifestDir, style.mount.out);
20450
+ const out = path42.resolve(manifestDir, style.mount.out);
20105
20451
  const format = sheetFormat(args);
20106
- const { extension, document } = renderSheetDocument(format, atlas, { imageName: path40.basename(out) });
20452
+ const { extension, document } = renderSheetDocument(format, atlas, { imageName: path42.basename(out) });
20107
20453
  const metadata = out.replace(/\.png$/, "") + extension;
20108
20454
  const companion = out.replace(/\.png$/, "") + ".pixelkiln.json";
20109
20455
  const outputs = [
@@ -20123,7 +20469,7 @@ async function runMount(args) {
20123
20469
  await provenanceFile("$lock", args.lock),
20124
20470
  ...qualityRecords,
20125
20471
  ...artifactSources.filter(
20126
- (source) => source.id !== "$base" || path40.resolve(source.path) !== out
20472
+ (source) => source.id !== "$base" || path42.resolve(source.path) !== out
20127
20473
  )
20128
20474
  ],
20129
20475
  options: {
@@ -20137,14 +20483,14 @@ async function runMount(args) {
20137
20483
  log(
20138
20484
  ` ${styleId}: ${atlas.frames.length} cell(s) into ${atlas.sheet.width}x${atlas.sheet.height}` + (overBase ? ` over ${style.mount.base}` : " (new sheet)")
20139
20485
  );
20140
- log(` ${path40.relative(process.cwd(), out)} + atlas/provenance JSON`);
20486
+ log(` ${path42.relative(process.cwd(), out)} + atlas/provenance JSON`);
20141
20487
  for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
20142
20488
  }
20143
20489
  }
20144
20490
  async function runExport(args) {
20145
20491
  const { loaded, specs, lock } = await openProject2(args);
20146
20492
  const format = args.format ?? "generic";
20147
- const manifestDir = path40.dirname(path40.resolve(args.manifest));
20493
+ const manifestDir = path42.dirname(path42.resolve(args.manifest));
20148
20494
  const selected = specs.filter((spec) => {
20149
20495
  if (spec.generator !== "tiles" && spec.generator !== "terrain") return false;
20150
20496
  if (args.styles.length && !args.styles.includes(spec.styleId)) return false;
@@ -20160,12 +20506,12 @@ async function runExport(args) {
20160
20506
  for (const spec of selected) {
20161
20507
  const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
20162
20508
  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;
20509
+ const defaultBase = path42.resolve(manifestDir, style.outDir, `${spec.assetId}-tileset`);
20510
+ const base = args.out ? path42.resolve(args.out.replace(/\.(?:png|json|tsj|tres)$/i, "")) : defaultBase;
20165
20511
  const result = exportTileset(entry, spec, {
20166
20512
  format,
20167
20513
  manifestDir,
20168
- imageName: path40.basename(`${base}.png`),
20514
+ imageName: path42.basename(`${base}.png`),
20169
20515
  columns: args.columns
20170
20516
  });
20171
20517
  const outputs = [
@@ -20183,7 +20529,7 @@ async function runExport(args) {
20183
20529
  asset: spec.assetId,
20184
20530
  columns: args.columns ?? null,
20185
20531
  format,
20186
- image: path40.basename(`${base}.png`),
20532
+ image: path42.basename(`${base}.png`),
20187
20533
  providerRules: result.generic.providerRules,
20188
20534
  style: spec.styleId,
20189
20535
  tileType: spec.tileType ?? null
@@ -20193,7 +20539,7 @@ async function runExport(args) {
20193
20539
  ` ${spec.styleId}/${spec.assetId}: ${result.generic.tiles.length} tile(s), ${result.generic.sheet.width}x${result.generic.sheet.height} (${format})`
20194
20540
  );
20195
20541
  log(
20196
- ` ${path40.relative(process.cwd(), base)}.png + ${path40.basename(base)}${result.extension} + .pixelkiln.json`
20542
+ ` ${path42.relative(process.cwd(), base)}.png + ${path42.basename(base)}${result.extension} + .pixelkiln.json`
20197
20543
  );
20198
20544
  }
20199
20545
  }
@@ -20206,23 +20552,23 @@ function sheetFormat(args) {
20206
20552
  }
20207
20553
 
20208
20554
  // src/cli/commands/pixellab-utils.ts
20209
- import path42 from "path";
20555
+ import path44 from "path";
20210
20556
 
20211
20557
  // 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";
20558
+ import { existsSync as existsSync33 } from "fs";
20559
+ import { mkdir as mkdir13, readFile as readFile29, writeFile as writeFile18 } from "fs/promises";
20560
+ import path43 from "path";
20215
20561
  var UNZOOM_MIN_SIDE = 256;
20216
20562
  var UNZOOM_MAX_AREA = 2048 * 2048;
20217
20563
  function defaultUnzoomOut(input) {
20218
- const ext = path41.extname(input);
20564
+ const ext = path43.extname(input);
20219
20565
  return `${ext ? input.slice(0, -ext.length) : input}.unzoomed.png`;
20220
20566
  }
20221
20567
  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);
20568
+ const out = path43.resolve(opts.out ?? defaultUnzoomOut(input));
20569
+ if (path43.resolve(input) === out) throw new Error("unzoom will not overwrite its own input; pass a different --out");
20570
+ if (existsSync33(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
20571
+ const bytes = await readFile29(input);
20226
20572
  const metadata = imageMetadata(bytes);
20227
20573
  if (!metadata) throw new Error(`${input} is not a readable PNG or JPEG`);
20228
20574
  const { width, height, format } = metadata;
@@ -20235,22 +20581,22 @@ async function unzoomFile(client2, input, opts = {}) {
20235
20581
  throw new Error(`${input} is ${width}x${height}; PixelLab's unzoom takes at most 2048x2048 worth of pixels`);
20236
20582
  }
20237
20583
  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);
20584
+ await mkdir13(path43.dirname(out), { recursive: true });
20585
+ await writeFile18(out, res.png);
20240
20586
  return { out, original: res.originalSize, unzoomed: res.unzoomedSize, zoomFactor: res.zoomFactor, usage: res.usage };
20241
20587
  }
20242
20588
  var FONT_WEIGHTS = ["Bold", "Regular"];
20243
20589
  var FONT_GLYPH_SIZES = [8, 16, 32, 64];
20244
20590
  var FONT_COST_GENERATIONS = 25;
20245
20591
  function fontOutputPaths(out) {
20246
- const base = path41.resolve(out.replace(/\.(ttf|png)$/i, ""));
20592
+ const base = path43.resolve(out.replace(/\.(ttf|png)$/i, ""));
20247
20593
  return { ttf: `${base}.ttf`, atlas: `${base}.png` };
20248
20594
  }
20249
20595
  async function generateFont(client2, opts) {
20250
20596
  const { ttf, atlas } = fontOutputPaths(opts.out);
20251
20597
  if (!opts.force) {
20252
20598
  for (const file of [ttf, atlas]) {
20253
- if (existsSync32(file)) throw new Error(`${file} already exists; pass --force to replace it`);
20599
+ if (existsSync33(file)) throw new Error(`${file} already exists; pass --force to replace it`);
20254
20600
  }
20255
20601
  }
20256
20602
  const sleep3 = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
@@ -20277,9 +20623,9 @@ async function generateFont(client2, opts) {
20277
20623
  client2.download(job.download_ttf_url),
20278
20624
  client2.download(job.download_atlas_url)
20279
20625
  ]);
20280
- await mkdir13(path41.dirname(ttf), { recursive: true });
20281
- await writeFile17(ttf, ttfBytes);
20282
- await writeFile17(atlas, atlasBytes);
20626
+ await mkdir13(path43.dirname(ttf), { recursive: true });
20627
+ await writeFile18(ttf, ttfBytes);
20628
+ await writeFile18(atlas, atlasBytes);
20283
20629
  return { jobId, ttf, atlas, glyphPx: job.glyph_px ?? null, usage: job.usage };
20284
20630
  }
20285
20631
  if (Date.now() - started > timeout) {
@@ -20293,7 +20639,7 @@ async function generateFont(client2, opts) {
20293
20639
 
20294
20640
  // src/cli/commands/pixellab-utils.ts
20295
20641
  function loadKeys(args) {
20296
- loadEnvFiles(path42.dirname(path42.resolve(args.manifest)));
20642
+ loadEnvFiles(path44.dirname(path44.resolve(args.manifest)));
20297
20643
  loadEnvFiles(process.cwd());
20298
20644
  }
20299
20645
  async function runUnzoom(args) {
@@ -20359,19 +20705,19 @@ async function runFont(args) {
20359
20705
  }
20360
20706
 
20361
20707
  // src/cli/commands/quality.ts
20362
- import path44 from "path";
20363
- import { readFile as readFile30 } from "fs/promises";
20708
+ import path46 from "path";
20709
+ import { readFile as readFile31 } from "fs/promises";
20364
20710
 
20365
20711
  // 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";
20712
+ import path45 from "path";
20713
+ import { existsSync as existsSync34 } from "fs";
20714
+ import { readFile as readFile30 } from "fs/promises";
20715
+ import { z as z13 } from "zod";
20370
20716
  var SHA256_RE = /^[0-9a-f]{64}$/;
20371
20717
  var HEX_RE = /^#[0-9a-f]{6}$/;
20372
20718
  var MAX_REDMEAN_DISTANCE = 765;
20373
- var QualityPathSchema = z12.string().min(1).refine(
20374
- (value) => !path43.posix.isAbsolute(value) && !value.includes("\\"),
20719
+ var QualityPathSchema = z13.string().min(1).refine(
20720
+ (value) => !path45.posix.isAbsolute(value) && !value.includes("\\"),
20375
20721
  "expected a portable relative path"
20376
20722
  );
20377
20723
  var DEFAULT_QUALITY_TOLERANCES = {
@@ -20384,53 +20730,53 @@ var DEFAULT_QUALITY_TOLERANCES = {
20384
20730
  maxEdgeContrastDrop: 0.03,
20385
20731
  maxIsolatedPixelIncrease: 5e-3
20386
20732
  };
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)
20733
+ var QualityToleranceSchema = z13.object({
20734
+ requireExactHash: z13.boolean(),
20735
+ maxColorCountIncrease: z13.number().int().min(0),
20736
+ maxNewColors: z13.number().int().min(0),
20737
+ maxTransparencyDelta: z13.number().min(0).max(1),
20738
+ maxPartialAlphaIncrease: z13.number().min(0).max(1),
20739
+ maxEdgeDensityDelta: z13.number().min(0).max(1),
20740
+ maxEdgeContrastDrop: z13.number().min(0).max(1),
20741
+ maxIsolatedPixelIncrease: z13.number().min(0).max(1)
20396
20742
  }).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)
20743
+ var ImageQualityMetricsSchema = z13.object({
20744
+ sha256: z13.string().regex(SHA256_RE),
20745
+ width: z13.number().int().min(1),
20746
+ height: z13.number().int().min(1),
20747
+ colorCount: z13.number().int().min(0),
20748
+ colors: z13.array(z13.string().regex(HEX_RE)),
20749
+ transparency: z13.number().min(0).max(1),
20750
+ partialAlpha: z13.number().min(0).max(1),
20751
+ edgeDensity: z13.number().min(0).max(1),
20752
+ meanEdgeContrast: z13.number().min(0).max(1),
20753
+ isolatedPixelRatio: z13.number().min(0).max(1)
20408
20754
  }).strict();
20409
- var QualityBaselineCaseSchema = z12.object({
20410
- id: z12.string().regex(/^[a-z0-9][a-z0-9/_-]*$/),
20755
+ var QualityBaselineCaseSchema = z13.object({
20756
+ id: z13.string().regex(/^[a-z0-9][a-z0-9/_-]*$/),
20411
20757
  file: QualityPathSchema,
20412
- record: z12.object({
20758
+ record: z13.object({
20413
20759
  path: QualityPathSchema,
20414
- sha256: z12.string().regex(SHA256_RE)
20760
+ sha256: z13.string().regex(SHA256_RE)
20415
20761
  }).strict().optional(),
20416
20762
  expected: ImageQualityMetricsSchema,
20417
20763
  tolerances: QualityToleranceSchema
20418
20764
  }).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)
20765
+ var QualityBaselineSchema = z13.object({
20766
+ $schema: z13.string().url().optional(),
20767
+ format: z13.literal("pixelkiln-quality-baseline"),
20768
+ schemaVersion: z13.literal(1),
20769
+ cases: z13.array(QualityBaselineCaseSchema).min(1)
20424
20770
  }).strict().superRefine((baseline, context) => {
20425
20771
  const ids = baseline.cases.map((entry) => entry.id);
20426
20772
  if (new Set(ids).size !== ids.length) {
20427
- context.addIssue({ code: z12.ZodIssueCode.custom, path: ["cases"], message: "duplicate case id" });
20773
+ context.addIssue({ code: z13.ZodIssueCode.custom, path: ["cases"], message: "duplicate case id" });
20428
20774
  }
20429
20775
  });
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(),
20776
+ var QualityInputSchema = z13.object({
20777
+ id: z13.string().regex(/^[a-z0-9][a-z0-9/_-]*$/),
20778
+ path: z13.string().min(1),
20779
+ record: z13.string().min(1).optional(),
20434
20780
  tolerances: QualityToleranceSchema.partial().optional()
20435
20781
  }).strict();
20436
20782
  function message4(error) {
@@ -20518,10 +20864,10 @@ function measureDecodedImageQuality(png, digest2) {
20518
20864
  };
20519
20865
  }
20520
20866
  async function measureImageQuality(file) {
20521
- const absolute = path43.resolve(file);
20867
+ const absolute = path45.resolve(file);
20522
20868
  let bytes;
20523
20869
  try {
20524
- bytes = await readFile29(absolute);
20870
+ bytes = await readFile30(absolute);
20525
20871
  } catch (error) {
20526
20872
  throw new Error(`Cannot read quality image ${absolute}: ${message4(error)}`, { cause: error });
20527
20873
  }
@@ -20532,7 +20878,7 @@ async function measureImageQuality(file) {
20532
20878
  }
20533
20879
  }
20534
20880
  function resolveQualityInputs(raw, inputsFilePath) {
20535
- const parsed = z12.array(QualityInputSchema).min(1).safeParse(raw);
20881
+ const parsed = z13.array(QualityInputSchema).min(1).safeParse(raw);
20536
20882
  if (!parsed.success) {
20537
20883
  throw new Error(
20538
20884
  "--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 +20886,11 @@ function resolveQualityInputs(raw, inputsFilePath) {
20540
20886
  }
20541
20887
  const ids = parsed.data.map((entry) => entry.id);
20542
20888
  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));
20889
+ const root = path45.dirname(path45.resolve(inputsFilePath));
20544
20890
  return parsed.data.map((entry) => ({
20545
20891
  id: entry.id,
20546
- file: path43.resolve(root, entry.path),
20547
- ...entry.record ? { record: path43.resolve(root, entry.record) } : {},
20892
+ file: path45.resolve(root, entry.path),
20893
+ ...entry.record ? { record: path45.resolve(root, entry.record) } : {},
20548
20894
  tolerances: QualityToleranceSchema.parse({
20549
20895
  ...DEFAULT_QUALITY_TOLERANCES,
20550
20896
  ...entry.tolerances
@@ -20552,7 +20898,7 @@ function resolveQualityInputs(raw, inputsFilePath) {
20552
20898
  }));
20553
20899
  }
20554
20900
  function portableRelative2(from, to) {
20555
- return path43.relative(from, path43.resolve(to)).split(path43.sep).join("/") || ".";
20901
+ return path45.relative(from, path45.resolve(to)).split(path45.sep).join("/") || ".";
20556
20902
  }
20557
20903
  async function verifyRecordForImage(recordPath, imagePath) {
20558
20904
  const verification = await verifyArtifactBundle(recordPath);
@@ -20567,20 +20913,20 @@ async function verifyRecordForImage(recordPath, imagePath) {
20567
20913
  if (record.kind !== "refine" || options === null || typeof options !== "object" || options.schema !== "pixelkiln-quality") {
20568
20914
  throw new Error(`Quality record ${recordPath} is not a PixelKiln refinement record.`);
20569
20915
  }
20570
- const outputs = record.outputs.map((output) => path43.resolve(path43.dirname(recordPath), output.path));
20571
- if (!outputs.includes(path43.resolve(imagePath))) {
20916
+ const outputs = record.outputs.map((output) => path45.resolve(path45.dirname(recordPath), output.path));
20917
+ if (!outputs.includes(path45.resolve(imagePath))) {
20572
20918
  throw new Error(`Quality record ${recordPath} does not own ${imagePath}.`);
20573
20919
  }
20574
20920
  }
20575
20921
  async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
20576
20922
  if (!inputs.length) throw new Error("Cannot snapshot an empty quality baseline.");
20577
- const absolute = path43.resolve(baselinePath);
20923
+ const absolute = path45.resolve(baselinePath);
20578
20924
  for (const input of inputs) {
20579
- if (path43.resolve(input.file) === absolute || input.record && path43.resolve(input.record) === absolute) {
20925
+ if (path45.resolve(input.file) === absolute || input.record && path45.resolve(input.record) === absolute) {
20580
20926
  throw new Error(`Quality baseline output would overwrite an input: ${absolute}.`);
20581
20927
  }
20582
20928
  }
20583
- const root = path43.dirname(absolute);
20929
+ const root = path45.dirname(absolute);
20584
20930
  const cases = [];
20585
20931
  for (const input of [...inputs].sort((a, b) => a.id.localeCompare(b.id))) {
20586
20932
  const expected = await measureImageQuality(input.file);
@@ -20607,8 +20953,8 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
20607
20953
  cases
20608
20954
  });
20609
20955
  const data = Buffer.from(JSON.stringify(baseline, null, 2) + "\n");
20610
- if (existsSync33(absolute) && !options.force) {
20611
- const current = await readFile29(absolute);
20956
+ if (existsSync34(absolute) && !options.force) {
20957
+ const current = await readFile30(absolute);
20612
20958
  if (!current.equals(data)) {
20613
20959
  throw new OverwriteRefusedError(
20614
20960
  `Quality baseline already exists with different content: ${absolute}. Pass --force to replace it.`,
@@ -20620,10 +20966,10 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
20620
20966
  return { baseline, path: absolute, changed: result.changed.length > 0 };
20621
20967
  }
20622
20968
  async function readQualityBaseline(baselinePath) {
20623
- const absolute = path43.resolve(baselinePath);
20969
+ const absolute = path45.resolve(baselinePath);
20624
20970
  let raw;
20625
20971
  try {
20626
- raw = JSON.parse(await readFile29(absolute, "utf8"));
20972
+ raw = JSON.parse(await readFile30(absolute, "utf8"));
20627
20973
  } catch (error) {
20628
20974
  throw new Error(`Could not read quality baseline ${absolute}: ${message4(error)}`, { cause: error });
20629
20975
  }
@@ -20693,12 +21039,12 @@ function regressionViolations(expected, actual, tolerances) {
20693
21039
  return { violations, warnings };
20694
21040
  }
20695
21041
  async function checkQualityBaseline(baselinePath) {
20696
- const absolute = path43.resolve(baselinePath);
21042
+ const absolute = path45.resolve(baselinePath);
20697
21043
  const baseline = await readQualityBaseline(absolute);
20698
- const root = path43.dirname(absolute);
21044
+ const root = path45.dirname(absolute);
20699
21045
  const cases = [];
20700
21046
  for (const entry of baseline.cases) {
20701
- const file = path43.resolve(root, entry.file);
21047
+ const file = path45.resolve(root, entry.file);
20702
21048
  let actual = null;
20703
21049
  const violations = [];
20704
21050
  const warnings = [];
@@ -20711,7 +21057,7 @@ async function checkQualityBaseline(baselinePath) {
20711
21057
  violations.push(message4(error));
20712
21058
  }
20713
21059
  if (entry.record) {
20714
- const recordPath = path43.resolve(root, entry.record.path);
21060
+ const recordPath = path45.resolve(root, entry.record.path);
20715
21061
  try {
20716
21062
  const recordHash = await sha256File(recordPath);
20717
21063
  if (recordHash !== entry.record.sha256) violations.push("quality record hash changed");
@@ -20753,15 +21099,15 @@ async function runQuality(args) {
20753
21099
  if (!args.out) throw new Error("quality snapshot needs --out <pixelkiln.quality.json>.");
20754
21100
  let raw;
20755
21101
  try {
20756
- raw = JSON.parse(await readFile30(path44.resolve(args.inputs), "utf8"));
21102
+ raw = JSON.parse(await readFile31(path46.resolve(args.inputs), "utf8"));
20757
21103
  } catch (error) {
20758
21104
  throw new Error(
20759
- `Could not read quality inputs ${path44.resolve(args.inputs)}: ${error instanceof Error ? error.message : String(error)}`,
21105
+ `Could not read quality inputs ${path46.resolve(args.inputs)}: ${error instanceof Error ? error.message : String(error)}`,
20760
21106
  { cause: error }
20761
21107
  );
20762
21108
  }
20763
21109
  const inputs = resolveQualityInputs(raw, args.inputs);
20764
- if (path44.resolve(args.inputs) === path44.resolve(args.out)) {
21110
+ if (path46.resolve(args.inputs) === path46.resolve(args.out)) {
20765
21111
  throw new Error("quality snapshot --out must not overwrite its --inputs file.");
20766
21112
  }
20767
21113
  const result = await snapshotQualityBaseline(inputs, args.out, { force: args.force });
@@ -20774,7 +21120,7 @@ async function runQuality(args) {
20774
21120
  }, null, 2));
20775
21121
  } else {
20776
21122
  log(
20777
- ` ${result.changed ? "wrote" : "unchanged"} ${path44.relative(process.cwd(), result.path)} (${result.baseline.cases.length} case(s))`
21123
+ ` ${result.changed ? "wrote" : "unchanged"} ${path46.relative(process.cwd(), result.path)} (${result.baseline.cases.length} case(s))`
20778
21124
  );
20779
21125
  log(` Review the tolerances, commit the baseline, then run quality check in CI.`);
20780
21126
  }
@@ -20800,54 +21146,54 @@ async function runQuality(args) {
20800
21146
  }
20801
21147
 
20802
21148
  // src/cli/commands/recipe.ts
20803
- import path46 from "path";
21149
+ import path48 from "path";
20804
21150
 
20805
21151
  // 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";
21152
+ import path47 from "path";
21153
+ import { existsSync as existsSync35 } from "fs";
21154
+ import { readdir as readdir3, readFile as readFile32 } from "fs/promises";
20809
21155
  import { fileURLToPath as fileURLToPath2 } from "url";
20810
- import { z as z13 } from "zod";
21156
+ import { z as z14 } from "zod";
20811
21157
  var SHA256_RE2 = /^[0-9a-f]{64}$/;
20812
21158
  var RECIPE_ID_RE = /^[a-z0-9][a-z0-9-]*\/[a-z0-9][a-z0-9-]*$/;
20813
21159
  var VERSION_RE = /^\d+\.\d+\.\d+$/;
20814
21160
  var RECIPE_FILE = "pixelkiln.recipe.json";
20815
21161
  var RECIPE_DIR_TOKEN = "{{recipeDir}}";
20816
- var PortablePathSchema = z13.string().min(1).refine((value) => {
20817
- if (value.includes("\\") || path45.posix.isAbsolute(value)) return false;
21162
+ var PortablePathSchema = z14.string().min(1).refine((value) => {
21163
+ if (value.includes("\\") || path47.posix.isAbsolute(value)) return false;
20818
21164
  const parts = value.split("/");
20819
21165
  return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
20820
21166
  }, "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)
21167
+ var RecipeBindingSchema = z14.object({
21168
+ nodeId: z14.string().min(1),
21169
+ input: z14.string().min(1)
20824
21170
  }).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({
21171
+ var RecipeSchema = z14.object({
21172
+ $schema: z14.string().url().optional(),
21173
+ format: z14.literal("pixelkiln-recipe"),
21174
+ schemaVersion: z14.literal(1),
21175
+ id: z14.string().regex(RECIPE_ID_RE, "expected provider/name"),
21176
+ version: z14.string().regex(VERSION_RE, "expected x.y.z"),
21177
+ provider: z14.string().regex(/^[a-z0-9][a-z0-9-]*$/),
21178
+ summary: z14.string().min(1).max(240),
21179
+ files: z14.array(z14.object({
20834
21180
  path: PortablePathSchema,
20835
- role: z13.enum(["workflow", "reference"]),
20836
- sha256: z13.string().regex(SHA256_RE2)
21181
+ role: z14.enum(["workflow", "reference"]),
21182
+ sha256: z14.string().regex(SHA256_RE2)
20837
21183
  }).strict()).default([]),
20838
- models: z13.array(z13.object({
21184
+ models: z14.array(z14.object({
20839
21185
  path: PortablePathSchema,
20840
- sha256: z13.string().regex(SHA256_RE2),
20841
- source: z13.string().url(),
20842
- license: z13.string().min(1)
21186
+ sha256: z14.string().regex(SHA256_RE2),
21187
+ source: z14.string().url(),
21188
+ license: z14.string().min(1)
20843
21189
  }).strict()).default([]),
20844
- styleId: z13.string().regex(/^[a-z0-9][a-z0-9-]*$/),
21190
+ styleId: z14.string().regex(/^[a-z0-9][a-z0-9-]*$/),
20845
21191
  style: StyleSchema,
20846
- workflow: z13.object({
21192
+ workflow: z14.object({
20847
21193
  path: PortablePathSchema,
20848
- outputNodeId: z13.string().min(1),
20849
- numImages: z13.number().int().min(1).max(16),
20850
- bindings: z13.object({
21194
+ outputNodeId: z14.string().min(1),
21195
+ numImages: z14.number().int().min(1).max(16),
21196
+ bindings: z14.object({
20851
21197
  prompt: RecipeBindingSchema,
20852
21198
  width: RecipeBindingSchema.optional(),
20853
21199
  height: RecipeBindingSchema.optional(),
@@ -20858,67 +21204,67 @@ var RecipeSchema = z13.object({
20858
21204
  strength: RecipeBindingSchema.optional()
20859
21205
  }).strict()
20860
21206
  }).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)
21207
+ quality: z14.object({
21208
+ stage: z14.enum(["composition-source", "production-candidate"]),
21209
+ workingCanvas: z14.object({
21210
+ width: z14.number().int().min(16),
21211
+ height: z14.number().int().min(16)
20866
21212
  }).strict(),
20867
- recommendedNativeSize: z13.object({
20868
- min: z13.number().int().min(1),
20869
- max: z13.number().int().min(1)
21213
+ recommendedNativeSize: z14.object({
21214
+ min: z14.number().int().min(1),
21215
+ max: z14.number().int().min(1)
20870
21216
  }).strict(),
20871
- paletteColors: z13.object({
20872
- min: z13.number().int().min(1),
20873
- max: z13.number().int().min(1)
21217
+ paletteColors: z14.object({
21218
+ min: z14.number().int().min(1),
21219
+ max: z14.number().int().min(1)
20874
21220
  }).strict(),
20875
- background: z13.enum(["opaque", "transparent", "full-bleed"]),
20876
- checks: z13.array(z13.enum([
21221
+ background: z14.enum(["opaque", "transparent", "full-bleed"]),
21222
+ checks: z14.array(z14.enum([
20877
21223
  "prompt-coverage",
20878
21224
  "native-grid",
20879
21225
  "final-palette",
20880
21226
  "alpha-edge",
20881
21227
  "human-1x"
20882
21228
  ])).min(1),
20883
- notes: z13.array(z13.string().min(1)).default([])
21229
+ notes: z14.array(z14.string().min(1)).default([])
20884
21230
  }).strict(),
20885
- integrity: z13.object({
20886
- algorithm: z13.literal("sha256"),
20887
- digest: z13.string().regex(SHA256_RE2)
21231
+ integrity: z14.object({
21232
+ algorithm: z14.literal("sha256"),
21233
+ digest: z14.string().regex(SHA256_RE2)
20888
21234
  }).strict()
20889
21235
  }).strict().superRefine((recipe, context) => {
20890
21236
  if (recipe.style.provider && recipe.style.provider !== recipe.provider) {
20891
21237
  context.addIssue({
20892
- code: z13.ZodIssueCode.custom,
21238
+ code: z14.ZodIssueCode.custom,
20893
21239
  path: ["style", "provider"],
20894
21240
  message: "must match the recipe provider"
20895
21241
  });
20896
21242
  }
20897
21243
  const filePaths = recipe.files.map((file) => file.path);
20898
21244
  if (new Set(filePaths).size !== filePaths.length) {
20899
- context.addIssue({ code: z13.ZodIssueCode.custom, path: ["files"], message: "duplicate file path" });
21245
+ context.addIssue({ code: z14.ZodIssueCode.custom, path: ["files"], message: "duplicate file path" });
20900
21246
  }
20901
21247
  const modelPaths = recipe.models.map((model) => model.path);
20902
21248
  if (new Set(modelPaths).size !== modelPaths.length) {
20903
- context.addIssue({ code: z13.ZodIssueCode.custom, path: ["models"], message: "duplicate model path" });
21249
+ context.addIssue({ code: z14.ZodIssueCode.custom, path: ["models"], message: "duplicate model path" });
20904
21250
  }
20905
21251
  if (filePaths.includes(RECIPE_FILE)) {
20906
21252
  context.addIssue({
20907
- code: z13.ZodIssueCode.custom,
21253
+ code: z14.ZodIssueCode.custom,
20908
21254
  path: ["files"],
20909
21255
  message: `${RECIPE_FILE} cannot hash itself`
20910
21256
  });
20911
21257
  }
20912
21258
  if (recipe.quality.recommendedNativeSize.min > recipe.quality.recommendedNativeSize.max) {
20913
21259
  context.addIssue({
20914
- code: z13.ZodIssueCode.custom,
21260
+ code: z14.ZodIssueCode.custom,
20915
21261
  path: ["quality", "recommendedNativeSize"],
20916
21262
  message: "min cannot exceed max"
20917
21263
  });
20918
21264
  }
20919
21265
  if (recipe.quality.paletteColors.min > recipe.quality.paletteColors.max) {
20920
21266
  context.addIssue({
20921
- code: z13.ZodIssueCode.custom,
21267
+ code: z14.ZodIssueCode.custom,
20922
21268
  path: ["quality", "paletteColors"],
20923
21269
  message: "min cannot exceed max"
20924
21270
  });
@@ -20927,7 +21273,7 @@ var RecipeSchema = z13.object({
20927
21273
  (file) => file.path === recipe.workflow.path && file.role === "workflow"
20928
21274
  )) {
20929
21275
  context.addIssue({
20930
- code: z13.ZodIssueCode.custom,
21276
+ code: z14.ZodIssueCode.custom,
20931
21277
  path: ["workflow", "path"],
20932
21278
  message: "must name a file whose role is workflow"
20933
21279
  });
@@ -20935,7 +21281,7 @@ var RecipeSchema = z13.object({
20935
21281
  if (recipe.provider === "comfyui") {
20936
21282
  if (!recipe.workflow) {
20937
21283
  context.addIssue({
20938
- code: z13.ZodIssueCode.custom,
21284
+ code: z14.ZodIssueCode.custom,
20939
21285
  path: ["workflow"],
20940
21286
  message: "ComfyUI recipes require workflow metadata"
20941
21287
  });
@@ -20944,7 +21290,7 @@ var RecipeSchema = z13.object({
20944
21290
  const options = recipe.style.providerOptions.comfyui;
20945
21291
  if (!options || typeof options !== "object" || Array.isArray(options)) {
20946
21292
  context.addIssue({
20947
- code: z13.ZodIssueCode.custom,
21293
+ code: z14.ZodIssueCode.custom,
20948
21294
  path: ["style", "providerOptions", "comfyui"],
20949
21295
  message: "ComfyUI recipes require ComfyUI provider options"
20950
21296
  });
@@ -20958,7 +21304,7 @@ var RecipeSchema = z13.object({
20958
21304
  };
20959
21305
  if (JSON.stringify(canonical3(options)) !== JSON.stringify(canonical3(expected))) {
20960
21306
  context.addIssue({
20961
- code: z13.ZodIssueCode.custom,
21307
+ code: z14.ZodIssueCode.custom,
20962
21308
  path: ["style", "providerOptions", "comfyui"],
20963
21309
  message: "must match workflow metadata and use {{recipeDir}} for workflowFile"
20964
21310
  });
@@ -20990,10 +21336,10 @@ function message5(error) {
20990
21336
  return error instanceof Error ? error.message : String(error);
20991
21337
  }
20992
21338
  async function readRecipeFile(recipePath, bundled) {
20993
- const absolute = path45.resolve(recipePath);
21339
+ const absolute = path47.resolve(recipePath);
20994
21340
  let raw;
20995
21341
  try {
20996
- raw = JSON.parse(await readFile31(absolute, "utf8"));
21342
+ raw = JSON.parse(await readFile32(absolute, "utf8"));
20997
21343
  } catch (error) {
20998
21344
  throw new Error(`Could not read recipe ${absolute}: ${message5(error)}`, { cause: error });
20999
21345
  }
@@ -21004,13 +21350,13 @@ async function readRecipeFile(recipePath, bundled) {
21004
21350
  ` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
21005
21351
  );
21006
21352
  }
21007
- return { recipe: parsed.data, authored: raw, path: absolute, dir: path45.dirname(absolute), bundled };
21353
+ return { recipe: parsed.data, authored: raw, path: absolute, dir: path47.dirname(absolute), bundled };
21008
21354
  }
21009
21355
  async function walkRecipeFiles(root) {
21010
- if (!existsSync34(root)) return [];
21356
+ if (!existsSync35(root)) return [];
21011
21357
  const out = [];
21012
21358
  for (const entry of await readdir3(root, { withFileTypes: true })) {
21013
- const resolved = path45.join(root, entry.name);
21359
+ const resolved = path47.join(root, entry.name);
21014
21360
  if (entry.isDirectory()) out.push(...await walkRecipeFiles(resolved));
21015
21361
  else if (entry.isFile() && entry.name === RECIPE_FILE) out.push(resolved);
21016
21362
  }
@@ -21021,7 +21367,7 @@ function bundledRecipeRoot() {
21021
21367
  }
21022
21368
  async function listBundledRecipes(root = bundledRecipeRoot()) {
21023
21369
  const recipes = await Promise.all(
21024
- (await walkRecipeFiles(path45.resolve(root))).map((file) => readRecipeFile(file, true))
21370
+ (await walkRecipeFiles(path47.resolve(root))).map((file) => readRecipeFile(file, true))
21025
21371
  );
21026
21372
  return recipes.sort(
21027
21373
  (a, b) => a.recipe.id.localeCompare(b.recipe.id) || compareVersions(b.recipe.version, a.recipe.version)
@@ -21044,9 +21390,9 @@ function parseSelector(selector) {
21044
21390
  return { id: match[1], version: match[2] };
21045
21391
  }
21046
21392
  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);
21393
+ const local = path47.resolve(target);
21394
+ if (existsSync35(local)) {
21395
+ const recipePath = path47.basename(local) === RECIPE_FILE ? local : path47.join(local, RECIPE_FILE);
21050
21396
  return readRecipeFile(recipePath, false);
21051
21397
  }
21052
21398
  if (target.startsWith(".") || target.startsWith("/") || target.includes("\\")) {
@@ -21060,8 +21406,8 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
21060
21406
  return matches[0];
21061
21407
  }
21062
21408
  async function verifyFile(root, file) {
21063
- const absolute = path45.resolve(root, ...file.path.split("/"));
21064
- if (!existsSync34(absolute)) {
21409
+ const absolute = path47.resolve(root, ...file.path.split("/"));
21410
+ if (!existsSync35(absolute)) {
21065
21411
  return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
21066
21412
  }
21067
21413
  const actualSha256 = await sha256File(absolute);
@@ -21076,7 +21422,7 @@ async function verifyRecipe(target, options = {}) {
21076
21422
  const loaded = await resolveRecipe(target, options.bundledRoot);
21077
21423
  const actualIntegrity = recipeDigest(loaded.authored);
21078
21424
  const files = await Promise.all(loaded.recipe.files.map((file) => verifyFile(loaded.dir, file)));
21079
- const modelRoot = options.modelRoot ? path45.resolve(options.modelRoot) : null;
21425
+ const modelRoot = options.modelRoot ? path47.resolve(options.modelRoot) : null;
21080
21426
  const models = modelRoot ? await Promise.all(loaded.recipe.models.map(async (model) => ({
21081
21427
  ...await verifyFile(modelRoot, model),
21082
21428
  source: model.source,
@@ -21110,7 +21456,7 @@ async function verifyRecipe(target, options = {}) {
21110
21456
  };
21111
21457
  }
21112
21458
  function renderStyle(style, destination, cwd) {
21113
- const portableDir = path45.relative(cwd, destination).split(path45.sep).join("/") || ".";
21459
+ const portableDir = path47.relative(cwd, destination).split(path47.sep).join("/") || ".";
21114
21460
  const replace = (value) => {
21115
21461
  if (typeof value === "string") return value.replaceAll(RECIPE_DIR_TOKEN, portableDir);
21116
21462
  if (Array.isArray(value)) return value.map(replace);
@@ -21122,25 +21468,25 @@ function renderStyle(style, destination, cwd) {
21122
21468
  return replace(style);
21123
21469
  }
21124
21470
  async function installRecipe(target, options = {}) {
21125
- const cwd = path45.resolve(options.cwd ?? process.cwd());
21471
+ const cwd = path47.resolve(options.cwd ?? process.cwd());
21126
21472
  const loaded = await resolveRecipe(target, options.bundledRoot);
21127
21473
  const verification = await verifyRecipe(loaded.path);
21128
21474
  if (!verification.ok) {
21129
21475
  throw new Error(`Recipe ${loaded.recipe.id}@${loaded.recipe.version} failed integrity verification.`);
21130
21476
  }
21131
- const destination = options.out ? path45.resolve(cwd, options.out) : path45.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
21477
+ const destination = options.out ? path47.resolve(cwd, options.out) : path47.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
21132
21478
  const sources = [
21133
21479
  { path: RECIPE_FILE, source: loaded.path },
21134
- ...loaded.recipe.files.map((file) => ({ path: file.path, source: path45.join(loaded.dir, ...file.path.split("/")) }))
21480
+ ...loaded.recipe.files.map((file) => ({ path: file.path, source: path47.join(loaded.dir, ...file.path.split("/")) }))
21135
21481
  ];
21136
21482
  const files = await Promise.all(sources.map(async (file) => ({
21137
- path: path45.join(destination, ...file.path.split("/")),
21138
- data: await readFile31(file.source)
21483
+ path: path47.join(destination, ...file.path.split("/")),
21484
+ data: await readFile32(file.source)
21139
21485
  })));
21140
21486
  if (!options.force) {
21141
21487
  for (const file of files) {
21142
- if (!existsSync34(file.path)) continue;
21143
- const current = await readFile31(file.path);
21488
+ if (!existsSync35(file.path)) continue;
21489
+ const current = await readFile32(file.path);
21144
21490
  if (!current.equals(file.data)) {
21145
21491
  throw new OverwriteRefusedError(
21146
21492
  `Recipe destination has local changes: ${file.path}. Choose another --out or pass --force to replace declared recipe files.`,
@@ -21234,14 +21580,14 @@ async function runRecipe(args) {
21234
21580
  }, null, 2));
21235
21581
  } else {
21236
21582
  log(` installed ${result.recipe.id}@${result.recipe.version}`);
21237
- log(` ${path46.relative(process.cwd(), result.destination) || "."}`);
21583
+ log(` ${path48.relative(process.cwd(), result.destination) || "."}`);
21238
21584
  log(`
21239
21585
  Add this entry under your manifest's styles object:`);
21240
21586
  log(JSON.stringify({ [result.styleId]: result.style }, null, 2));
21241
21587
  if (result.recipe.models.length) {
21242
21588
  log(`
21243
21589
  Models are not downloaded automatically. Verify them with:`);
21244
- log(` pixelkiln recipe verify ${path46.relative(process.cwd(), result.destination)} --model-root <ComfyUI/models>`);
21590
+ log(` pixelkiln recipe verify ${path48.relative(process.cwd(), result.destination)} --model-root <ComfyUI/models>`);
21245
21591
  }
21246
21592
  }
21247
21593
  return;
@@ -21249,7 +21595,7 @@ async function runRecipe(args) {
21249
21595
  }
21250
21596
 
21251
21597
  // src/cli/commands/refine.ts
21252
- import path47 from "path";
21598
+ import path49 from "path";
21253
21599
  async function runRefine(args) {
21254
21600
  if (args.from || args.subcommand === "approve") {
21255
21601
  if (!args.from) {
@@ -21279,12 +21625,12 @@ async function runRefine(args) {
21279
21625
  ` recovered ${result3.detection.columns}x${result3.detection.rows} native grid (${result3.detection.confidence}: ${result3.detection.consensus})`
21280
21626
  );
21281
21627
  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)}`);
21628
+ log(` wrote ${path49.relative(process.cwd(), result3.output)}`);
21629
+ log(` quality record: ${path49.relative(process.cwd(), result3.record)}`);
21284
21630
  log(`
21285
21631
  Automated checks passed. Human 1\xD7 review is still required:`);
21286
21632
  log(
21287
- ` pixelkiln refine approve --from ${path47.relative(process.cwd(), result3.record)} --reviewer "Your Name"`
21633
+ ` pixelkiln refine approve --from ${path49.relative(process.cwd(), result3.record)} --reviewer "Your Name"`
21288
21634
  );
21289
21635
  }
21290
21636
  return;
@@ -21312,7 +21658,7 @@ async function runRefine(args) {
21312
21658
  if (args.json) log(JSON.stringify(result3, null, 2));
21313
21659
  else {
21314
21660
  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()}`
21661
+ result3.options.frameSet ? ` approved ${result3.outputs.length} frames in ${path49.relative(process.cwd(), result3.record)} by ${args.reviewer.trim()}` : ` approved ${path49.relative(process.cwd(), result3.output)} by ${args.reviewer.trim()}`
21316
21662
  );
21317
21663
  }
21318
21664
  return;
@@ -21321,7 +21667,7 @@ async function runRefine(args) {
21321
21667
  if (args.json) {
21322
21668
  log(JSON.stringify(result2, null, 2));
21323
21669
  } else {
21324
- log(` ${result2.safe ? "release-ready" : "not release-ready"}: ${path47.relative(process.cwd(), result2.output || result2.record)}`);
21670
+ log(` ${result2.safe ? "release-ready" : "not release-ready"}: ${path49.relative(process.cwd(), result2.output || result2.record)}`);
21325
21671
  for (const reason of result2.reasons) log(` ${reason}`);
21326
21672
  }
21327
21673
  if (!result2.safe) process.exitCode = 1;
@@ -21367,7 +21713,7 @@ async function runRefine(args) {
21367
21713
  log(` ${item.state.padEnd(16)} ${item.key} ${item.reason}`);
21368
21714
  if (item.state === "needs-approval") {
21369
21715
  log(
21370
- ` pixelkiln refine approve --from ${path47.relative(process.cwd(), item.record)} --reviewer "Your Name"`
21716
+ ` pixelkiln refine approve --from ${path49.relative(process.cwd(), item.record)} --reviewer "Your Name"`
21371
21717
  );
21372
21718
  }
21373
21719
  }
@@ -21378,9 +21724,9 @@ async function runRefine(args) {
21378
21724
  }
21379
21725
 
21380
21726
  // 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";
21727
+ import path50 from "path";
21728
+ import { existsSync as existsSync36 } from "fs";
21729
+ import { readFile as readFile33, writeFile as writeFile19 } from "fs/promises";
21384
21730
 
21385
21731
  // src/skeleton-preview.ts
21386
21732
  var RIGHT = [255, 140, 40, 255];
@@ -21491,12 +21837,12 @@ var Canvas = class {
21491
21837
  async function runEstimateSkeleton(args) {
21492
21838
  const image = args.target;
21493
21839
  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) {
21840
+ if (args.out && existsSync36(path50.resolve(args.out)) && !args.force) {
21495
21841
  throw new UsageError(`${args.out} already exists; pass --force to replace it`);
21496
21842
  }
21497
- loadEnvFiles(path48.dirname(path48.resolve(args.manifest)));
21843
+ loadEnvFiles(path50.dirname(path50.resolve(args.manifest)));
21498
21844
  loadEnvFiles(process.cwd());
21499
- const bytes = await readFile32(path48.resolve(image));
21845
+ const bytes = await readFile33(path50.resolve(image));
21500
21846
  const metadata = imageMetadata(bytes);
21501
21847
  if (!metadata) throw new UsageError(`${image} is not a readable PNG or JPEG`);
21502
21848
  const sizeProblem = estimateSkeletonSizeProblem(metadata.width, metadata.height);
@@ -21508,7 +21854,7 @@ async function runEstimateSkeleton(args) {
21508
21854
  const frames = args.frames ?? DEFAULT_SCAFFOLD_FRAMES;
21509
21855
  const json = JSON.stringify(scaffoldSkeletonSet(res.keypoints, frames), null, 2);
21510
21856
  if (args.out) {
21511
- await writeFile18(path48.resolve(args.out), json + "\n");
21857
+ await writeFile19(path50.resolve(args.out), json + "\n");
21512
21858
  log(` wrote ${args.out}: the estimated pose and ${frames} frames to edit`);
21513
21859
  log(` preview it with: pixelkiln skeleton-preview ${args.out} --from ${image}`);
21514
21860
  } else {
@@ -21521,8 +21867,8 @@ async function runSkeletonPreview(args) {
21521
21867
  let sourceFile;
21522
21868
  let label;
21523
21869
  if (target.endsWith(".json")) {
21524
- keypointsFile = path48.resolve(target);
21525
- sourceFile = args.from ? path48.resolve(args.from) : void 0;
21870
+ keypointsFile = path50.resolve(target);
21871
+ sourceFile = args.from ? path50.resolve(args.from) : void 0;
21526
21872
  label = target;
21527
21873
  } else {
21528
21874
  const loaded = await loadManifest(args.manifest);
@@ -21534,31 +21880,31 @@ async function runSkeletonPreview(args) {
21534
21880
  );
21535
21881
  }
21536
21882
  keypointsFile = spec.revision.keypointsFile;
21537
- sourceFile = args.from ? path48.resolve(args.from) : spec.revision.sourceFile;
21883
+ sourceFile = args.from ? path50.resolve(args.from) : spec.revision.sourceFile;
21538
21884
  label = `${spec.styleId}/${spec.assetId}`;
21539
21885
  }
21540
- if (!existsSync35(keypointsFile)) throw new UsageError(`Keypoints file does not exist yet: ${keypointsFile}`);
21886
+ if (!existsSync36(keypointsFile)) throw new UsageError(`Keypoints file does not exist yet: ${keypointsFile}`);
21541
21887
  let json;
21542
21888
  try {
21543
- json = JSON.parse(await readFile32(keypointsFile, "utf8"));
21889
+ json = JSON.parse(await readFile33(keypointsFile, "utf8"));
21544
21890
  } catch (error) {
21545
21891
  throw new UsageError(`${keypointsFile} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
21546
21892
  }
21547
21893
  const set = parseSkeletonSet(json, keypointsFile);
21548
21894
  let source;
21549
21895
  if (sourceFile) {
21550
- if (!existsSync35(sourceFile)) {
21896
+ if (!existsSync36(sourceFile)) {
21551
21897
  log(` source image does not exist yet, drawing on a plain ground: ${sourceFile}`);
21552
21898
  } else {
21553
- const bytes = await readFile32(sourceFile);
21899
+ const bytes = await readFile33(sourceFile);
21554
21900
  if (imageMetadata(bytes)?.format !== "png") throw new UsageError(`${sourceFile} must be a PNG to preview over`);
21555
21901
  source = decodePng(bytes);
21556
21902
  }
21557
21903
  }
21558
- const out = path48.resolve(args.out ?? keypointsFile.replace(/\.json$/, "") + ".preview.png");
21559
- await writeFile18(out, renderSkeletonSheet(set, { source }));
21904
+ const out = path50.resolve(args.out ?? keypointsFile.replace(/\.json$/, "") + ".preview.png");
21905
+ await writeFile19(out, renderSkeletonSheet(set, { source }));
21560
21906
  log(` ${label}: ${set.frames.length} frames after the starting pose`);
21561
- const shown = path48.relative(process.cwd(), out);
21907
+ const shown = path50.relative(process.cwd(), out);
21562
21908
  log(` wrote ${shown.startsWith("..") ? out : shown}`);
21563
21909
  }
21564
21910
 
@@ -21604,15 +21950,15 @@ async function runTools(args) {
21604
21950
  }
21605
21951
 
21606
21952
  // src/cli/commands/workspace.ts
21607
- import path49 from "path";
21608
- import { existsSync as existsSync36 } from "fs";
21953
+ import path51 from "path";
21954
+ import { existsSync as existsSync37 } from "fs";
21609
21955
  async function runWorkspace(args) {
21610
- const workspacePath = path49.resolve(args.workspace ?? "pixelkiln.workspace.json");
21611
- const dir = path49.dirname(workspacePath);
21956
+ const workspacePath = path51.resolve(args.workspace ?? "pixelkiln.workspace.json");
21957
+ const dir = path51.dirname(workspacePath);
21612
21958
  if (args.subcommand === "add") {
21613
- const manifestPath = path49.resolve(args.target);
21959
+ const manifestPath = path51.resolve(args.target);
21614
21960
  const loadedTarget = await loadManifest(manifestPath);
21615
- const lockPath = args.explicitLock ? path49.resolve(args.explicitLock) : path49.join(path49.dirname(manifestPath), "pixelkiln.lock.json");
21961
+ const lockPath = args.explicitLock ? path51.resolve(args.explicitLock) : path51.join(path51.dirname(manifestPath), "pixelkiln.lock.json");
21616
21962
  const ws = await loadWorkspace(workspacePath);
21617
21963
  const id = args.name ?? loadedTarget.manifest.name;
21618
21964
  if (ws.projects.some((p) => p.id === id)) {
@@ -21632,10 +21978,10 @@ async function runWorkspace(args) {
21632
21978
  ...args.account ? { account: args.account } : {}
21633
21979
  };
21634
21980
  await saveWorkspace(workspacePath, { version: 1, projects: [...ws.projects, project] });
21635
- log(` registered "${id}" in ${path49.relative(process.cwd(), workspacePath)}`);
21981
+ log(` registered "${id}" in ${path51.relative(process.cwd(), workspacePath)}`);
21636
21982
  log(` manifest: ${project.manifest}`);
21637
21983
  log(` lock: ${project.lock}`);
21638
- if (!existsSync36(lockPath)) {
21984
+ if (!existsSync37(lockPath)) {
21639
21985
  log(
21640
21986
  ` warning: no lockfile there yet; this project contributes no claims until one is generated`
21641
21987
  );
@@ -21644,7 +21990,7 @@ async function runWorkspace(args) {
21644
21990
  }
21645
21991
  if (args.subcommand === "remove") {
21646
21992
  const ws = await loadWorkspace(workspacePath);
21647
- const resolvedTarget = path49.resolve(args.target);
21993
+ const resolvedTarget = path51.resolve(args.target);
21648
21994
  const match = ws.projects.find(
21649
21995
  (p) => p.id === args.target || resolveProject(dir, p).manifestPath === resolvedTarget
21650
21996
  );
@@ -21655,11 +22001,11 @@ async function runWorkspace(args) {
21655
22001
  version: 1,
21656
22002
  projects: ws.projects.filter((p) => p !== match)
21657
22003
  });
21658
- log(` removed "${match.id}" from ${path49.relative(process.cwd(), workspacePath)}`);
22004
+ log(` removed "${match.id}" from ${path51.relative(process.cwd(), workspacePath)}`);
21659
22005
  return;
21660
22006
  }
21661
22007
  if (args.subcommand === "list") {
21662
- if (!existsSync36(workspacePath)) {
22008
+ if (!existsSync37(workspacePath)) {
21663
22009
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
21664
22010
  }
21665
22011
  const ws = await loadWorkspace(workspacePath);
@@ -21667,9 +22013,9 @@ async function runWorkspace(args) {
21667
22013
  if (args.json) {
21668
22014
  log(JSON.stringify({ version: 1, workspace: workspacePath, projects: ws.projects, diagnostics }, null, 2));
21669
22015
  } else if (!ws.projects.length) {
21670
- log(` no projects registered in ${path49.relative(process.cwd(), workspacePath)}`);
22016
+ log(` no projects registered in ${path51.relative(process.cwd(), workspacePath)}`);
21671
22017
  } else {
21672
- log(` ${ws.projects.length} project(s) in ${path49.relative(process.cwd(), workspacePath)}:`);
22018
+ log(` ${ws.projects.length} project(s) in ${path51.relative(process.cwd(), workspacePath)}:`);
21673
22019
  for (const p of ws.projects) {
21674
22020
  log(` ${p.id.padEnd(24)} ${p.manifest.padEnd(40)} (${p.provider}${p.account ? `, ${p.account}` : ""})`);
21675
22021
  }
@@ -21679,7 +22025,7 @@ async function runWorkspace(args) {
21679
22025
  return;
21680
22026
  }
21681
22027
  if (args.subcommand === "status") {
21682
- if (!existsSync36(workspacePath)) {
22028
+ if (!existsSync37(workspacePath)) {
21683
22029
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
21684
22030
  }
21685
22031
  const ws = await loadWorkspace(workspacePath);
@@ -21687,7 +22033,7 @@ async function runWorkspace(args) {
21687
22033
  if (args.json) {
21688
22034
  log(JSON.stringify({ ...report, workspace: workspacePath }, null, 2));
21689
22035
  } else {
21690
- log(` workspace: ${path49.relative(process.cwd(), workspacePath)}`);
22036
+ log(` workspace: ${path51.relative(process.cwd(), workspacePath)}`);
21691
22037
  for (const p of report.projects) {
21692
22038
  if (p.error) {
21693
22039
  log(`
@@ -21741,8 +22087,8 @@ async function runHelp() {
21741
22087
  async function runVersion() {
21742
22088
  for (const up of ["../package.json", "../../package.json"]) {
21743
22089
  const url = new URL(up, import.meta.url);
21744
- if (!existsSync37(url)) continue;
21745
- const pkg = JSON.parse(await readFile33(url, "utf8"));
22090
+ if (!existsSync38(url)) continue;
22091
+ const pkg = JSON.parse(await readFile34(url, "utf8"));
21746
22092
  if (pkg.name === "pixelkiln") {
21747
22093
  log(`${pkg.name} ${pkg.version}`);
21748
22094
  return;