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/index.js CHANGED
@@ -9,6 +9,7 @@ var RevisionModeSchema = z.enum([
9
9
  "outpaint",
10
10
  "reduce-colors",
11
11
  "correct-pixelart",
12
+ "remove-background",
12
13
  "animate",
13
14
  "animate-pixminimax",
14
15
  "animate-skeleton",
@@ -340,7 +341,22 @@ var RevisionSchema = z.object({
340
341
  * `interpolate`: required, the ending keyframe; the parent is the start.
341
342
  */
342
343
  lastFrame: z.string().min(1).optional(),
343
- /** `animate-pixminimax` only: facing direction, used only alongside `enhancePrompt` to hold the sprite's facing. */
344
+ /**
345
+ * `animate`/`animate-pixminimax` only: colour de-flicker sensitivity.
346
+ * Frames whose foreground drifts from the first frame beyond this are
347
+ * corrected toward it; 0 corrects every frame and higher values correct
348
+ * fewer. Omit for PixelLab's default.
349
+ */
350
+ driftThreshold: z.number().min(0).optional(),
351
+ /**
352
+ * `animate-pixminimax` only: one noun phrase for what the parent shows.
353
+ * PixelLab reads it off the image when omitted, and can decline some
354
+ * images, failing the job until it is sent.
355
+ */
356
+ subjectDescription: z.string().min(1).max(300).optional(),
357
+ /** `animate-pixminimax` only: the pose the parent is in, read off the image by PixelLab when omitted. */
358
+ initialPose: z.string().min(1).max(300).optional(),
359
+ /** `animate-pixminimax` only: facing direction, one of the model's caption fields, so it also holds the sprite's facing. */
344
360
  direction: CharacterDirectionSchema.optional(),
345
361
  /** `animate`/`animate-pixminimax` only: let PixelLab expand the action into a fuller motion description first. */
346
362
  enhancePrompt: z.boolean().optional(),
@@ -360,12 +376,15 @@ var RevisionSchema = z.object({
360
376
  * same `mannequin`/`bear`/`cat`/`dog`/`horse`/`lion` vocabulary.
361
377
  */
362
378
  skeletonTemplate: z.string().min(1).optional(),
379
+ /** `remove-background` only: `simple` is faster and suits flat backgrounds; `complex` handles detailed edges. PixelLab defaults to `simple`. */
380
+ removalTask: z.enum(["simple", "complex"]).optional(),
363
381
  /**
364
- * `animate-skeleton` only: what the character *looks like*, a noun
382
+ * `animate-skeleton`: what the character *looks like*, a noun
365
383
  * phrase (colours, clothing, held items) — distinct from the asset's
366
384
  * own `prompt`, which this mode sends as the motion's short label
367
385
  * (PixelLab's `action`), the same `prompt`-as-motion convention
368
- * `animate`/`animate-pixminimax` already use.
386
+ * `animate`/`animate-pixminimax` already use. `remove-background`: a
387
+ * short description of the foreground, which PixelLab uses as a hint.
369
388
  */
370
389
  description: z.string().min(1).optional(),
371
390
  /**
@@ -482,7 +501,44 @@ var RevisionSchema = z.object({
482
501
  path: ["keypointsFile"]
483
502
  });
484
503
  }
485
- for (const field of ["keypointsFile", "skeletonTemplate", "description"]) {
504
+ if (revision.driftThreshold !== void 0 && revision.mode !== "animate" && revision.mode !== "animate-pixminimax") {
505
+ context.addIssue({
506
+ code: z.ZodIssueCode.custom,
507
+ message: "driftThreshold applies to animate/animate-pixminimax revisions only",
508
+ path: ["driftThreshold"]
509
+ });
510
+ }
511
+ for (const field of ["subjectDescription", "initialPose"]) {
512
+ if (revision[field] !== void 0 && revision.mode !== "animate-pixminimax") {
513
+ context.addIssue({
514
+ code: z.ZodIssueCode.custom,
515
+ message: `${field} applies to animate-pixminimax revisions only`,
516
+ path: [field]
517
+ });
518
+ }
519
+ }
520
+ if (revision.removalTask !== void 0 && revision.mode !== "remove-background") {
521
+ context.addIssue({
522
+ code: z.ZodIssueCode.custom,
523
+ message: "removalTask applies to remove-background revisions only",
524
+ path: ["removalTask"]
525
+ });
526
+ }
527
+ if (revision.strength !== void 0 && revision.mode === "remove-background") {
528
+ context.addIssue({
529
+ code: z.ZodIssueCode.custom,
530
+ message: "remove-background revisions do not take a strength",
531
+ path: ["strength"]
532
+ });
533
+ }
534
+ if (revision.description !== void 0 && revision.mode !== "animate-skeleton" && revision.mode !== "remove-background") {
535
+ context.addIssue({
536
+ code: z.ZodIssueCode.custom,
537
+ message: "description applies to animate-skeleton/remove-background revisions only",
538
+ path: ["description"]
539
+ });
540
+ }
541
+ for (const field of ["keypointsFile", "skeletonTemplate"]) {
486
542
  if (revision[field] !== void 0 && revision.mode !== "animate-skeleton") {
487
543
  context.addIssue({
488
544
  code: z.ZodIssueCode.custom,
@@ -1136,6 +1192,10 @@ var LockEntrySchema = z.object({
1136
1192
  keypointsSha256: z.string().regex(/^[0-9a-f]{64}$/).optional(),
1137
1193
  skeletonTemplate: z.string().min(1).optional(),
1138
1194
  description: z.string().min(1).optional(),
1195
+ removalTask: z.enum(["simple", "complex"]).optional(),
1196
+ driftThreshold: z.number().min(0).optional(),
1197
+ subjectDescription: z.string().min(1).max(300).optional(),
1198
+ initialPose: z.string().min(1).max(300).optional(),
1139
1199
  engine: z.enum(["pro-flash"]).optional()
1140
1200
  }).strict().nullable().default(null),
1141
1201
  /** For a mirror: the asset it flips and a hash over that asset's output hashes when this was made. */
@@ -1564,6 +1624,7 @@ var CorrectPixelartResponseSchema = z3.object({
1564
1624
  images: z3.array(z3.object({ base64: z3.string().min(1) }).passthrough()).min(1),
1565
1625
  usage: z3.unknown().optional()
1566
1626
  }).passthrough();
1627
+ var RemoveBackgroundResponseSchema = z3.object({ image: z3.object({ base64: z3.string().min(1) }).passthrough(), usage: z3.unknown().optional() }).passthrough();
1567
1628
  var ProFlashImageSubmitSchema = z3.object({
1568
1629
  background_job_id: z3.string().min(1),
1569
1630
  image_id: z3.string().min(1),
@@ -1755,9 +1816,9 @@ var PixelLabClient = class {
1755
1816
  apiKey;
1756
1817
  http;
1757
1818
  /** Retries transport failures, 429, and 5xx; see http.ts for the policy. */
1758
- async request(path36, init) {
1819
+ async request(path38, init) {
1759
1820
  const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
1760
- const res = await this.http(`${BASE}${path36}`, {
1821
+ const res = await this.http(`${BASE}${path38}`, {
1761
1822
  ...init,
1762
1823
  headers: {
1763
1824
  Authorization: auth,
@@ -1767,13 +1828,13 @@ var PixelLabClient = class {
1767
1828
  });
1768
1829
  const text = await res.text();
1769
1830
  if (!res.ok) {
1770
- throw new PixelLabError(`${init?.method ?? "GET"} ${path36} \u2192 ${res.status}`, res.status, text);
1831
+ throw new PixelLabError(`${init?.method ?? "GET"} ${path38} \u2192 ${res.status}`, res.status, text);
1771
1832
  }
1772
1833
  if (!text) return {};
1773
1834
  try {
1774
1835
  return JSON.parse(text);
1775
1836
  } catch {
1776
- throw new Error(`${init?.method ?? "GET"} ${path36} returned invalid JSON`);
1837
+ throw new Error(`${init?.method ?? "GET"} ${path38} returned invalid JSON`);
1777
1838
  }
1778
1839
  }
1779
1840
  async balance() {
@@ -2178,10 +2239,10 @@ var PixelLabClient = class {
2178
2239
  const palette = args.paletteSwatchBase64 ? { color_image: { type: "base64", base64: args.paletteSwatchBase64, format: "png" }, force_colors: true } : {};
2179
2240
  const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
2180
2241
  const south = args.reference?.south;
2181
- let path36;
2242
+ let path38;
2182
2243
  let body;
2183
2244
  if (args.mode === "v3") {
2184
- path36 = "/create-character-v3";
2245
+ path38 = "/create-character-v3";
2185
2246
  body = {
2186
2247
  description: args.description,
2187
2248
  image_size: imageSize,
@@ -2195,7 +2256,7 @@ var PixelLabClient = class {
2195
2256
  ...args.enhancePrompt ? { enhance_prompt: true } : {}
2196
2257
  };
2197
2258
  } else if (args.mode === "pro-flash") {
2198
- path36 = "/create-character-pro-flash";
2259
+ path38 = "/create-character-pro-flash";
2199
2260
  const traits = args.styleTraits;
2200
2261
  body = {
2201
2262
  description: args.description,
@@ -2219,7 +2280,7 @@ var PixelLabClient = class {
2219
2280
  } : {}
2220
2281
  };
2221
2282
  } else if (args.mode === "pro") {
2222
- path36 = "/create-character-pro";
2283
+ path38 = "/create-character-pro";
2223
2284
  body = {
2224
2285
  description: args.description,
2225
2286
  image_size: imageSize,
@@ -2233,7 +2294,7 @@ var PixelLabClient = class {
2233
2294
  ...!south && args.styleCharacterId ? { style_character_id: args.styleCharacterId } : {}
2234
2295
  };
2235
2296
  } else {
2236
- path36 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
2297
+ path38 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
2237
2298
  body = {
2238
2299
  description: args.description,
2239
2300
  image_size: imageSize,
@@ -2250,8 +2311,8 @@ var PixelLabClient = class {
2250
2311
  ...palette
2251
2312
  };
2252
2313
  }
2253
- const raw = await this.request(path36, { method: "POST", body: JSON.stringify(body) });
2254
- return validateResponse(CharacterSubmitSchema, raw, path36.slice(1));
2314
+ const raw = await this.request(path38, { method: "POST", body: JSON.stringify(body) });
2315
+ return validateResponse(CharacterSubmitSchema, raw, path38.slice(1));
2255
2316
  }
2256
2317
  /** A text edit of an existing character, applied to every direction and stored as a sibling. */
2257
2318
  async createCharacterState(args) {
@@ -2628,6 +2689,27 @@ var PixelLabClient = class {
2628
2689
  const pngs = cleanupResults(res.images, images.length, "correct-pixelart");
2629
2690
  return { png: pngs[0], pngs, usage: res.usage };
2630
2691
  }
2692
+ /**
2693
+ * `/remove-background`: a transparent PNG from one pixel-art image,
2694
+ * synchronously, like the rest of the Cleanup tier. The API takes at most
2695
+ * 400x400 worth of area, one image per call. `simple` is faster and suits
2696
+ * flat backgrounds; `complex` is slower and handles detailed edges. The
2697
+ * schema's response example is dollar-denominated (`usd: 0.01`); the
2698
+ * subscription cost measured 1 generation (docs/ENDPOINTS.md), and the
2699
+ * default `simple` task is the one measured.
2700
+ */
2701
+ async removeBackground(args) {
2702
+ const body = { image: args.image, image_size: { width: args.width, height: args.height } };
2703
+ if (args.task) body.background_removal_task = args.task === "complex" ? "remove_complex_background" : "remove_simple_background";
2704
+ if (args.text) body.text = args.text;
2705
+ if (args.seed != null) body.seed = args.seed;
2706
+ const res = validateResponse(
2707
+ RemoveBackgroundResponseSchema,
2708
+ await this.request("/remove-background", { method: "POST", body: JSON.stringify(body) }),
2709
+ "remove-background"
2710
+ );
2711
+ return { png: Buffer.from(res.image.base64, "base64"), usage: res.usage };
2712
+ }
2631
2713
  /**
2632
2714
  * `/unzoom`: recover native-resolution pixel art from an upscaled image (a
2633
2715
  * 32x32 sprite saved at 512x512). Synchronous like the Cleanup tier.
@@ -2736,6 +2818,7 @@ var PixelLabClient = class {
2736
2818
  */
2737
2819
  async animateWithTextV3(args) {
2738
2820
  const body = { first_frame: args.firstFrame, action: args.action };
2821
+ if (args.driftThreshold != null) body.drift_threshold = args.driftThreshold;
2739
2822
  if (args.lastFrame) body.last_frame = args.lastFrame;
2740
2823
  if (args.frameCount != null) body.frame_count = args.frameCount;
2741
2824
  if (args.seed != null) body.seed = args.seed;
@@ -2755,6 +2838,10 @@ var PixelLabClient = class {
2755
2838
  */
2756
2839
  async animatePixminimax(args) {
2757
2840
  const body = { first_frame: args.firstFrame, description: args.description };
2841
+ if (args.view) body.view = args.view;
2842
+ if (args.subjectDescription) body.subject_description = args.subjectDescription;
2843
+ if (args.initialPose) body.initial_pose = args.initialPose;
2844
+ if (args.driftThreshold != null) body.drift_threshold = args.driftThreshold;
2758
2845
  if (args.lastFrame) body.last_frame = args.lastFrame;
2759
2846
  if (args.frameCount != null) body.frame_count = args.frameCount;
2760
2847
  if (args.seed != null) body.seed = args.seed;
@@ -2790,6 +2877,7 @@ var PixelLabClient = class {
2790
2877
  keypoints: args.keypoints,
2791
2878
  direction: args.direction
2792
2879
  };
2880
+ if (args.view) body.view = args.view;
2793
2881
  if (args.templateId) body.template_id = args.templateId;
2794
2882
  if (args.action) body.action = args.action;
2795
2883
  if (args.description) body.description = args.description;
@@ -3437,10 +3525,10 @@ import { createReadStream } from "fs";
3437
3525
  function sha256(data) {
3438
3526
  return createHash("sha256").update(data).digest("hex");
3439
3527
  }
3440
- async function sha256File(path36) {
3528
+ async function sha256File(path38) {
3441
3529
  return new Promise((resolve, reject) => {
3442
3530
  const hash = createHash("sha256");
3443
- const stream = createReadStream(path36);
3531
+ const stream = createReadStream(path38);
3444
3532
  stream.on("data", (chunk2) => hash.update(chunk2));
3445
3533
  stream.on("error", reject);
3446
3534
  stream.on("end", () => resolve(hash.digest("hex")));
@@ -3564,6 +3652,10 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
3564
3652
  keypointsSha256: spec.revision.keypointsSha256,
3565
3653
  skeletonTemplate: spec.revision.skeletonTemplate,
3566
3654
  description: spec.revision.description,
3655
+ removalTask: spec.revision.removalTask,
3656
+ driftThreshold: spec.revision.driftThreshold,
3657
+ subjectDescription: spec.revision.subjectDescription,
3658
+ initialPose: spec.revision.initialPose,
3567
3659
  engine: spec.revision.engine
3568
3660
  } : void 0
3569
3661
  })
@@ -3739,7 +3831,7 @@ var PixelLabProvider = class _PixelLabProvider {
3739
3831
  * outpaint path yet.
3740
3832
  */
3741
3833
  supportsRevision(mode2) {
3742
- 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";
3834
+ 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";
3743
3835
  }
3744
3836
  /** PixelLab's own constraints: submissions must be >2s apart, and
3745
3837
  * background jobs in flight are capped by subscription tier (Tier 1=8,
@@ -3767,6 +3859,9 @@ var PixelLabProvider = class _PixelLabProvider {
3767
3859
  if (spec.batch?.role === "member") {
3768
3860
  return { unit: "generations", amount: 0, candidates: 1 };
3769
3861
  }
3862
+ if (spec.revision?.mode === "remove-background") {
3863
+ return { unit: "generations", amount: 1, candidates: 1 };
3864
+ }
3770
3865
  if (spec.revision?.mode === "reduce-colors" || spec.revision?.mode === "correct-pixelart") {
3771
3866
  return { unit: "generations", amount: 0.1, candidates: 1 };
3772
3867
  }
@@ -3855,6 +3950,12 @@ var PixelLabProvider = class _PixelLabProvider {
3855
3950
  throw new Error(`PixelLab correct-pixelart source is ${width}x${height}; the API takes at most 1024 pixels per side`);
3856
3951
  }
3857
3952
  }
3953
+ if (spec.revision?.mode === "remove-background") {
3954
+ const { sourceWidth: width, sourceHeight: height } = spec.revision;
3955
+ if (width != null && height != null && (width > 400 || height > 400)) {
3956
+ throw new Error(`PixelLab remove-background source is ${width}x${height}; the API takes at most 400x400`);
3957
+ }
3958
+ }
3858
3959
  if (spec.revision?.mode === "animate-skeleton") {
3859
3960
  const { sourceWidth: width, sourceHeight: height, frames, skeletonTemplate } = spec.revision;
3860
3961
  if (width != null && height != null && (width > 256 || height > 256)) {
@@ -3875,6 +3976,9 @@ var PixelLabProvider = class _PixelLabProvider {
3875
3976
  if (mode2 === "animate" && frames != null && frames > 16) {
3876
3977
  throw new Error(`PixelLab animate takes 4 to 16 frames; ${frames} is too many (use animate-pixminimax for up to 40)`);
3877
3978
  }
3979
+ if (mode2 === "animate-pixminimax" && frames != null && frames % 4 !== 0) {
3980
+ throw new Error(`PixelLab animate-pixminimax takes a multiple of 4 frames (4 to 40); ${frames} is not one`);
3981
+ }
3878
3982
  if (lastFrameWidth != null && lastFrameHeight != null && width != null && height != null && (lastFrameWidth !== width || lastFrameHeight !== height)) {
3879
3983
  throw new Error(
3880
3984
  `PixelLab ${mode2} lastFrame is ${lastFrameWidth}x${lastFrameHeight}; source is ${width}x${height} \u2014 they must match`
@@ -4966,6 +5070,18 @@ var PixelLabProvider = class _PixelLabProvider {
4966
5070
  const res2 = await this.client.correctPixelart({ images, strength: revision.strength });
4967
5071
  return this.cacheCleanupResult(res2.pngs, res2.usage, roles, revision.sourceFps);
4968
5072
  }
5073
+ if (revision.mode === "remove-background") {
5074
+ if (width == null || height == null) throw new Error(`${spec.styleId}/${spec.assetId}: remove-background needs the source's size`);
5075
+ const res2 = await this.client.removeBackground({
5076
+ image,
5077
+ width,
5078
+ height,
5079
+ task: revision.removalTask,
5080
+ text: revision.description,
5081
+ seed: spec.seed
5082
+ });
5083
+ return this.cacheCleanupResult([res2.png], res2.usage, roles, revision.sourceFps);
5084
+ }
4969
5085
  if (revision.mode === "interpolate") {
4970
5086
  const lastFrame = this.readLastFrame(spec);
4971
5087
  if (!lastFrame) throw new Error(`${spec.styleId}/${spec.assetId}: interpolate needs lastFrame, the ending keyframe`);
@@ -5001,7 +5117,8 @@ var PixelLabProvider = class _PixelLabProvider {
5001
5117
  frameCount: revision.frames,
5002
5118
  seed: spec.seed,
5003
5119
  noBackground: spec.noBackground,
5004
- enhancePrompt: revision.enhancePrompt
5120
+ enhancePrompt: revision.enhancePrompt,
5121
+ driftThreshold: revision.driftThreshold
5005
5122
  });
5006
5123
  return { jobId: res3.background_job_id };
5007
5124
  }
@@ -5013,7 +5130,11 @@ var PixelLabProvider = class _PixelLabProvider {
5013
5130
  seed: spec.seed,
5014
5131
  noBackground: spec.noBackground,
5015
5132
  enhancePrompt: revision.enhancePrompt,
5016
- direction: revision.direction
5133
+ direction: revision.direction,
5134
+ ...spec.view === "side" || spec.view === "low top-down" || spec.view === "high top-down" ? { view: spec.view } : {},
5135
+ subjectDescription: revision.subjectDescription,
5136
+ initialPose: revision.initialPose,
5137
+ driftThreshold: revision.driftThreshold
5017
5138
  });
5018
5139
  return { jobId: res2.background_job_id };
5019
5140
  }
@@ -5030,6 +5151,9 @@ var PixelLabProvider = class _PixelLabProvider {
5030
5151
  firstFrameKeypoints: revision.skeleton.firstFrameKeypoints,
5031
5152
  keypoints: revision.skeleton.frames,
5032
5153
  direction: revision.direction,
5154
+ // The style's camera, when it is one this endpoint knows; its enum is
5155
+ // narrower than a style's free-string `view`.
5156
+ ...spec.view === "side" || spec.view === "low top-down" || spec.view === "high top-down" ? { view: spec.view } : {},
5033
5157
  templateId: revision.skeletonTemplate,
5034
5158
  action: spec.prompt,
5035
5159
  description: revision.description,
@@ -5100,7 +5224,7 @@ var PixelLabProvider = class _PixelLabProvider {
5100
5224
  }
5101
5225
  async poll(jobId, generator, context) {
5102
5226
  const revisionMode = context?.spec?.revision?.mode;
5103
- if (revisionMode === "reduce-colors" || revisionMode === "correct-pixelart") {
5227
+ if (revisionMode === "reduce-colors" || revisionMode === "correct-pixelart" || revisionMode === "remove-background") {
5104
5228
  return this.pollCachedRevision(jobId, context);
5105
5229
  }
5106
5230
  if (revisionMode === "animate" || revisionMode === "animate-pixminimax" || revisionMode === "animate-skeleton" || revisionMode === "interpolate" || revisionMode === "edit-animation") {
@@ -5808,9 +5932,9 @@ var RetroDiffusionClient = class {
5808
5932
  if (!response.ok) throw new ProviderError("retrodiffusion", `Retro Diffusion download failed (${response.status})`, { status: response.status });
5809
5933
  return Buffer.from(await response.arrayBuffer());
5810
5934
  }
5811
- async call(path36, init = {}) {
5935
+ async call(path38, init = {}) {
5812
5936
  if (!this.token) throw new Error("RD_API_KEY is not set");
5813
- const response = await this.request(`${this.baseUrl}${path36}`, {
5937
+ const response = await this.request(`${this.baseUrl}${path38}`, {
5814
5938
  ...init,
5815
5939
  headers: {
5816
5940
  "Content-Type": "application/json",
@@ -6119,8 +6243,13 @@ function retroError(value) {
6119
6243
  }
6120
6244
 
6121
6245
  // src/providers/scenario.ts
6246
+ import { createHash as createHash2 } from "crypto";
6247
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync, writeFileSync as writeFileSync2 } from "fs";
6248
+ import os2 from "os";
6249
+ import path2 from "path";
6122
6250
  var DEFAULT_BASE_URL2 = "https://api.cloud.scenario.com/v1";
6123
6251
  var SOURCE_PROTOCOL2 = "scenario:";
6252
+ var SUBMIT_RETRIES = 8;
6124
6253
  var PROTECTED_PARAMETERS = /* @__PURE__ */ new Set([
6125
6254
  "dryRun",
6126
6255
  "height",
@@ -6167,7 +6296,11 @@ var ScenarioClient = class {
6167
6296
  async submit(modelId, body, projectId) {
6168
6297
  const response = await this.call(
6169
6298
  `/generate/custom/${encodeURIComponent(modelId)}`,
6170
- { method: "POST", body: JSON.stringify(body) },
6299
+ // A 429 here means this account's concurrent-job limit refused the
6300
+ // request, so nothing was accepted or billed; wait it out instead of
6301
+ // failing the asset. The default four retries give up after about half
6302
+ // a minute, before a running job has finished.
6303
+ { method: "POST", body: JSON.stringify(body), retries: SUBMIT_RETRIES },
6171
6304
  { projectId }
6172
6305
  );
6173
6306
  const job = response?.job;
@@ -6175,6 +6308,29 @@ var ScenarioClient = class {
6175
6308
  if (!jobId) throw new Error("Scenario did not return an async job id");
6176
6309
  return jobId;
6177
6310
  }
6311
+ /**
6312
+ * Uploads one image (POST /assets) and returns its asset id, which a model's
6313
+ * reference input takes. Free, and idempotent per image: the id is kept by
6314
+ * content hash so a rerun does not upload the same bytes again.
6315
+ */
6316
+ async uploadImage(image, projectId) {
6317
+ const bytes = Buffer.from(image.base64, "base64");
6318
+ const digest2 = sha256(bytes);
6319
+ const cached = readUploadCache()[uploadCacheKey(this.baseUrl, projectId, digest2)];
6320
+ if (cached) return cached;
6321
+ const response = await this.call(
6322
+ "/assets",
6323
+ {
6324
+ method: "POST",
6325
+ body: JSON.stringify({ image: image.base64, name: `pixelkiln-${digest2.slice(0, 12)}.${image.format === "png" ? "png" : "jpg"}` })
6326
+ },
6327
+ { projectId }
6328
+ );
6329
+ const id = response?.asset?.id;
6330
+ if (typeof id !== "string" || !id) throw new Error("Scenario did not return an uploaded asset id");
6331
+ writeUploadCache(uploadCacheKey(this.baseUrl, projectId, digest2), id);
6332
+ return id;
6333
+ }
6178
6334
  async job(id, projectId) {
6179
6335
  const response = await this.call(`/jobs/${encodeURIComponent(id)}`, {}, { projectId });
6180
6336
  const raw = response?.job;
@@ -6301,6 +6457,14 @@ var ScenarioProvider = class _ScenarioProvider {
6301
6457
  supports(generator) {
6302
6458
  return generator === "map";
6303
6459
  }
6460
+ /**
6461
+ * An edit is a generation with the parent as a reference image, which the
6462
+ * editing models (GPT Image, Gemini, FLUX) take. Masked inpainting and the
6463
+ * PixelLab-specific modes have no common Scenario input.
6464
+ */
6465
+ supportsRevision(mode2) {
6466
+ return mode2 === "image-to-image";
6467
+ }
6304
6468
  estimate(spec) {
6305
6469
  const options = scenarioOptions(spec);
6306
6470
  return {
@@ -6321,11 +6485,31 @@ var ScenarioProvider = class _ScenarioProvider {
6321
6485
  if (!Number.isInteger(count) || count < 1 || count > 4) {
6322
6486
  throw new Error("Scenario numOutputs must be a whole number from 1 to 4");
6323
6487
  }
6324
- if (spec.width < 128 || spec.width > 2048 || spec.width % 16 !== 0 || spec.height < 128 || spec.height > 2048 || spec.height % 16 !== 0) {
6325
- throw new Error("Scenario width and height must be multiples of 16 from 128 to 2048");
6488
+ if (!Number.isInteger(spec.width) || spec.width < 16 || spec.width > 4096 || !Number.isInteger(spec.height) || spec.height < 16 || spec.height > 4096) {
6489
+ throw new Error("Scenario width and height must be whole numbers from 16 to 4096");
6326
6490
  }
6327
- if (styleImages.length) {
6328
- throw new Error("Scenario styleImages are not supported yet; use a reusable Scenario model");
6491
+ const revision = spec.revision;
6492
+ if (revision) {
6493
+ if (revision.sourceMembers?.length) {
6494
+ throw new Error(
6495
+ `Scenario edits one image; ${revision.sourceAssetId} is a ${revision.sourceMembers.length}-member set`
6496
+ );
6497
+ }
6498
+ if (revision.strength != null) {
6499
+ throw new Error("Scenario image-to-image takes no strength; use the model's own parameters");
6500
+ }
6501
+ }
6502
+ const parameter = referenceParameter(options);
6503
+ if ((styleImages.length || revision) && Object.hasOwn(options.parameters ?? {}, parameter)) {
6504
+ throw new Error(
6505
+ `Scenario parameters.${parameter} is the reference-image input and is filled from styleImages and revisions`
6506
+ );
6507
+ }
6508
+ if (styleImages.length + (revision ? 1 : 0) > 1 && options.referenceArray === false) {
6509
+ throw new Error(`Scenario ${parameter} takes one image; use one style image or a revision, not both`);
6510
+ }
6511
+ if (options.referenceParameter != null && (typeof options.referenceParameter !== "string" || !options.referenceParameter.trim())) {
6512
+ throw new Error("Scenario referenceParameter must be a non-empty string");
6329
6513
  }
6330
6514
  if (options.projectId != null && (typeof options.projectId !== "string" || !options.projectId.trim())) {
6331
6515
  throw new Error("Scenario projectId must be a non-empty string");
@@ -6338,7 +6522,12 @@ var ScenarioProvider = class _ScenarioProvider {
6338
6522
  async submit(spec, styleImages) {
6339
6523
  this.validate(spec, styleImages);
6340
6524
  const options = scenarioOptions(spec);
6341
- const body = requestBody(spec, options);
6525
+ const references = [];
6526
+ if (spec.revision) references.push(readRevisionSource(spec));
6527
+ for (const image of styleImages) references.push({ base64: image.base64, format: image.format });
6528
+ const referenceAssetIds = [];
6529
+ for (const image of references) referenceAssetIds.push(await this.client.uploadImage(image, options.projectId));
6530
+ const body = requestBody(spec, options, referenceAssetIds);
6342
6531
  const quote = await this.client.quote(options.modelId, body, options.projectId);
6343
6532
  if (quote.total > options.maxComputeUnits + Number.EPSILON) {
6344
6533
  throw new Error(
@@ -6355,7 +6544,8 @@ var ScenarioProvider = class _ScenarioProvider {
6355
6544
  quotedComputeUnits: quote.total,
6356
6545
  creativeUnitsCost: quote.creativeUnitsCost,
6357
6546
  costDetails: quote.costDetails,
6358
- ipDetectionCost: quote.ipDetectionCost
6547
+ ipDetectionCost: quote.ipDetectionCost,
6548
+ ...referenceAssetIds.length ? { referenceAssetIds } : {}
6359
6549
  }
6360
6550
  };
6361
6551
  }
@@ -6377,7 +6567,13 @@ var ScenarioProvider = class _ScenarioProvider {
6377
6567
  }
6378
6568
  const assetIds = scenarioAssetIds(job);
6379
6569
  if (!assetIds.length) return { status: "failed", error: "Scenario job returned no asset IDs" };
6380
- const assets = await this.assets(assetIds, reference.projectId);
6570
+ let assets;
6571
+ try {
6572
+ assets = await this.assets(assetIds, reference.projectId);
6573
+ } catch (error) {
6574
+ if (error instanceof UnsupportedScenarioOutput) return { status: "failed", error: error.message };
6575
+ throw error;
6576
+ }
6381
6577
  if (assets.length > 1) {
6382
6578
  return {
6383
6579
  status: "review",
@@ -6432,6 +6628,32 @@ var ScenarioProvider = class _ScenarioProvider {
6432
6628
  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));
6433
6629
  }
6434
6630
  };
6631
+ function uploadCachePath() {
6632
+ return process.env.SCENARIO_UPLOAD_CACHE ?? path2.join(os2.tmpdir(), "pixelkiln-scenario", "uploads.json");
6633
+ }
6634
+ function uploadCacheKey(baseUrl, projectId, digest2) {
6635
+ return createHash2("sha256").update(`${baseUrl}
6636
+ ${projectId ?? ""}
6637
+ ${digest2}`).digest("hex");
6638
+ }
6639
+ function readUploadCache() {
6640
+ try {
6641
+ const value = JSON.parse(readFileSync2(uploadCachePath(), "utf8"));
6642
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
6643
+ } catch {
6644
+ return {};
6645
+ }
6646
+ }
6647
+ function writeUploadCache(key, assetId) {
6648
+ try {
6649
+ const file = uploadCachePath();
6650
+ mkdirSync2(path2.dirname(file), { recursive: true });
6651
+ const tmp = `${file}.${process.pid}.tmp`;
6652
+ writeFileSync2(tmp, JSON.stringify({ ...readUploadCache(), [key]: assetId }));
6653
+ renameSync(tmp, file);
6654
+ } catch {
6655
+ }
6656
+ }
6435
6657
  function scenarioOptions(spec) {
6436
6658
  return spec.providerOptions;
6437
6659
  }
@@ -6447,9 +6669,25 @@ function scenarioBaseUrl(value) {
6447
6669
  }
6448
6670
  return url.href.replace(/\/$/, "");
6449
6671
  }
6450
- function requestBody(spec, options) {
6672
+ function referenceParameter(options) {
6673
+ return options.referenceParameter?.trim() || "referenceImages";
6674
+ }
6675
+ function readRevisionSource(spec) {
6676
+ const revision = spec.revision;
6677
+ if (!revision.sourceFile || !revision.sourceSha256 || !revision.sourceFormat) {
6678
+ throw new Error(`${spec.styleId}/${spec.assetId}: revision source is not ready`);
6679
+ }
6680
+ const bytes = readFileSync2(revision.sourceFile);
6681
+ if (sha256(bytes) !== revision.sourceSha256) {
6682
+ throw new Error(`${spec.styleId}/${spec.assetId}: revision source changed after the manifest was resolved`);
6683
+ }
6684
+ return { base64: bytes.toString("base64"), format: revision.sourceFormat };
6685
+ }
6686
+ function requestBody(spec, options, referenceAssetIds = []) {
6687
+ const parameter = referenceParameter(options);
6451
6688
  return {
6452
6689
  ...options.parameters ?? {},
6690
+ ...referenceAssetIds.length ? { [parameter]: options.referenceArray === false ? referenceAssetIds[0] : referenceAssetIds } : {},
6453
6691
  prompt: spec.prompt,
6454
6692
  width: spec.width,
6455
6693
  height: spec.height,
@@ -6539,9 +6777,11 @@ function scenarioAssetIds(job) {
6539
6777
  function assetDownloadUrl(asset) {
6540
6778
  return asset.originalFileUrl || asset.url;
6541
6779
  }
6780
+ var UnsupportedScenarioOutput = class extends Error {
6781
+ };
6542
6782
  function assertPngAsset(asset) {
6543
6783
  if (asset.originalMimeType && asset.originalMimeType !== MediaType.PNG) {
6544
- throw new Error(
6784
+ throw new UnsupportedScenarioOutput(
6545
6785
  `Scenario asset ${asset.id} is ${asset.originalMimeType}; PixelKiln's Scenario MVP requires PNG output`
6546
6786
  );
6547
6787
  }
@@ -6608,7 +6848,7 @@ function redactScenarioError(value, secrets) {
6608
6848
  // src/providers/comfyui.ts
6609
6849
  import { randomUUID as randomUUID2 } from "crypto";
6610
6850
  import { readFile } from "fs/promises";
6611
- import path2 from "path";
6851
+ import path3 from "path";
6612
6852
  var DEFAULT_BASE_URL3 = "http://127.0.0.1:8188";
6613
6853
  var BUILTIN_BINDINGS = /* @__PURE__ */ new Set([
6614
6854
  "prompt",
@@ -6745,7 +6985,7 @@ var ComfyUIProvider = class _ComfyUIProvider {
6745
6985
  }
6746
6986
  async resolveOptions(value, context) {
6747
6987
  const options = parseOptions(value);
6748
- const workflowPath = path2.resolve(context.root, options.workflowFile);
6988
+ const workflowPath = path3.resolve(context.root, options.workflowFile);
6749
6989
  let raw;
6750
6990
  try {
6751
6991
  raw = await readFile(workflowPath, "utf8");
@@ -6800,7 +7040,7 @@ var ComfyUIProvider = class _ComfyUIProvider {
6800
7040
  if (typeof item !== "string") {
6801
7041
  throw new Error(`ComfyUI ${label} must be a manifest-relative PNG or JPEG path`);
6802
7042
  }
6803
- const file = path2.resolve(context.root, item);
7043
+ const file = path3.resolve(context.root, item);
6804
7044
  let bytes;
6805
7045
  try {
6806
7046
  bytes = await readFile(file);
@@ -7512,7 +7752,7 @@ function canonical(value) {
7512
7752
  }
7513
7753
 
7514
7754
  // src/providers/fake.ts
7515
- import { createHash as createHash2 } from "crypto";
7755
+ import { createHash as createHash3 } from "crypto";
7516
7756
  var FAKE_PNG = Buffer.from(
7517
7757
  "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
7518
7758
  "base64"
@@ -7614,7 +7854,7 @@ var FakeProvider = class {
7614
7854
  }
7615
7855
  async download(url) {
7616
7856
  if (!url.startsWith("fake://")) throw new Error(`fake provider cannot download ${url}`);
7617
- const salt = createHash2("sha256").update(url).digest();
7857
+ const salt = createHash3("sha256").update(url).digest();
7618
7858
  return Buffer.concat([FAKE_PNG, salt.subarray(0, 4)]);
7619
7859
  }
7620
7860
  async balance() {
@@ -7763,7 +8003,7 @@ registerProvider({
7763
8003
  // src/manifest.ts
7764
8004
  import { readFile as readFile3 } from "fs/promises";
7765
8005
  import { existsSync as existsSync4 } from "fs";
7766
- import path5 from "path";
8006
+ import path6 from "path";
7767
8007
 
7768
8008
  // src/loop-directions.ts
7769
8009
  var SINGLE_OUTPUT_FIELDS = ["file", "cell", "remoteId", "source", "sourceByStyle", "outputRole"];
@@ -7852,12 +8092,12 @@ function isObject2(value) {
7852
8092
 
7853
8093
  // src/outputs.ts
7854
8094
  import { existsSync as existsSync3 } from "fs";
7855
- import path4 from "path";
8095
+ import path5 from "path";
7856
8096
 
7857
8097
  // src/lock.ts
7858
8098
  import { readFile as readFile2, writeFile, rename, mkdir, rmdir, rm, stat } from "fs/promises";
7859
8099
  import { existsSync as existsSync2 } from "fs";
7860
- import path3 from "path";
8100
+ import path4 from "path";
7861
8101
  async function loadLock(lockPath) {
7862
8102
  if (!existsSync2(lockPath)) return { version: 2, entries: {} };
7863
8103
  try {
@@ -7874,7 +8114,7 @@ var saveQueues = /* @__PURE__ */ new Map();
7874
8114
  var dirtyPatches = /* @__PURE__ */ new WeakMap();
7875
8115
  var dirtyDeletes = /* @__PURE__ */ new WeakMap();
7876
8116
  function saveLock(lockPath, lock) {
7877
- const queueKey = path3.resolve(lockPath);
8117
+ const queueKey = path4.resolve(lockPath);
7878
8118
  const previous = saveQueues.get(queueKey) ?? Promise.resolve();
7879
8119
  const next = previous.catch(() => {
7880
8120
  }).then(() => writeLockNow(lockPath, lock));
@@ -7886,7 +8126,7 @@ function saveLock(lockPath, lock) {
7886
8126
  return next;
7887
8127
  }
7888
8128
  async function writeLockNow(lockPath, lock) {
7889
- await mkdir(path3.dirname(path3.resolve(lockPath)), { recursive: true });
8129
+ await mkdir(path4.dirname(path4.resolve(lockPath)), { recursive: true });
7890
8130
  const release = await acquireFileLock(lockPath);
7891
8131
  try {
7892
8132
  await writeLockWhileHeld(lockPath, lock);
@@ -8031,22 +8271,22 @@ function totalSpend(lock, unit = "generations") {
8031
8271
 
8032
8272
  // src/outputs.ts
8033
8273
  function portableOutputPath(file, manifestDir) {
8034
- const root = path4.resolve(manifestDir);
8035
- const absolute = path4.isAbsolute(file) ? path4.normalize(file) : path4.resolve(root, file);
8036
- const relative = path4.relative(root, absolute);
8037
- const value = path4.isAbsolute(relative) ? absolute : relative;
8038
- return value.split(path4.sep).join("/");
8274
+ const root = path5.resolve(manifestDir);
8275
+ const absolute = path5.isAbsolute(file) ? path5.normalize(file) : path5.resolve(root, file);
8276
+ const relative = path5.relative(root, absolute);
8277
+ const value = path5.isAbsolute(relative) ? absolute : relative;
8278
+ return value.split(path5.sep).join("/");
8039
8279
  }
8040
8280
  function resolveOutputPath(recordedPath, manifestDir) {
8041
- if (path4.isAbsolute(recordedPath)) return path4.normalize(recordedPath);
8042
- if (path4.win32.isAbsolute(recordedPath)) return recordedPath;
8043
- return path4.resolve(manifestDir, recordedPath.split(/[\\/]/).join(path4.sep));
8281
+ if (path5.isAbsolute(recordedPath)) return path5.normalize(recordedPath);
8282
+ if (path5.win32.isAbsolute(recordedPath)) return recordedPath;
8283
+ return path5.resolve(manifestDir, recordedPath.split(/[\\/]/).join(path5.sep));
8044
8284
  }
8045
8285
  function expectedOutputPath(spec, role, index, total, mediaType) {
8046
8286
  return memberPath(spec.outFile, role, index, total, mediaType);
8047
8287
  }
8048
8288
  function memberPath(file, role, index, total, mediaType) {
8049
- const originalExt = path4.extname(file);
8289
+ const originalExt = path5.extname(file);
8050
8290
  const ext = mediaType ? mediaExtension(mediaType) : originalExt || ".png";
8051
8291
  const stem = originalExt ? file.slice(0, -originalExt.length) : file;
8052
8292
  if (total === 1) return `${stem}${ext}`;
@@ -8060,10 +8300,10 @@ function isFrameSetEntry(entry) {
8060
8300
  return entry.generator === "frames" && isMemberSetEntry(entry);
8061
8301
  }
8062
8302
  function sourceIsStem(source, entry, manifestDir) {
8063
- return isMemberSetEntry(entry) && !existsSync3(path4.resolve(manifestDir, source));
8303
+ return isMemberSetEntry(entry) && !existsSync3(path5.resolve(manifestDir, source));
8064
8304
  }
8065
8305
  function sourceOutputPath(source, entry, index, manifestDir) {
8066
- const absolute = path4.resolve(manifestDir, source);
8306
+ const absolute = path5.resolve(manifestDir, source);
8067
8307
  if (!sourceIsStem(source, entry, manifestDir)) return absolute;
8068
8308
  const output = entry.outputs[index];
8069
8309
  return memberPath(absolute, output.role, index, entry.outputs.length, output.mediaType);
@@ -8150,7 +8390,7 @@ function resolveSpecOutputs(spec, lock, manifestDir) {
8150
8390
  id: spec.assetId,
8151
8391
  index: 0,
8152
8392
  path: spec.outFile,
8153
- absolutePath: path4.resolve(spec.outFile),
8393
+ absolutePath: path5.resolve(spec.outFile),
8154
8394
  sha256: ""
8155
8395
  }];
8156
8396
  }
@@ -8166,7 +8406,7 @@ function frameSetFps(entry) {
8166
8406
 
8167
8407
  // src/manifest.ts
8168
8408
  async function loadManifest(manifestPath) {
8169
- const abs = path5.resolve(manifestPath);
8409
+ const abs = path6.resolve(manifestPath);
8170
8410
  if (!existsSync4(abs)) throw new ProjectError(`No manifest at ${abs}`);
8171
8411
  const expansion = expandLoopDirections(JSON.parse(await readFile3(abs, "utf8")));
8172
8412
  if (expansion.issues.length) {
@@ -8259,7 +8499,7 @@ ${issues}`);
8259
8499
  ${unknownReferences.map((i) => ` ${i}`).join("\n")}`);
8260
8500
  }
8261
8501
  const loopFamilies = Object.keys(expansion.families).length ? { loopFamilies: expansion.families } : {};
8262
- return { manifest: parsed.data, root: path5.dirname(abs), path: abs, ...loopFamilies };
8502
+ return { manifest: parsed.data, root: path6.dirname(abs), path: abs, ...loopFamilies };
8263
8503
  }
8264
8504
  function parentAssetId(asset) {
8265
8505
  return asset?.revision?.from ?? asset?.state?.of ?? asset?.animation?.of ?? asset?.portrait?.of ?? asset?.outfit?.of;
@@ -8360,7 +8600,7 @@ async function resolveSpecs(loaded, filter) {
8360
8600
  }
8361
8601
  const styleImageCache = /* @__PURE__ */ new Map();
8362
8602
  async function loadStyleImage(rel, what = "Style image") {
8363
- const abs = path5.resolve(root, rel);
8603
+ const abs = path6.resolve(root, rel);
8364
8604
  let hit = styleImageCache.get(abs);
8365
8605
  if (!hit) {
8366
8606
  if (!existsSync4(abs)) throw new Error(`${what} not found: ${abs}`);
@@ -8390,7 +8630,7 @@ async function resolveSpecs(loaded, filter) {
8390
8630
  styleImageDimensions.push(loadedImage);
8391
8631
  }
8392
8632
  const resolvedImages = style.styleImages.map((image) => {
8393
- const hit = styleImageCache.get(path5.resolve(root, image.path));
8633
+ const hit = styleImageCache.get(path6.resolve(root, image.path));
8394
8634
  return { base64: hit.base64, width: hit.width, height: hit.height, format: hit.format };
8395
8635
  });
8396
8636
  const styleSpecs = /* @__PURE__ */ new Map();
@@ -8491,9 +8731,9 @@ async function resolveSpecs(loaded, filter) {
8491
8731
  } else {
8492
8732
  prompt = [style.promptPrefix, subject, style.promptSuffix].map((p) => p.trim()).filter(Boolean).join(", ");
8493
8733
  }
8494
- const relFile = asset.file ?? path5.join(asset.category ?? "", `${assetId}.png`);
8495
- const outFile = path5.resolve(root, style.outDir, relFile);
8496
- const qualityOutFile = style.quality ? path5.resolve(root, style.quality.outDir, pngPath(relFile)) : void 0;
8734
+ const relFile = asset.file ?? path6.join(asset.category ?? "", `${assetId}.png`);
8735
+ const outFile = path6.resolve(root, style.outDir, relFile);
8736
+ const qualityOutFile = style.quality ? path6.resolve(root, style.quality.outDir, pngPath(relFile)) : void 0;
8497
8737
  const tileSize = generator === "tiles" ? size : style.tileSize ?? 32;
8498
8738
  const tileVariations = tileFeatureOutputCount(generator === "tiles" ? style.tileFeature : void 0) ?? tileVariationCount(countNumberedDescriptions(prompt));
8499
8739
  const terrainTiles = generator === "terrain" ? terrainTileCount(style.terrainTransitionSize) : 0;
@@ -8588,7 +8828,7 @@ async function resolveSpecs(loaded, filter) {
8588
8828
  minGridConfidence: style.quality.minGridConfidence,
8589
8829
  ...style.quality.minTransparency == null ? {} : { minTransparency: style.quality.minTransparency },
8590
8830
  ...style.quality.fixerRevision ? { fixerRevision: style.quality.fixerRevision } : {},
8591
- ...style.quality.fixerPython ? { fixerPython: path5.resolve(root, style.quality.fixerPython) } : {},
8831
+ ...style.quality.fixerPython ? { fixerPython: path6.resolve(root, style.quality.fixerPython) } : {},
8592
8832
  ...style.quality.fps == null ? {} : { fps: style.quality.fps }
8593
8833
  }
8594
8834
  } : {},
@@ -8724,7 +8964,7 @@ async function resolveSpecs(loaded, filter) {
8724
8964
  animation: sourceSpec.character.animation,
8725
8965
  outfit: {
8726
8966
  reference: {
8727
- path: path5.resolve(root, asset.outfit.reference),
8967
+ path: path6.resolve(root, asset.outfit.reference),
8728
8968
  sha256: referenceImage.hash,
8729
8969
  width: referenceImage.width,
8730
8970
  height: referenceImage.height,
@@ -8746,23 +8986,23 @@ async function resolveSpecs(loaded, filter) {
8746
8986
  );
8747
8987
  }
8748
8988
  const sourceSpec = await finalize(asset.revision.from);
8749
- const sourceStem = sourceSpec.quality?.outFile ?? (sourceSpec.source ? path5.resolve(root, sourceSpec.source) : sourceSpec.outFile);
8989
+ const sourceStem = sourceSpec.quality?.outFile ?? (sourceSpec.source ? path6.resolve(root, sourceSpec.source) : sourceSpec.outFile);
8750
8990
  const sourceMembers = await revisionSourceMembers(sourceStem, `revision source ${asset.revision.from}`);
8751
8991
  const sourceFile = sourceMembers?.[0]?.file ?? sourceStem;
8752
8992
  const sourceImage = sourceMembers ? { ...sourceMembers[0], hash: sha256(JSON.stringify(sourceMembers.map((member) => member.sha256))) } : await optionalRevisionImage(sourceFile, "revision source");
8753
8993
  const sourceFps = sourceSpec.character?.animation?.fps ?? sourceSpec.objectPro?.animation?.fps ?? sourceSpec.revision?.fps ?? sourceSpec.revision?.sourceFps;
8754
- const maskFile = asset.revision.mask ? path5.resolve(root, asset.revision.mask) : void 0;
8994
+ const maskFile = asset.revision.mask ? path6.resolve(root, asset.revision.mask) : void 0;
8755
8995
  const maskImage = maskFile ? await optionalRevisionImage(maskFile, "revision mask", "png") : null;
8756
8996
  if (sourceImage && maskImage && (sourceImage.width !== maskImage.width || sourceImage.height !== maskImage.height)) {
8757
8997
  throw new Error(
8758
8998
  `Revision mask for ${styleId}/${assetId} is ${maskImage.width}x${maskImage.height}; source ${asset.revision.from} is ${sourceImage.width}x${sourceImage.height}`
8759
8999
  );
8760
9000
  }
8761
- const paletteImageFile = asset.revision.paletteImage ? path5.resolve(root, asset.revision.paletteImage) : void 0;
9001
+ const paletteImageFile = asset.revision.paletteImage ? path6.resolve(root, asset.revision.paletteImage) : void 0;
8762
9002
  const paletteImage = paletteImageFile ? await optionalRevisionImage(paletteImageFile, "revision palette image", "png") : null;
8763
- const lastFrameFile = asset.revision.lastFrame ? path5.resolve(root, asset.revision.lastFrame) : void 0;
9003
+ const lastFrameFile = asset.revision.lastFrame ? path6.resolve(root, asset.revision.lastFrame) : void 0;
8764
9004
  const lastFrame = lastFrameFile ? await optionalRevisionImage(lastFrameFile, "revision last frame") : null;
8765
- const keypointsFile = asset.revision.keypointsFile ? path5.resolve(root, asset.revision.keypointsFile) : void 0;
9005
+ const keypointsFile = asset.revision.keypointsFile ? path6.resolve(root, asset.revision.keypointsFile) : void 0;
8766
9006
  const keypoints = keypointsFile ? await optionalSkeletonSet(keypointsFile) : null;
8767
9007
  resolved.revision = {
8768
9008
  mode: asset.revision.mode,
@@ -8816,6 +9056,10 @@ async function resolveSpecs(loaded, filter) {
8816
9056
  } : {},
8817
9057
  ...asset.revision.skeletonTemplate ? { skeletonTemplate: asset.revision.skeletonTemplate } : {},
8818
9058
  ...asset.revision.description ? { description: asset.revision.description } : {},
9059
+ ...asset.revision.removalTask ? { removalTask: asset.revision.removalTask } : {},
9060
+ ...asset.revision.driftThreshold == null ? {} : { driftThreshold: asset.revision.driftThreshold },
9061
+ ...asset.revision.subjectDescription ? { subjectDescription: asset.revision.subjectDescription } : {},
9062
+ ...asset.revision.initialPose ? { initialPose: asset.revision.initialPose } : {},
8819
9063
  ...asset.revision.engine ? { engine: asset.revision.engine } : {}
8820
9064
  };
8821
9065
  }
@@ -8925,7 +9169,7 @@ async function resolveSpecs(loaded, filter) {
8925
9169
  return specs;
8926
9170
  }
8927
9171
  function pngPath(file) {
8928
- const extension = path5.extname(file);
9172
+ const extension = path6.extname(file);
8929
9173
  return extension ? `${file.slice(0, -extension.length)}.png` : `${file}.png`;
8930
9174
  }
8931
9175
  async function resolveMapScene(root, styleId, assetId, generator, scene, provider) {
@@ -8935,12 +9179,12 @@ async function resolveMapScene(root, styleId, assetId, generator, scene, provide
8935
9179
  if (!provider.supportsMapScene) {
8936
9180
  throw new Error(`assets.${assetId}.scene: provider "${provider.id}" cannot draw into a scene; PixelLab's /map-objects can`);
8937
9181
  }
8938
- const file = path5.resolve(root, scene.image);
9182
+ const file = path6.resolve(root, scene.image);
8939
9183
  const image = await optionalRevisionImage(file, "scene image", "png");
8940
9184
  const base = { file, sha256: image?.hash ?? null, width: image?.width ?? null, height: image?.height ?? null };
8941
9185
  const placement = scene.placement;
8942
9186
  if ("mask" in placement) {
8943
- const maskFile = path5.resolve(root, placement.mask);
9187
+ const maskFile = path6.resolve(root, placement.mask);
8944
9188
  const mask = await optionalRevisionImage(maskFile, "scene mask", "png");
8945
9189
  if (image && mask && (image.width !== mask.width || image.height !== mask.height)) {
8946
9190
  throw new Error(
@@ -9012,7 +9256,7 @@ async function resolveStyleImages(loaded, styleId) {
9012
9256
  if (!style) throw new Error(`Unknown style "${styleId}"`);
9013
9257
  const out = [];
9014
9258
  for (const img of style.styleImages) {
9015
- const file = path5.resolve(loaded.root, img.path);
9259
+ const file = path6.resolve(loaded.root, img.path);
9016
9260
  const buf = await readFile3(file);
9017
9261
  const metadata = imageMetadata(buf);
9018
9262
  if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${file}`);
@@ -9043,7 +9287,7 @@ async function resolveCharacterShape(asset, style, kind, files) {
9043
9287
  };
9044
9288
  if (asset.concept) {
9045
9289
  const image = await files.load(asset.concept, "Concept image");
9046
- shape.concept = { path: path5.resolve(files.root, asset.concept), sha256: image.hash, width: image.width, height: image.height, format: image.format };
9290
+ shape.concept = { path: path6.resolve(files.root, asset.concept), sha256: image.hash, width: image.width, height: image.height, format: image.format };
9047
9291
  }
9048
9292
  if (asset.reference) {
9049
9293
  const byDirection = typeof asset.reference === "string" ? { south: asset.reference } : asset.reference;
@@ -9051,7 +9295,7 @@ async function resolveCharacterShape(asset, style, kind, files) {
9051
9295
  for (const [direction, rel] of Object.entries(byDirection)) {
9052
9296
  const image = await files.load(rel, `Reference sprite (${direction})`);
9053
9297
  shape.reference[direction] = {
9054
- path: path5.resolve(files.root, rel),
9298
+ path: path6.resolve(files.root, rel),
9055
9299
  sha256: image.hash,
9056
9300
  width: image.width,
9057
9301
  height: image.height,
@@ -9072,7 +9316,7 @@ async function resolveCharacterShape(asset, style, kind, files) {
9072
9316
  const animationMode = asset.animation.mode ?? (asset.animation.template ? "template" : "v3");
9073
9317
  const pose = async (rel, what) => {
9074
9318
  const image = await files.load(rel, what);
9075
- return { path: path5.resolve(files.root, rel), sha256: image.hash, width: image.width, height: image.height, format: image.format };
9319
+ return { path: path6.resolve(files.root, rel), sha256: image.hash, width: image.width, height: image.height, format: image.format };
9076
9320
  };
9077
9321
  shape.animation = {
9078
9322
  mode: animationMode,
@@ -9103,7 +9347,7 @@ async function resolveObjectProShape(assetId, asset, style, kind, files) {
9103
9347
  const south = typeof asset.reference === "string" ? asset.reference : asset.reference.south;
9104
9348
  if (south) {
9105
9349
  const image = await files.load(south, "Reference sprite");
9106
- shape.reference = { path: path5.resolve(files.root, south), sha256: image.hash, width: image.width, height: image.height, format: image.format };
9350
+ shape.reference = { path: path6.resolve(files.root, south), sha256: image.hash, width: image.width, height: image.height, format: image.format };
9107
9351
  }
9108
9352
  }
9109
9353
  if (asset.state) {
@@ -9131,7 +9375,7 @@ async function resolveObjectProShape(assetId, asset, style, kind, files) {
9131
9375
  }
9132
9376
  const pose = async (rel, what) => {
9133
9377
  const image = await files.load(rel, what);
9134
- return { path: path5.resolve(files.root, rel), sha256: image.hash, width: image.width, height: image.height, format: image.format };
9378
+ return { path: path6.resolve(files.root, rel), sha256: image.hash, width: image.width, height: image.height, format: image.format };
9135
9379
  };
9136
9380
  shape.animation = {
9137
9381
  mode: asset.animation.mode ?? "v3",
@@ -9151,11 +9395,11 @@ function characterDirections(directions) {
9151
9395
  }
9152
9396
 
9153
9397
  // src/project.ts
9154
- import path13 from "path";
9398
+ import path14 from "path";
9155
9399
 
9156
9400
  // src/env.ts
9157
- import { readFileSync as readFileSync2, existsSync as existsSync5 } from "fs";
9158
- import path6 from "path";
9401
+ import { readFileSync as readFileSync3, existsSync as existsSync5 } from "fs";
9402
+ import path7 from "path";
9159
9403
  function loadEnvFiles(dir) {
9160
9404
  const loaded = [];
9161
9405
  for (const [file, vars] of envFiles(dir)) {
@@ -9169,10 +9413,10 @@ function loadEnvFiles(dir) {
9169
9413
  function envFiles(dir) {
9170
9414
  const out = [];
9171
9415
  for (const name of [".env.local", ".env"]) {
9172
- const file = path6.join(dir, name);
9416
+ const file = path7.join(dir, name);
9173
9417
  if (!existsSync5(file)) continue;
9174
9418
  try {
9175
- out.push([file, parseEnv(readFileSync2(file, "utf8"))]);
9419
+ out.push([file, parseEnv(readFileSync3(file, "utf8"))]);
9176
9420
  } catch {
9177
9421
  }
9178
9422
  }
@@ -9199,11 +9443,11 @@ function parseEnv(contents) {
9199
9443
 
9200
9444
  // src/pipeline/plan.ts
9201
9445
  import { existsSync as existsSync8 } from "fs";
9202
- import path12 from "path";
9446
+ import path13 from "path";
9203
9447
 
9204
9448
  // src/pipeline/mirror.ts
9205
9449
  import { mkdir as mkdir2, readFile as readFile4, rename as rename2, writeFile as writeFile2 } from "fs/promises";
9206
- import path7 from "path";
9450
+ import path8 from "path";
9207
9451
 
9208
9452
  // src/palette.ts
9209
9453
  function normalizePalette(values) {
@@ -9342,7 +9586,7 @@ async function deriveMirror(spec, lock, lockPath, opts = {}) {
9342
9586
  for (const [index, member] of members.entries()) {
9343
9587
  const bytes = flipHorizontal(await readFile4(member.file));
9344
9588
  const target = expectedOutputPath(spec, member.role, index, members.length, MediaType.PNG);
9345
- await mkdir2(path7.dirname(target), { recursive: true });
9589
+ await mkdir2(path8.dirname(target), { recursive: true });
9346
9590
  const tmp = `${target}.pixelkiln.tmp`;
9347
9591
  await writeFile2(tmp, bytes);
9348
9592
  await rename2(tmp, target);
@@ -9405,28 +9649,28 @@ async function deriveMirror(spec, lock, lockPath, opts = {}) {
9405
9649
 
9406
9650
  // src/pipeline/quality-profile.ts
9407
9651
  import { existsSync as existsSync6 } from "fs";
9408
- import path10 from "path";
9652
+ import path11 from "path";
9409
9653
 
9410
9654
  // src/pipeline/refine.ts
9411
- import path9 from "path";
9655
+ import path10 from "path";
9412
9656
  import { execFile } from "child_process";
9413
9657
  import { mkdtemp, readFile as readFile6, rm as rm3 } from "fs/promises";
9414
9658
  import { tmpdir } from "os";
9415
9659
  import { promisify } from "util";
9416
9660
 
9417
9661
  // src/artifacts.ts
9418
- import path8 from "path";
9419
- import { createHash as createHash3, randomUUID as randomUUID3 } from "crypto";
9662
+ import path9 from "path";
9663
+ import { createHash as createHash4, randomUUID as randomUUID3 } from "crypto";
9420
9664
  import { mkdir as mkdir3, readFile as readFile5, rename as rename3, rm as rm2, writeFile as writeFile3 } from "fs/promises";
9421
9665
  var activeTransactions = /* @__PURE__ */ new Set();
9422
9666
  function message(error) {
9423
9667
  return error instanceof Error ? error.message : String(error);
9424
9668
  }
9425
9669
  function digest(data) {
9426
- return createHash3("sha256").update(data).digest("hex");
9670
+ return createHash4("sha256").update(data).digest("hex");
9427
9671
  }
9428
9672
  function portableRelative(from, to) {
9429
- return path8.relative(from, path8.resolve(to)).split(path8.sep).join("/") || ".";
9673
+ return path9.relative(from, path9.resolve(to)).split(path9.sep).join("/") || ".";
9430
9674
  }
9431
9675
  function canonical2(value) {
9432
9676
  if (value === null || typeof value === "string" || typeof value === "boolean") return value;
@@ -9476,7 +9720,7 @@ function parseArtifactManifest(absolute, data) {
9476
9720
  return raw;
9477
9721
  }
9478
9722
  async function readArtifactBundleManifest(manifestPath) {
9479
- const absolute = path8.resolve(manifestPath);
9723
+ const absolute = path9.resolve(manifestPath);
9480
9724
  return parseArtifactManifest(absolute, await readFile5(absolute, "utf8"));
9481
9725
  }
9482
9726
  async function readOptional(file) {
@@ -9500,8 +9744,8 @@ function processIsAlive(pid) {
9500
9744
  }
9501
9745
  }
9502
9746
  function validTemporaryPath(candidate, destination, type) {
9503
- return path8.dirname(candidate) === path8.dirname(destination) && path8.basename(candidate).startsWith(
9504
- `.${path8.basename(destination)}.pixelkiln-${type}-`
9747
+ return path9.dirname(candidate) === path9.dirname(destination) && path9.basename(candidate).startsWith(
9748
+ `.${path9.basename(destination)}.pixelkiln-${type}-`
9505
9749
  );
9506
9750
  }
9507
9751
  function parseTransaction(journal, data) {
@@ -9521,7 +9765,7 @@ async function removeTransactionFiles(journal) {
9521
9765
  await rm2(transactionMarker(journal), { force: true });
9522
9766
  }
9523
9767
  async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
9524
- const journal = path8.resolve(recoveryFile);
9768
+ const journal = path9.resolve(recoveryFile);
9525
9769
  const bytes = await readOptional(journal);
9526
9770
  if (!bytes) {
9527
9771
  await rm2(transactionMarker(journal), { force: true });
@@ -9537,7 +9781,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
9537
9781
  );
9538
9782
  }
9539
9783
  for (const entry of transaction.entries) {
9540
- if (typeof entry.destination !== "string" || typeof entry.stage !== "string" || typeof entry.sha256 !== "string" || entry.backup !== void 0 && typeof entry.backup !== "string" || !allowedDestinations.has(path8.resolve(entry.destination)) || !validTemporaryPath(entry.stage, entry.destination, "stage") || entry.backup !== void 0 && !validTemporaryPath(entry.backup, entry.destination, "backup")) {
9784
+ if (typeof entry.destination !== "string" || typeof entry.stage !== "string" || typeof entry.sha256 !== "string" || entry.backup !== void 0 && typeof entry.backup !== "string" || !allowedDestinations.has(path9.resolve(entry.destination)) || !validTemporaryPath(entry.stage, entry.destination, "stage") || entry.backup !== void 0 && !validTemporaryPath(entry.backup, entry.destination, "backup")) {
9541
9785
  throw new Error(
9542
9786
  `Refusing unsafe artifact recovery from ${journal}; its destinations or temporary paths do not match the current bundle.`
9543
9787
  );
@@ -9583,7 +9827,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
9583
9827
  await removeTransactionFiles(journal);
9584
9828
  }
9585
9829
  function createArtifactBundleManifest(manifestPath, outputs, provenance) {
9586
- const root = path8.dirname(path8.resolve(manifestPath));
9830
+ const root = path9.dirname(path9.resolve(manifestPath));
9587
9831
  const sources = provenance.sources.map((source) => ({
9588
9832
  ...source,
9589
9833
  path: portableRelative(root, source.path)
@@ -9613,11 +9857,11 @@ function withArtifactManifest(manifestPath, outputs, provenance) {
9613
9857
  ];
9614
9858
  }
9615
9859
  async function writeManagedArtifactBundle(manifestPath, outputs, provenance, options = {}) {
9616
- const absoluteManifest = path8.resolve(manifestPath);
9860
+ const absoluteManifest = path9.resolve(manifestPath);
9617
9861
  const recoveryFile = `${absoluteManifest}.transaction`;
9618
9862
  const allowedDestinations = /* @__PURE__ */ new Set([
9619
9863
  absoluteManifest,
9620
- ...outputs.map((output) => path8.resolve(output.path))
9864
+ ...outputs.map((output) => path9.resolve(output.path))
9621
9865
  ]);
9622
9866
  await recoverArtifactTransaction(recoveryFile, allowedDestinations);
9623
9867
  const existingManifestBytes = await readOptional(absoluteManifest);
@@ -9635,13 +9879,13 @@ async function writeManagedArtifactBundle(manifestPath, outputs, provenance, opt
9635
9879
  }
9636
9880
  }
9637
9881
  if (!options.force) {
9638
- const root = path8.dirname(absoluteManifest);
9882
+ const root = path9.dirname(absoluteManifest);
9639
9883
  const recorded = new Map(
9640
- previous?.outputs.map((output) => [path8.resolve(root, output.path), output.sha256]) ?? []
9884
+ previous?.outputs.map((output) => [path9.resolve(root, output.path), output.sha256]) ?? []
9641
9885
  );
9642
9886
  const conflicts = [];
9643
9887
  for (const output of outputs) {
9644
- const destination = path8.resolve(output.path);
9888
+ const destination = path9.resolve(output.path);
9645
9889
  const current = await readOptional(destination);
9646
9890
  if (!current || digest(current) === digest(output.data)) continue;
9647
9891
  const expected = recorded.get(destination);
@@ -9663,15 +9907,15 @@ Pass { force: true } (CLI: --force) to take ownership and replace the complete b
9663
9907
  );
9664
9908
  }
9665
9909
  async function verifyArtifactBundle(manifestPath) {
9666
- const absolute = path8.resolve(manifestPath);
9910
+ const absolute = path9.resolve(manifestPath);
9667
9911
  const raw = await readArtifactBundleManifest(absolute);
9668
- const root = path8.dirname(absolute);
9912
+ const root = path9.dirname(absolute);
9669
9913
  const changedSources = [];
9670
9914
  const changedOutputs = [];
9671
9915
  for (const source of raw.sources) {
9672
9916
  let actual = null;
9673
9917
  try {
9674
- actual = digest(await readFile5(path8.resolve(root, source.path)));
9918
+ actual = digest(await readFile5(path9.resolve(root, source.path)));
9675
9919
  } catch (error) {
9676
9920
  if (!isCode(error, "ENOENT")) throw error;
9677
9921
  }
@@ -9679,7 +9923,7 @@ async function verifyArtifactBundle(manifestPath) {
9679
9923
  }
9680
9924
  for (const output of raw.outputs) {
9681
9925
  try {
9682
- if (digest(await readFile5(path8.resolve(root, output.path))) !== output.sha256) {
9926
+ if (digest(await readFile5(path9.resolve(root, output.path))) !== output.sha256) {
9683
9927
  changedOutputs.push(output.path);
9684
9928
  }
9685
9929
  } catch (error) {
@@ -9735,7 +9979,7 @@ async function writeArtifactBundle(files, options = {}) {
9735
9979
  if (!files.length) throw new Error("An artifact bundle must contain at least one file.");
9736
9980
  const normalized = files.map((file) => {
9737
9981
  if (!file.path.trim()) throw new Error("Artifact paths cannot be empty.");
9738
- return { destination: path8.resolve(file.path), data: Buffer.from(file.data) };
9982
+ return { destination: path9.resolve(file.path), data: Buffer.from(file.data) };
9739
9983
  });
9740
9984
  const destinations = /* @__PURE__ */ new Set();
9741
9985
  for (const artifact of normalized) {
@@ -9765,21 +10009,21 @@ async function writeArtifactBundle(files, options = {}) {
9765
10009
  if (!changed.length) return { changed: [], unchanged };
9766
10010
  const token = randomUUID3();
9767
10011
  for (const [index, artifact] of changed.entries()) {
9768
- const basename = path8.basename(artifact.destination);
9769
- artifact.stage = path8.join(
9770
- path8.dirname(artifact.destination),
10012
+ const basename = path9.basename(artifact.destination);
10013
+ artifact.stage = path9.join(
10014
+ path9.dirname(artifact.destination),
9771
10015
  `.${basename}.pixelkiln-stage-${token}-${index}`
9772
10016
  );
9773
10017
  if (artifact.existed) {
9774
- artifact.backup = path8.join(
9775
- path8.dirname(artifact.destination),
10018
+ artifact.backup = path9.join(
10019
+ path9.dirname(artifact.destination),
9776
10020
  `.${basename}.pixelkiln-backup-${token}-${index}`
9777
10021
  );
9778
10022
  }
9779
10023
  }
9780
- const journal = options.recoveryFile ? path8.resolve(options.recoveryFile) : null;
10024
+ const journal = options.recoveryFile ? path9.resolve(options.recoveryFile) : null;
9781
10025
  if (journal) {
9782
- await mkdir3(path8.dirname(journal), { recursive: true });
10026
+ await mkdir3(path9.dirname(journal), { recursive: true });
9783
10027
  const transaction = {
9784
10028
  format: "pixelkiln-artifact-transaction",
9785
10029
  version: 1,
@@ -9804,7 +10048,7 @@ async function writeArtifactBundle(files, options = {}) {
9804
10048
  }
9805
10049
  try {
9806
10050
  for (const [index, artifact] of changed.entries()) {
9807
- await mkdir3(path8.dirname(artifact.destination), { recursive: true });
10051
+ await mkdir3(path9.dirname(artifact.destination), { recursive: true });
9808
10052
  await options.beforeStage?.(artifact.destination, index);
9809
10053
  await writeFile3(artifact.stage, artifact.data, { flag: "wx" });
9810
10054
  }
@@ -10070,10 +10314,10 @@ function sameFrameGrid(first, next) {
10070
10314
  return next.detection.stepX === first.detection.stepX && next.detection.stepY === first.detection.stepY && next.detection.columns === first.detection.columns && next.detection.rows === first.detection.rows && phaseMatches(next.detection.phaseX, first.detection.phaseX, first.detection.stepX) && phaseMatches(next.detection.phaseY, first.detection.phaseY, first.detection.stepY) && next.audit.width === first.audit.width && next.audit.height === first.audit.height && next.sourceWidth === first.sourceWidth && next.sourceHeight === first.sourceHeight;
10071
10315
  }
10072
10316
  async function refineAsset(options) {
10073
- const source = path9.resolve(options.source);
10074
- const output = path9.resolve(options.output);
10317
+ const source = path10.resolve(options.source);
10318
+ const output = path10.resolve(options.output);
10075
10319
  if (source === output) throw new Error("Refinement source and output must be different files.");
10076
- const record = path9.resolve(qualityRecordPath(output));
10320
+ const record = path10.resolve(qualityRecordPath(output));
10077
10321
  const palette = normalizePalette(options.palette).map((color) => color.hex);
10078
10322
  const minGridConfidence = requireConfidence(options.minGridConfidence ?? "high");
10079
10323
  if (options.minTransparency !== void 0 && (!Number.isFinite(options.minTransparency) || options.minTransparency < 0 || options.minTransparency > 1)) throw new Error("minTransparency must be between 0 and 1.");
@@ -10096,8 +10340,8 @@ async function refineAsset(options) {
10096
10340
  );
10097
10341
  }
10098
10342
  }
10099
- const temp = await mkdtemp(path9.join(tmpdir(), "pixelkiln-refine-"));
10100
- const recovered = path9.join(temp, "native.png");
10343
+ const temp = await mkdtemp(path10.join(tmpdir(), "pixelkiln-refine-"));
10344
+ const recovered = path10.join(temp, "native.png");
10101
10345
  const fixerCommand = compatibilityCommand ?? options.fixerPython ?? process.env.PIXELKILN_PIXEL_FIXER_PYTHON ?? "python3";
10102
10346
  const args = compatibilityCommand ? [...options.fixerArgsPrefix ?? [], source, "--extract", recovered] : ["-c", PYTHON_FIXER_BRIDGE, source, recovered];
10103
10347
  try {
@@ -10182,18 +10426,18 @@ async function refineFrameSet(options) {
10182
10426
  if (roles.some((role) => !role.trim()) || new Set(roles).size !== roles.length) {
10183
10427
  throw new Error("Frame-set roles must be non-empty and unique.");
10184
10428
  }
10185
- const output = path9.resolve(options.output);
10186
- const record = path9.resolve(qualityRecordPath(output));
10429
+ const output = path10.resolve(options.output);
10430
+ const record = path10.resolve(qualityRecordPath(output));
10187
10431
  const palette = normalizeRefinementPalette(options.palette);
10188
10432
  const finalOutputs = options.sources.map((source, index) => qualityFrameOutputPath(output, source.role, index));
10189
10433
  if (new Set(finalOutputs).size !== finalOutputs.length) {
10190
10434
  throw new Error("Frame-set roles resolve to duplicate output filenames.");
10191
10435
  }
10192
- const sourceSet = new Set(options.sources.map((source) => path9.resolve(source.path)));
10436
+ const sourceSet = new Set(options.sources.map((source) => path10.resolve(source.path)));
10193
10437
  if (finalOutputs.some((file) => sourceSet.has(file))) {
10194
10438
  throw new Error("Frame-set refinement sources and outputs must be different files.");
10195
10439
  }
10196
- const temp = await mkdtemp(path9.join(tmpdir(), "pixelkiln-refine-frames-"));
10440
+ const temp = await mkdtemp(path10.join(tmpdir(), "pixelkiln-refine-frames-"));
10197
10441
  try {
10198
10442
  const frames = [];
10199
10443
  const outputs = [];
@@ -10201,8 +10445,8 @@ async function refineFrameSet(options) {
10201
10445
  const sourcePaths = [];
10202
10446
  let firstOptions = null;
10203
10447
  for (const [index, sourceEntry] of options.sources.entries()) {
10204
- const source = path9.resolve(sourceEntry.path);
10205
- const temporaryOutput = path9.join(temp, `${String(index).padStart(2, "0")}.png`);
10448
+ const source = path10.resolve(sourceEntry.path);
10449
+ const temporaryOutput = path10.join(temp, `${String(index).padStart(2, "0")}.png`);
10206
10450
  const refined = await refineAsset({
10207
10451
  source,
10208
10452
  output: temporaryOutput,
@@ -10277,7 +10521,7 @@ async function refineFrameSet(options) {
10277
10521
  );
10278
10522
  return {
10279
10523
  sources: sourcePaths,
10280
- outputs: outputs.map((item) => path9.resolve(item.path)),
10524
+ outputs: outputs.map((item) => path10.resolve(item.path)),
10281
10525
  record,
10282
10526
  frames,
10283
10527
  palette,
@@ -10288,12 +10532,12 @@ async function refineFrameSet(options) {
10288
10532
  }
10289
10533
  }
10290
10534
  async function checkQualityRecord(recordPath) {
10291
- const record = path9.resolve(recordPath);
10535
+ const record = path10.resolve(recordPath);
10292
10536
  const manifest = await readArtifactBundleManifest(record);
10293
10537
  const options = parseRecordOptions(manifest, record);
10294
10538
  const verification = await verifyArtifactBundle(record);
10295
- const sources = manifest.sources.map((source2) => path9.resolve(path9.dirname(record), source2.path));
10296
- const outputs = manifest.outputs.map((output2) => path9.resolve(path9.dirname(record), output2.path));
10539
+ const sources = manifest.sources.map((source2) => path10.resolve(path10.dirname(record), source2.path));
10540
+ const outputs = manifest.outputs.map((output2) => path10.resolve(path10.dirname(record), output2.path));
10297
10541
  const source = sources[0] ?? "";
10298
10542
  const output = outputs[0] ?? "";
10299
10543
  const expectedCount = options.frameSet?.count ?? 1;
@@ -10326,7 +10570,7 @@ async function checkQualityRecord(recordPath) {
10326
10570
  async function approveQualityRecord(recordPath, approval) {
10327
10571
  const reviewer = approval.reviewer.trim();
10328
10572
  if (!reviewer) throw new Error("A human reviewer name is required.");
10329
- const record = path9.resolve(recordPath);
10573
+ const record = path10.resolve(recordPath);
10330
10574
  const manifest = await readArtifactBundleManifest(record);
10331
10575
  const options = parseRecordOptions(manifest, record);
10332
10576
  const verification = await verifyArtifactBundle(record);
@@ -10347,14 +10591,14 @@ async function approveQualityRecord(recordPath, approval) {
10347
10591
  `A refinement record must contain exactly ${expectedCount} source(s) and output(s).`
10348
10592
  );
10349
10593
  }
10350
- const root = path9.dirname(record);
10594
+ const root = path10.dirname(record);
10351
10595
  const outputs = await Promise.all(manifest.outputs.map(async (entry) => {
10352
- const outputPath = path9.resolve(root, entry.path);
10596
+ const outputPath = path10.resolve(root, entry.path);
10353
10597
  return { path: outputPath, data: await readFile6(outputPath) };
10354
10598
  }));
10355
10599
  const sources = manifest.sources.map((source) => ({
10356
10600
  ...source,
10357
- path: path9.resolve(root, source.path)
10601
+ path: path10.resolve(root, source.path)
10358
10602
  }));
10359
10603
  const approved = {
10360
10604
  ...options,
@@ -10386,7 +10630,7 @@ async function qualitySource(spec, lock) {
10386
10630
  if (spec.generator === "frames") {
10387
10631
  return { items: null, reason: "frame-set quality requires ordered downloaded provider outputs" };
10388
10632
  }
10389
- const source = path10.resolve(spec.root, spec.source);
10633
+ const source = path11.resolve(spec.root, spec.source);
10390
10634
  return existsSync6(source) ? { items: [{ role: "source", path: source }] } : { items: null, reason: `declared source is missing: ${spec.source}` };
10391
10635
  }
10392
10636
  const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
@@ -10453,7 +10697,7 @@ async function inspectQualityProfile(spec, lock) {
10453
10697
  const expectedRevision = spec.quality.fixerRevision ?? PIXEL_ART_FIXER_REVISION;
10454
10698
  const expectedTransparency = spec.quality.minTransparency ?? null;
10455
10699
  const expectedFrameSet = spec.generator === "frames" ? { fps: spec.quality.fps ?? 12, count: source.items.length, roles: source.items.map((item) => item.role) } : null;
10456
- const contractChanged = JSON.stringify(check.sources.map((item) => path10.resolve(item))) !== JSON.stringify(sourcePaths.map((item) => path10.resolve(item))) || JSON.stringify(check.outputs.map((item) => path10.resolve(item))) !== JSON.stringify(expectedOutputs.map((item) => path10.resolve(item))) || JSON.stringify(check.options.palette.colors) !== JSON.stringify(expectedPalette) || check.options.nativeGrid.revision !== expectedRevision || check.options.audit.thresholds.minGridConfidence !== spec.quality.minGridConfidence || check.options.audit.thresholds.minTransparency !== expectedTransparency || (expectedFrameSet ? !check.options.frameSet || check.options.frameSet.fps !== expectedFrameSet.fps || check.options.frameSet.count !== expectedFrameSet.count || JSON.stringify(check.options.frameSet.roles) !== JSON.stringify(expectedFrameSet.roles) : check.options.frameSet !== void 0);
10700
+ const contractChanged = JSON.stringify(check.sources.map((item) => path11.resolve(item))) !== JSON.stringify(sourcePaths.map((item) => path11.resolve(item))) || JSON.stringify(check.outputs.map((item) => path11.resolve(item))) !== JSON.stringify(expectedOutputs.map((item) => path11.resolve(item))) || JSON.stringify(check.options.palette.colors) !== JSON.stringify(expectedPalette) || check.options.nativeGrid.revision !== expectedRevision || check.options.audit.thresholds.minGridConfidence !== spec.quality.minGridConfidence || check.options.audit.thresholds.minTransparency !== expectedTransparency || (expectedFrameSet ? !check.options.frameSet || check.options.frameSet.fps !== expectedFrameSet.fps || check.options.frameSet.count !== expectedFrameSet.count || JSON.stringify(check.options.frameSet.roles) !== JSON.stringify(expectedFrameSet.roles) : check.options.frameSet !== void 0);
10457
10701
  if (contractChanged) {
10458
10702
  return {
10459
10703
  key,
@@ -10617,7 +10861,7 @@ async function requireApprovedQualitySources(specs, lock, assetIds, outputRoles)
10617
10861
 
10618
10862
  // src/pipeline/revision.ts
10619
10863
  import { existsSync as existsSync7 } from "fs";
10620
- import path11 from "path";
10864
+ import path12 from "path";
10621
10865
  async function inspectRevisionReadiness(spec, lock) {
10622
10866
  if (spec.mirror) return inspectMirror(spec, lock, /* @__PURE__ */ new Set());
10623
10867
  if (spec.character?.kind === "outfit") return inspectOutfit(spec, lock, /* @__PURE__ */ new Set());
@@ -10760,8 +11004,8 @@ async function inspectSourceMembers(spec, lock) {
10760
11004
  if (parent.source) return { ready: true, reason: "declared source set is present" };
10761
11005
  const entry = lock.entries[lockKey(parent.styleId, parent.assetId)];
10762
11006
  if (!entry) return { ready: false, reason: "revision source is not in the lockfile" };
10763
- const recorded = entry.outputs.map((_, index) => path11.resolve(currentEntryOutputPath(entry, parent, index))).sort();
10764
- const found = members.map((member) => path11.resolve(member.file)).sort();
11007
+ const recorded = entry.outputs.map((_, index) => path12.resolve(currentEntryOutputPath(entry, parent, index))).sort();
11008
+ const found = members.map((member) => path12.resolve(member.file)).sort();
10765
11009
  if (recorded.length !== found.length || recorded.some((file, index) => file !== found[index])) {
10766
11010
  return {
10767
11011
  ready: false,
@@ -10810,7 +11054,7 @@ async function inspectParent(spec, lock, seen) {
10810
11054
  };
10811
11055
  }
10812
11056
  if (spec.source) {
10813
- const source = path11.resolve(spec.root, spec.source);
11057
+ const source = path12.resolve(spec.root, spec.source);
10814
11058
  return existsSync7(source) ? { ready: true, reason: "declared source is present" } : { ready: false, reason: `declared source is missing: ${spec.source}` };
10815
11059
  }
10816
11060
  const entry = lock.entries[key];
@@ -10889,7 +11133,7 @@ async function buildPlan(specs, lock, opts = {}) {
10889
11133
  state = "missing";
10890
11134
  reason = "--force";
10891
11135
  } else if (!entry && spec.source && spec.generator !== "frames") {
10892
- if (existsSync8(path12.resolve(spec.root, spec.source))) {
11136
+ if (existsSync8(path13.resolve(spec.root, spec.source))) {
10893
11137
  state = "ok";
10894
11138
  reason = `placed from ${spec.source}; not generated`;
10895
11139
  } else {
@@ -11031,13 +11275,13 @@ function summarize(plan) {
11031
11275
 
11032
11276
  // src/project.ts
11033
11277
  async function openProject(manifestPath = "pixelkiln.manifest.json", opts = {}) {
11034
- const absolute = path13.resolve(manifestPath);
11035
- const root = path13.dirname(absolute);
11278
+ const absolute = path14.resolve(manifestPath);
11279
+ const root = path14.dirname(absolute);
11036
11280
  if (opts.env !== false) {
11037
11281
  loadEnvFiles(root);
11038
- if (path13.resolve(process.cwd()) !== root) loadEnvFiles(process.cwd());
11282
+ if (path14.resolve(process.cwd()) !== root) loadEnvFiles(process.cwd());
11039
11283
  }
11040
- const lockPath = path13.resolve(opts.lockPath ?? path13.join(root, "pixelkiln.lock.json"));
11284
+ const lockPath = path14.resolve(opts.lockPath ?? path14.join(root, "pixelkiln.lock.json"));
11041
11285
  const loaded = await loadManifest(absolute);
11042
11286
  const specs = await resolveSpecs(loaded, { styles: opts.styles ?? [], assets: opts.assets ?? [] });
11043
11287
  const lock = await loadLock(lockPath);
@@ -11104,7 +11348,7 @@ function shouldPersistSourceUrl(value) {
11104
11348
  }
11105
11349
 
11106
11350
  // src/recipes.ts
11107
- import path14 from "path";
11351
+ import path15 from "path";
11108
11352
  import { existsSync as existsSync9 } from "fs";
11109
11353
  import { readdir, readFile as readFile7 } from "fs/promises";
11110
11354
  import { fileURLToPath } from "url";
@@ -11115,7 +11359,7 @@ var VERSION_RE = /^\d+\.\d+\.\d+$/;
11115
11359
  var RECIPE_FILE = "pixelkiln.recipe.json";
11116
11360
  var RECIPE_DIR_TOKEN = "{{recipeDir}}";
11117
11361
  var PortablePathSchema = z4.string().min(1).refine((value) => {
11118
- if (value.includes("\\") || path14.posix.isAbsolute(value)) return false;
11362
+ if (value.includes("\\") || path15.posix.isAbsolute(value)) return false;
11119
11363
  const parts = value.split("/");
11120
11364
  return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
11121
11365
  }, "expected a portable relative path without '.', '..', or backslashes");
@@ -11291,7 +11535,7 @@ function message3(error) {
11291
11535
  return error instanceof Error ? error.message : String(error);
11292
11536
  }
11293
11537
  async function readRecipeFile(recipePath, bundled) {
11294
- const absolute = path14.resolve(recipePath);
11538
+ const absolute = path15.resolve(recipePath);
11295
11539
  let raw;
11296
11540
  try {
11297
11541
  raw = JSON.parse(await readFile7(absolute, "utf8"));
@@ -11305,13 +11549,13 @@ async function readRecipeFile(recipePath, bundled) {
11305
11549
  ` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
11306
11550
  );
11307
11551
  }
11308
- return { recipe: parsed.data, authored: raw, path: absolute, dir: path14.dirname(absolute), bundled };
11552
+ return { recipe: parsed.data, authored: raw, path: absolute, dir: path15.dirname(absolute), bundled };
11309
11553
  }
11310
11554
  async function walkRecipeFiles(root) {
11311
11555
  if (!existsSync9(root)) return [];
11312
11556
  const out = [];
11313
11557
  for (const entry of await readdir(root, { withFileTypes: true })) {
11314
- const resolved = path14.join(root, entry.name);
11558
+ const resolved = path15.join(root, entry.name);
11315
11559
  if (entry.isDirectory()) out.push(...await walkRecipeFiles(resolved));
11316
11560
  else if (entry.isFile() && entry.name === RECIPE_FILE) out.push(resolved);
11317
11561
  }
@@ -11322,7 +11566,7 @@ function bundledRecipeRoot() {
11322
11566
  }
11323
11567
  async function listBundledRecipes(root = bundledRecipeRoot()) {
11324
11568
  const recipes = await Promise.all(
11325
- (await walkRecipeFiles(path14.resolve(root))).map((file) => readRecipeFile(file, true))
11569
+ (await walkRecipeFiles(path15.resolve(root))).map((file) => readRecipeFile(file, true))
11326
11570
  );
11327
11571
  return recipes.sort(
11328
11572
  (a, b) => a.recipe.id.localeCompare(b.recipe.id) || compareVersions(b.recipe.version, a.recipe.version)
@@ -11345,9 +11589,9 @@ function parseSelector(selector) {
11345
11589
  return { id: match[1], version: match[2] };
11346
11590
  }
11347
11591
  async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
11348
- const local = path14.resolve(target);
11592
+ const local = path15.resolve(target);
11349
11593
  if (existsSync9(local)) {
11350
- const recipePath = path14.basename(local) === RECIPE_FILE ? local : path14.join(local, RECIPE_FILE);
11594
+ const recipePath = path15.basename(local) === RECIPE_FILE ? local : path15.join(local, RECIPE_FILE);
11351
11595
  return readRecipeFile(recipePath, false);
11352
11596
  }
11353
11597
  if (target.startsWith(".") || target.startsWith("/") || target.includes("\\")) {
@@ -11361,7 +11605,7 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
11361
11605
  return matches[0];
11362
11606
  }
11363
11607
  async function verifyFile(root, file) {
11364
- const absolute = path14.resolve(root, ...file.path.split("/"));
11608
+ const absolute = path15.resolve(root, ...file.path.split("/"));
11365
11609
  if (!existsSync9(absolute)) {
11366
11610
  return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
11367
11611
  }
@@ -11377,7 +11621,7 @@ async function verifyRecipe(target, options = {}) {
11377
11621
  const loaded = await resolveRecipe(target, options.bundledRoot);
11378
11622
  const actualIntegrity = recipeDigest(loaded.authored);
11379
11623
  const files = await Promise.all(loaded.recipe.files.map((file) => verifyFile(loaded.dir, file)));
11380
- const modelRoot = options.modelRoot ? path14.resolve(options.modelRoot) : null;
11624
+ const modelRoot = options.modelRoot ? path15.resolve(options.modelRoot) : null;
11381
11625
  const models = modelRoot ? await Promise.all(loaded.recipe.models.map(async (model) => ({
11382
11626
  ...await verifyFile(modelRoot, model),
11383
11627
  source: model.source,
@@ -11411,7 +11655,7 @@ async function verifyRecipe(target, options = {}) {
11411
11655
  };
11412
11656
  }
11413
11657
  function renderStyle(style, destination, cwd) {
11414
- const portableDir = path14.relative(cwd, destination).split(path14.sep).join("/") || ".";
11658
+ const portableDir = path15.relative(cwd, destination).split(path15.sep).join("/") || ".";
11415
11659
  const replace = (value) => {
11416
11660
  if (typeof value === "string") return value.replaceAll(RECIPE_DIR_TOKEN, portableDir);
11417
11661
  if (Array.isArray(value)) return value.map(replace);
@@ -11423,19 +11667,19 @@ function renderStyle(style, destination, cwd) {
11423
11667
  return replace(style);
11424
11668
  }
11425
11669
  async function installRecipe(target, options = {}) {
11426
- const cwd = path14.resolve(options.cwd ?? process.cwd());
11670
+ const cwd = path15.resolve(options.cwd ?? process.cwd());
11427
11671
  const loaded = await resolveRecipe(target, options.bundledRoot);
11428
11672
  const verification = await verifyRecipe(loaded.path);
11429
11673
  if (!verification.ok) {
11430
11674
  throw new Error(`Recipe ${loaded.recipe.id}@${loaded.recipe.version} failed integrity verification.`);
11431
11675
  }
11432
- const destination = options.out ? path14.resolve(cwd, options.out) : path14.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
11676
+ const destination = options.out ? path15.resolve(cwd, options.out) : path15.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
11433
11677
  const sources = [
11434
11678
  { path: RECIPE_FILE, source: loaded.path },
11435
- ...loaded.recipe.files.map((file) => ({ path: file.path, source: path14.join(loaded.dir, ...file.path.split("/")) }))
11679
+ ...loaded.recipe.files.map((file) => ({ path: file.path, source: path15.join(loaded.dir, ...file.path.split("/")) }))
11436
11680
  ];
11437
11681
  const files = await Promise.all(sources.map(async (file) => ({
11438
- path: path14.join(destination, ...file.path.split("/")),
11682
+ path: path15.join(destination, ...file.path.split("/")),
11439
11683
  data: await readFile7(file.source)
11440
11684
  })));
11441
11685
  if (!options.force) {
@@ -11467,7 +11711,7 @@ async function installRecipe(target, options = {}) {
11467
11711
  // src/pipeline/audit.ts
11468
11712
  import { readFile as readFile8 } from "fs/promises";
11469
11713
  import { existsSync as existsSync10 } from "fs";
11470
- import path15 from "path";
11714
+ import path16 from "path";
11471
11715
  function paletteDistance(asset, reference) {
11472
11716
  if (!asset.length || !reference.length) return 0;
11473
11717
  let total = 0;
@@ -11532,7 +11776,7 @@ async function auditStyle(loaded, specs, styleId, lock) {
11532
11776
  let referenceFromStyleImages = false;
11533
11777
  const refPalettes = [];
11534
11778
  for (const img of style.styleImages) {
11535
- const abs = path15.resolve(loaded.root, img.path);
11779
+ const abs = path16.resolve(loaded.root, img.path);
11536
11780
  if (!existsSync10(abs)) continue;
11537
11781
  try {
11538
11782
  refPalettes.push(extractPalette(decodePng(await readFile8(abs)), 12));
@@ -11613,16 +11857,16 @@ function hex(c) {
11613
11857
  // src/pixellab-utils.ts
11614
11858
  import { existsSync as existsSync11 } from "fs";
11615
11859
  import { mkdir as mkdir4, readFile as readFile9, writeFile as writeFile4 } from "fs/promises";
11616
- import path16 from "path";
11860
+ import path17 from "path";
11617
11861
  var UNZOOM_MIN_SIDE = 256;
11618
11862
  var UNZOOM_MAX_AREA = 2048 * 2048;
11619
11863
  function defaultUnzoomOut(input) {
11620
- const ext = path16.extname(input);
11864
+ const ext = path17.extname(input);
11621
11865
  return `${ext ? input.slice(0, -ext.length) : input}.unzoomed.png`;
11622
11866
  }
11623
11867
  async function unzoomFile(client2, input, opts = {}) {
11624
- const out = path16.resolve(opts.out ?? defaultUnzoomOut(input));
11625
- if (path16.resolve(input) === out) throw new Error("unzoom will not overwrite its own input; pass a different --out");
11868
+ const out = path17.resolve(opts.out ?? defaultUnzoomOut(input));
11869
+ if (path17.resolve(input) === out) throw new Error("unzoom will not overwrite its own input; pass a different --out");
11626
11870
  if (existsSync11(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
11627
11871
  const bytes = await readFile9(input);
11628
11872
  const metadata = imageMetadata(bytes);
@@ -11637,7 +11881,7 @@ async function unzoomFile(client2, input, opts = {}) {
11637
11881
  throw new Error(`${input} is ${width}x${height}; PixelLab's unzoom takes at most 2048x2048 worth of pixels`);
11638
11882
  }
11639
11883
  const res = await client2.unzoom({ image: { base64: bytes.toString("base64"), format }, quantize: opts.quantize });
11640
- await mkdir4(path16.dirname(out), { recursive: true });
11884
+ await mkdir4(path17.dirname(out), { recursive: true });
11641
11885
  await writeFile4(out, res.png);
11642
11886
  return { out, original: res.originalSize, unzoomed: res.unzoomedSize, zoomFactor: res.zoomFactor, usage: res.usage };
11643
11887
  }
@@ -11645,7 +11889,7 @@ var FONT_WEIGHTS = ["Bold", "Regular"];
11645
11889
  var FONT_GLYPH_SIZES = [8, 16, 32, 64];
11646
11890
  var FONT_COST_GENERATIONS = 25;
11647
11891
  function fontOutputPaths(out) {
11648
- const base = path16.resolve(out.replace(/\.(ttf|png)$/i, ""));
11892
+ const base = path17.resolve(out.replace(/\.(ttf|png)$/i, ""));
11649
11893
  return { ttf: `${base}.ttf`, atlas: `${base}.png` };
11650
11894
  }
11651
11895
  async function generateFont(client2, opts) {
@@ -11679,7 +11923,7 @@ async function generateFont(client2, opts) {
11679
11923
  client2.download(job.download_ttf_url),
11680
11924
  client2.download(job.download_atlas_url)
11681
11925
  ]);
11682
- await mkdir4(path16.dirname(ttf), { recursive: true });
11926
+ await mkdir4(path17.dirname(ttf), { recursive: true });
11683
11927
  await writeFile4(ttf, ttfBytes);
11684
11928
  await writeFile4(atlas, atlasBytes);
11685
11929
  return { jobId, ttf, atlas, glyphPx: job.glyph_px ?? null, usage: job.usage };
@@ -11694,7 +11938,7 @@ async function generateFont(client2, opts) {
11694
11938
  }
11695
11939
 
11696
11940
  // src/pipeline/quality-regression.ts
11697
- import path17 from "path";
11941
+ import path18 from "path";
11698
11942
  import { existsSync as existsSync12 } from "fs";
11699
11943
  import { readFile as readFile10 } from "fs/promises";
11700
11944
  import { z as z5 } from "zod";
@@ -11702,7 +11946,7 @@ var SHA256_RE2 = /^[0-9a-f]{64}$/;
11702
11946
  var HEX_RE = /^#[0-9a-f]{6}$/;
11703
11947
  var MAX_REDMEAN_DISTANCE = 765;
11704
11948
  var QualityPathSchema = z5.string().min(1).refine(
11705
- (value) => !path17.posix.isAbsolute(value) && !value.includes("\\"),
11949
+ (value) => !path18.posix.isAbsolute(value) && !value.includes("\\"),
11706
11950
  "expected a portable relative path"
11707
11951
  );
11708
11952
  var DEFAULT_QUALITY_TOLERANCES = {
@@ -11849,7 +12093,7 @@ function measureDecodedImageQuality(png, digest2) {
11849
12093
  };
11850
12094
  }
11851
12095
  async function measureImageQuality(file) {
11852
- const absolute = path17.resolve(file);
12096
+ const absolute = path18.resolve(file);
11853
12097
  let bytes;
11854
12098
  try {
11855
12099
  bytes = await readFile10(absolute);
@@ -11871,11 +12115,11 @@ function resolveQualityInputs(raw, inputsFilePath) {
11871
12115
  }
11872
12116
  const ids = parsed.data.map((entry) => entry.id);
11873
12117
  if (new Set(ids).size !== ids.length) throw new Error("--inputs contains a duplicate quality case id.");
11874
- const root = path17.dirname(path17.resolve(inputsFilePath));
12118
+ const root = path18.dirname(path18.resolve(inputsFilePath));
11875
12119
  return parsed.data.map((entry) => ({
11876
12120
  id: entry.id,
11877
- file: path17.resolve(root, entry.path),
11878
- ...entry.record ? { record: path17.resolve(root, entry.record) } : {},
12121
+ file: path18.resolve(root, entry.path),
12122
+ ...entry.record ? { record: path18.resolve(root, entry.record) } : {},
11879
12123
  tolerances: QualityToleranceSchema.parse({
11880
12124
  ...DEFAULT_QUALITY_TOLERANCES,
11881
12125
  ...entry.tolerances
@@ -11883,7 +12127,7 @@ function resolveQualityInputs(raw, inputsFilePath) {
11883
12127
  }));
11884
12128
  }
11885
12129
  function portableRelative2(from, to) {
11886
- return path17.relative(from, path17.resolve(to)).split(path17.sep).join("/") || ".";
12130
+ return path18.relative(from, path18.resolve(to)).split(path18.sep).join("/") || ".";
11887
12131
  }
11888
12132
  async function verifyRecordForImage(recordPath, imagePath) {
11889
12133
  const verification = await verifyArtifactBundle(recordPath);
@@ -11898,20 +12142,20 @@ async function verifyRecordForImage(recordPath, imagePath) {
11898
12142
  if (record.kind !== "refine" || options === null || typeof options !== "object" || options.schema !== "pixelkiln-quality") {
11899
12143
  throw new Error(`Quality record ${recordPath} is not a PixelKiln refinement record.`);
11900
12144
  }
11901
- const outputs = record.outputs.map((output) => path17.resolve(path17.dirname(recordPath), output.path));
11902
- if (!outputs.includes(path17.resolve(imagePath))) {
12145
+ const outputs = record.outputs.map((output) => path18.resolve(path18.dirname(recordPath), output.path));
12146
+ if (!outputs.includes(path18.resolve(imagePath))) {
11903
12147
  throw new Error(`Quality record ${recordPath} does not own ${imagePath}.`);
11904
12148
  }
11905
12149
  }
11906
12150
  async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
11907
12151
  if (!inputs.length) throw new Error("Cannot snapshot an empty quality baseline.");
11908
- const absolute = path17.resolve(baselinePath);
12152
+ const absolute = path18.resolve(baselinePath);
11909
12153
  for (const input of inputs) {
11910
- if (path17.resolve(input.file) === absolute || input.record && path17.resolve(input.record) === absolute) {
12154
+ if (path18.resolve(input.file) === absolute || input.record && path18.resolve(input.record) === absolute) {
11911
12155
  throw new Error(`Quality baseline output would overwrite an input: ${absolute}.`);
11912
12156
  }
11913
12157
  }
11914
- const root = path17.dirname(absolute);
12158
+ const root = path18.dirname(absolute);
11915
12159
  const cases = [];
11916
12160
  for (const input of [...inputs].sort((a, b) => a.id.localeCompare(b.id))) {
11917
12161
  const expected = await measureImageQuality(input.file);
@@ -11951,7 +12195,7 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
11951
12195
  return { baseline, path: absolute, changed: result.changed.length > 0 };
11952
12196
  }
11953
12197
  async function readQualityBaseline(baselinePath) {
11954
- const absolute = path17.resolve(baselinePath);
12198
+ const absolute = path18.resolve(baselinePath);
11955
12199
  let raw;
11956
12200
  try {
11957
12201
  raw = JSON.parse(await readFile10(absolute, "utf8"));
@@ -12024,12 +12268,12 @@ function regressionViolations(expected, actual, tolerances) {
12024
12268
  return { violations, warnings };
12025
12269
  }
12026
12270
  async function checkQualityBaseline(baselinePath) {
12027
- const absolute = path17.resolve(baselinePath);
12271
+ const absolute = path18.resolve(baselinePath);
12028
12272
  const baseline = await readQualityBaseline(absolute);
12029
- const root = path17.dirname(absolute);
12273
+ const root = path18.dirname(absolute);
12030
12274
  const cases = [];
12031
12275
  for (const entry of baseline.cases) {
12032
- const file = path17.resolve(root, entry.file);
12276
+ const file = path18.resolve(root, entry.file);
12033
12277
  let actual = null;
12034
12278
  const violations = [];
12035
12279
  const warnings = [];
@@ -12042,7 +12286,7 @@ async function checkQualityBaseline(baselinePath) {
12042
12286
  violations.push(message4(error));
12043
12287
  }
12044
12288
  if (entry.record) {
12045
- const recordPath = path17.resolve(root, entry.record.path);
12289
+ const recordPath = path18.resolve(root, entry.record.path);
12046
12290
  try {
12047
12291
  const recordHash = await sha256File(recordPath);
12048
12292
  if (recordHash !== entry.record.sha256) violations.push("quality record hash changed");
@@ -12080,7 +12324,7 @@ async function checkQualityBaseline(baselinePath) {
12080
12324
  // src/pipeline/cache-health.ts
12081
12325
  import { existsSync as existsSync16 } from "fs";
12082
12326
  import { readFile as readFile13, readdir as readdir2, rm as rm6 } from "fs/promises";
12083
- import path20 from "path";
12327
+ import path21 from "path";
12084
12328
 
12085
12329
  // src/cache.ts
12086
12330
  import { mkdir as mkdir5, readFile as readFile11, rename as rename4, rm as rm4, writeFile as writeFile5 } from "fs/promises";
@@ -12099,10 +12343,10 @@ function isSha256Hash(value) {
12099
12343
  function parseCache(value) {
12100
12344
  return HashCacheSchema.parse(value);
12101
12345
  }
12102
- async function loadCache(path36) {
12103
- if (!existsSync13(path36)) return { version: 1, hashes: {} };
12346
+ async function loadCache(path38) {
12347
+ if (!existsSync13(path38)) return { version: 1, hashes: {} };
12104
12348
  try {
12105
- const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile11(path36, "utf8")));
12349
+ const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile11(path38, "utf8")));
12106
12350
  if (!parsed.success) return { version: 1, hashes: {} };
12107
12351
  return {
12108
12352
  version: 1,
@@ -12114,18 +12358,18 @@ async function loadCache(path36) {
12114
12358
  return { version: 1, hashes: {} };
12115
12359
  }
12116
12360
  }
12117
- async function saveCache(path36, cache) {
12361
+ async function saveCache(path38, cache) {
12118
12362
  const sorted = {};
12119
12363
  for (const key of Object.keys(cache.hashes).sort()) {
12120
12364
  const hash = cache.hashes[key];
12121
12365
  if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
12122
12366
  sorted[key] = hash;
12123
12367
  }
12124
- await mkdir5(pathModule.dirname(pathModule.resolve(path36)), { recursive: true });
12125
- const tmp = `${path36}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
12368
+ await mkdir5(pathModule.dirname(pathModule.resolve(path38)), { recursive: true });
12369
+ const tmp = `${path38}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
12126
12370
  try {
12127
12371
  await writeFile5(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
12128
- await rename4(tmp, path36);
12372
+ await rename4(tmp, path38);
12129
12373
  } finally {
12130
12374
  await rm4(tmp, { force: true });
12131
12375
  }
@@ -12150,19 +12394,19 @@ function cachePathFor(lockPath) {
12150
12394
 
12151
12395
  // src/pipeline/history.ts
12152
12396
  import { existsSync as existsSync15 } from "fs";
12153
- import path19 from "path";
12397
+ import path20 from "path";
12154
12398
 
12155
12399
  // src/pipeline/fetch.ts
12156
- import { existsSync as existsSync14, readFileSync as readFileSync3 } from "fs";
12400
+ import { existsSync as existsSync14, readFileSync as readFileSync4 } from "fs";
12157
12401
  import { mkdir as mkdir6, readFile as readFile12, rename as rename5, rm as rm5, writeFile as writeFile6 } from "fs/promises";
12158
- import path18 from "path";
12402
+ import path19 from "path";
12159
12403
  async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12160
12404
  const log = opts.onProgress ?? (() => {
12161
12405
  });
12162
12406
  const result = { downloaded: 0, skipped: 0, failed: 0, ...opts.refresh ? { unchanged: 0 } : {} };
12163
12407
  const specByKey = new Map(specs.map((s) => [lockKey(s.styleId, s.assetId), s]));
12164
12408
  normalizeLockOutputPaths(lock, specs);
12165
- const cacheDir = opts.cacheDir === false ? null : path18.resolve(opts.cacheDir ?? path18.join(path18.dirname(lockPath), ".pixelkiln", "cache"));
12409
+ const cacheDir = opts.cacheDir === false ? null : path19.resolve(opts.cacheDir ?? path19.join(path19.dirname(lockPath), ".pixelkiln", "cache"));
12166
12410
  const pending = Object.entries(lock.entries).filter(([key, e]) => {
12167
12411
  if (e.provider !== provider.id || !specByKey.has(key)) return false;
12168
12412
  if (specByKey.get(key)?.mirror) return false;
@@ -12173,7 +12417,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12173
12417
  if (spec && e.specHash === spec.specHash && e.outputs.length && !postprocessCurrent(e, spec)) {
12174
12418
  return Boolean(opts.force) || e.outputs.every((output) => {
12175
12419
  const file = resolveOutputPath(output.path, spec.root);
12176
- return existsSync14(file) && sha256(readFileSync3(file)) === output.sha256;
12420
+ return existsSync14(file) && sha256(readFileSync4(file)) === output.sha256;
12177
12421
  });
12178
12422
  }
12179
12423
  if (!opts.repair) return false;
@@ -12181,7 +12425,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12181
12425
  return e.outputs.some((output) => {
12182
12426
  const file = resolveOutputPath(output.path, spec.root);
12183
12427
  if (!existsSync14(file)) return true;
12184
- const current = sha256(readFileSync3(file));
12428
+ const current = sha256(readFileSync4(file));
12185
12429
  if (current === output.sha256) return false;
12186
12430
  return Boolean(opts.force) || (e.supersededOutputs ?? []).some((old) => old.sha256 === current);
12187
12431
  });
@@ -12264,7 +12508,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12264
12508
  if (!source.url) throw new Error(`no durable source URL to refresh ${source.role ?? "asset"} from`);
12265
12509
  fresh = await provider.download(source.url);
12266
12510
  if (recorded && sha256(fresh) === recordedRaw && !reapply) {
12267
- log(` current ${path18.relative(process.cwd(), target)}`);
12511
+ log(` current ${path19.relative(process.cwd(), target)}`);
12268
12512
  outputs.push({ ...recorded, path: portableOutputPath(target, spec.root) });
12269
12513
  continue;
12270
12514
  }
@@ -12288,9 +12532,9 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12288
12532
  }
12289
12533
  if (reapply && !fresh) {
12290
12534
  if (!recorded.raw) raw = await readFile12(target);
12291
- log(` reapply ${path18.relative(process.cwd(), target)}`);
12535
+ log(` reapply ${path19.relative(process.cwd(), target)}`);
12292
12536
  } else {
12293
- log(` changed ${path18.relative(process.cwd(), target)} (upstream)`);
12537
+ log(` changed ${path19.relative(process.cwd(), target)} (upstream)`);
12294
12538
  }
12295
12539
  } else {
12296
12540
  const superseded = (entry.supersededOutputs ?? []).find(
@@ -12318,7 +12562,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12318
12562
  }
12319
12563
  }
12320
12564
  log(
12321
- ` replace ${path18.relative(process.cwd(), target)}` + (opts.force ? " (--force)" : " (previous tracked generation)")
12565
+ ` replace ${path19.relative(process.cwd(), target)}` + (opts.force ? " (--force)" : " (previous tracked generation)")
12322
12566
  );
12323
12567
  }
12324
12568
  }
@@ -12327,7 +12571,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12327
12571
  } else if (!raw && recorded && cacheDir) {
12328
12572
  if (!reapply) final = await readCachedMedia(cacheDir, recorded.sha256, expectedMediaType);
12329
12573
  if (!final && recordedRaw) raw = await readCachedMedia(cacheDir, recordedRaw, expectedMediaType);
12330
- if (final || raw) log(` cached ${path18.relative(process.cwd(), target)}`);
12574
+ if (final || raw) log(` cached ${path19.relative(process.cwd(), target)}`);
12331
12575
  }
12332
12576
  if (!final && !raw) {
12333
12577
  if (!source.url) {
@@ -12354,11 +12598,11 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12354
12598
  buf = processed.bytes;
12355
12599
  if (processed.changed) {
12356
12600
  rawHash = sha256(raw);
12357
- log(` snapped ${path18.relative(process.cwd(), target)} to ${wanted.palette.colors.length} colours`);
12601
+ log(` snapped ${path19.relative(process.cwd(), target)} to ${wanted.palette.colors.length} colours`);
12358
12602
  }
12359
12603
  }
12360
12604
  if (cacheDir) await cacheMedia(cacheDir, buf, mediaType);
12361
- await mkdir6(path18.dirname(target), { recursive: true });
12605
+ await mkdir6(path19.dirname(target), { recursive: true });
12362
12606
  const tmp = `${target}.pixelkiln.tmp`;
12363
12607
  await writeFile6(tmp, buf);
12364
12608
  outputs.push({
@@ -12372,7 +12616,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12372
12616
  await saveLock(lockPath, lock);
12373
12617
  await rename5(tmp, target);
12374
12618
  wrote = true;
12375
- log(` wrote ${path18.relative(process.cwd(), target)}`);
12619
+ log(` wrote ${path19.relative(process.cwd(), target)}`);
12376
12620
  }
12377
12621
  if (opts.refresh && !wrote) {
12378
12622
  if (rotated) {
@@ -12416,7 +12660,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12416
12660
  return result;
12417
12661
  }
12418
12662
  async function readCachedMedia(cacheDir, hash, mediaType) {
12419
- const file = path18.join(cacheDir, cacheFileName(hash, mediaType));
12663
+ const file = path19.join(cacheDir, cacheFileName(hash, mediaType));
12420
12664
  if (!existsSync14(file)) return null;
12421
12665
  try {
12422
12666
  const buf = await readFile12(file);
@@ -12430,7 +12674,7 @@ async function readCachedMedia(cacheDir, hash, mediaType) {
12430
12674
  async function cacheMedia(cacheDir, buf, mediaType, knownHash) {
12431
12675
  const hash = knownHash ?? sha256(buf);
12432
12676
  validateMedia(buf, mediaType);
12433
- const file = path18.join(cacheDir, cacheFileName(hash, mediaType));
12677
+ const file = path19.join(cacheDir, cacheFileName(hash, mediaType));
12434
12678
  if (await readCachedMedia(cacheDir, hash, mediaType)) return;
12435
12679
  await mkdir6(cacheDir, { recursive: true });
12436
12680
  const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
@@ -12560,7 +12804,7 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
12560
12804
  if (!existsSync15(file)) continue;
12561
12805
  if (await sha256File(file) !== entry.outputs[i].sha256 && !opts.force) {
12562
12806
  throw new OverwriteRefusedError(
12563
- `${path19.relative(process.cwd(), file)} was changed after download; keep it with pixelkiln edit, or pass --force to replace it`,
12807
+ `${path20.relative(process.cwd(), file)} was changed after download; keep it with pixelkiln edit, or pass --force to replace it`,
12564
12808
  [file]
12565
12809
  );
12566
12810
  }
@@ -12613,10 +12857,10 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
12613
12857
  // src/pipeline/cache-health.ts
12614
12858
  async function inspectCaches(lock, lockPath, options = {}) {
12615
12859
  if (options.prune && !existsSync16(lockPath)) {
12616
- throw new Error(`Refusing to prune without an existing lockfile at ${path20.resolve(lockPath)}`);
12860
+ throw new Error(`Refusing to prune without an existing lockfile at ${path21.resolve(lockPath)}`);
12617
12861
  }
12618
- const contentDir = path20.resolve(path20.dirname(lockPath), ".pixelkiln", "cache");
12619
- const remotePath = path20.resolve(cachePathFor(lockPath));
12862
+ const contentDir = path21.resolve(path21.dirname(lockPath), ".pixelkiln", "cache");
12863
+ const remotePath = path21.resolve(cachePathFor(lockPath));
12620
12864
  const referenced = referencedHashes(lock);
12621
12865
  let content = await inspectContentCache(contentDir, referenced);
12622
12866
  let remoteHashes = await inspectRemoteHashCache(remotePath);
@@ -12628,7 +12872,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
12628
12872
  ]);
12629
12873
  for (const name of names) {
12630
12874
  try {
12631
- await rm6(path20.join(contentDir, name), { force: true });
12875
+ await rm6(path21.join(contentDir, name), { force: true });
12632
12876
  removed.contentFiles++;
12633
12877
  } catch {
12634
12878
  }
@@ -12676,7 +12920,7 @@ async function inspectContentCache(contentDir, referenced) {
12676
12920
  continue;
12677
12921
  }
12678
12922
  report.files++;
12679
- const file = path20.join(contentDir, entry.name);
12923
+ const file = path21.join(contentDir, entry.name);
12680
12924
  const mediaType = mediaTypeFromExtension(entry.name);
12681
12925
  const expected = mediaType ? entry.name.slice(0, -4) : "";
12682
12926
  let bytes;
@@ -12743,20 +12987,20 @@ async function inspectRemoteHashCache(remotePath) {
12743
12987
  }
12744
12988
 
12745
12989
  // src/pipeline/pack.ts
12746
- import { readFileSync as readFileSync4 } from "fs";
12747
- import path21 from "path";
12990
+ import { readFileSync as readFileSync5 } from "fs";
12991
+ import path22 from "path";
12748
12992
  var DEFAULT_FRAME_SET_FPS = 12;
12749
12993
  function resolvePackInputs(raw, inputsFilePath) {
12750
12994
  if (!Array.isArray(raw) || !raw.length) {
12751
12995
  throw new Error("--inputs must be a non-empty JSON array of { id, path }");
12752
12996
  }
12753
- const baseDir = path21.dirname(path21.resolve(inputsFilePath));
12997
+ const baseDir = path22.dirname(path22.resolve(inputsFilePath));
12754
12998
  return raw.map((entry, i) => {
12755
12999
  const e = entry;
12756
13000
  if (typeof e.id !== "string" || typeof e.path !== "string") {
12757
13001
  throw new Error(`--inputs[${i}] needs string "id" and "path"`);
12758
13002
  }
12759
- return { id: e.id, path: path21.resolve(baseDir, e.path) };
13003
+ return { id: e.id, path: path22.resolve(baseDir, e.path) };
12760
13004
  });
12761
13005
  }
12762
13006
  function packSprites(inputs, options = {}) {
@@ -12777,7 +13021,7 @@ function packSprites(inputs, options = {}) {
12777
13021
  for (const input of ordered) {
12778
13022
  let sourceHash = null;
12779
13023
  try {
12780
- const bytes = readFileSync4(input.path);
13024
+ const bytes = readFileSync5(input.path);
12781
13025
  sourceHash = sha256(bytes);
12782
13026
  const png = decodePng(bytes);
12783
13027
  sprites.push({ id: input.id, width: png.width, height: png.height, pixels: png.pixels });
@@ -12840,7 +13084,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
12840
13084
  }
12841
13085
  const inputs2 = Object.entries(options.sourceOverrides).map(([id, source]) => ({
12842
13086
  id,
12843
- path: path21.resolve(source)
13087
+ path: path22.resolve(source)
12844
13088
  }));
12845
13089
  if (!inputs2.length) throw new Error(`No approved quality outputs for style "${styleId}".`);
12846
13090
  const packed2 = packSprites(inputs2, options);
@@ -12864,7 +13108,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
12864
13108
  let outputs = resolveEntryOutputs(entry, id, manifestDir);
12865
13109
  const source = sources[id];
12866
13110
  if (source && outputs.length <= 1) {
12867
- inputs.push({ id, path: path21.resolve(manifestDir, source) });
13111
+ inputs.push({ id, path: path22.resolve(manifestDir, source) });
12868
13112
  continue;
12869
13113
  }
12870
13114
  if (source && sourceIsStem(source, entry, manifestDir)) {
@@ -12898,7 +13142,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
12898
13142
  }
12899
13143
  }
12900
13144
  for (const [id, source] of Object.entries(sources)) {
12901
- if (!locked.has(id)) inputs.push({ id, path: path21.resolve(manifestDir, source) });
13145
+ if (!locked.has(id)) inputs.push({ id, path: path22.resolve(manifestDir, source) });
12902
13146
  }
12903
13147
  if (!inputs.length) {
12904
13148
  throw new Error(`No readable sprites for style "${styleId}"; ${noOutput.length} skipped.`);
@@ -12935,7 +13179,7 @@ function mountSprites(placements, options) {
12935
13179
  for (const p of placements) {
12936
13180
  let sourceHash = null;
12937
13181
  try {
12938
- const bytes = readFileSync4(p.path);
13182
+ const bytes = readFileSync5(p.path);
12939
13183
  sourceHash = sha256(bytes);
12940
13184
  const png = decodePng(bytes);
12941
13185
  if (png.width > cellWidth || png.height > cellHeight) {
@@ -13006,7 +13250,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
13006
13250
  const source = sources[id];
13007
13251
  const entry = lock.entries[`${styleId}/${id}`];
13008
13252
  if (source && !(entry && sourceIsStem(source, entry, manifestDir))) {
13009
- placements.push({ id, path: path21.resolve(manifestDir, source), cell });
13253
+ placements.push({ id, path: path22.resolve(manifestDir, source), cell });
13010
13254
  continue;
13011
13255
  }
13012
13256
  if (source && entry) {
@@ -13039,7 +13283,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
13039
13283
  `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}`
13040
13284
  );
13041
13285
  }
13042
- const basePng = mount.base ? readFileSync4(path21.resolve(manifestDir, mount.base)) : void 0;
13286
+ const basePng = mount.base ? readFileSync5(path22.resolve(manifestDir, mount.base)) : void 0;
13043
13287
  const mounted = mountSprites(placements, {
13044
13288
  cellWidth: mount.cellWidth,
13045
13289
  cellHeight: mount.cellHeight,
@@ -13049,7 +13293,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
13049
13293
  ...mounted,
13050
13294
  sources: mount.base ? [{
13051
13295
  id: "$base",
13052
- path: path21.resolve(manifestDir, mount.base),
13296
+ path: path22.resolve(manifestDir, mount.base),
13053
13297
  sha256: sha256(basePng),
13054
13298
  included: true
13055
13299
  }, ...mounted.sources] : mounted.sources,
@@ -13641,6 +13885,10 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
13641
13885
  ...spec.revision.keypointsSha256 ? { keypointsSha256: spec.revision.keypointsSha256 } : {},
13642
13886
  ...spec.revision.skeletonTemplate ? { skeletonTemplate: spec.revision.skeletonTemplate } : {},
13643
13887
  ...spec.revision.description ? { description: spec.revision.description } : {},
13888
+ ...spec.revision.removalTask ? { removalTask: spec.revision.removalTask } : {},
13889
+ ...spec.revision.driftThreshold == null ? {} : { driftThreshold: spec.revision.driftThreshold },
13890
+ ...spec.revision.subjectDescription ? { subjectDescription: spec.revision.subjectDescription } : {},
13891
+ ...spec.revision.initialPose ? { initialPose: spec.revision.initialPose } : {},
13644
13892
  ...spec.revision.engine ? { engine: spec.revision.engine } : {}
13645
13893
  } : null,
13646
13894
  outfit: spec.character?.kind === "outfit" && outfitParentEntry ? {
@@ -13857,7 +14105,7 @@ async function poll(provider, lock, lockPath, opts = {}) {
13857
14105
  // src/pipeline/doctor.ts
13858
14106
  import { constants, existsSync as existsSync17 } from "fs";
13859
14107
  import { access, stat as stat2 } from "fs/promises";
13860
- import path22 from "path";
14108
+ import path23 from "path";
13861
14109
  async function doctor(loaded, specs, lock, lockPath, opts = {}) {
13862
14110
  const checks = [];
13863
14111
  const add = (id, level, message6) => checks.push({ id, level, message: message6 });
@@ -13868,8 +14116,8 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
13868
14116
  );
13869
14117
  add("lock", "ok", `${Object.keys(lock.entries).length} valid v2 lock entr(ies)`);
13870
14118
  const dirs = [...new Set(specs.flatMap((spec) => [
13871
- path22.dirname(spec.outFile),
13872
- ...spec.quality ? [path22.dirname(spec.quality.outFile)] : []
14119
+ path23.dirname(spec.outFile),
14120
+ ...spec.quality ? [path23.dirname(spec.quality.outFile)] : []
13873
14121
  ]))];
13874
14122
  const unwritable = [];
13875
14123
  for (const dir of dirs) {
@@ -13892,7 +14140,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
13892
14140
  for (let index = 0; index < entry.outputs.length; index++) {
13893
14141
  const output = entry.outputs[index];
13894
14142
  const spec = specByKey.get(key);
13895
- const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, path22.dirname(lockPath));
14143
+ const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, path23.dirname(lockPath));
13896
14144
  const owner = outputOwners.get(absolute);
13897
14145
  if (owner && owner !== key) duplicateOutputs.push(`${output.path} (${owner}, ${key})`);
13898
14146
  outputOwners.set(absolute, key);
@@ -13907,7 +14155,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
13907
14155
  } else {
13908
14156
  add("lock-outputs", "ok", `${outputOwners.size} recorded output path(s) are uniquely owned`);
13909
14157
  }
13910
- const cacheDir = path22.join(path22.dirname(lockPath), ".pixelkiln", "cache");
14158
+ const cacheDir = path23.join(path23.dirname(lockPath), ".pixelkiln", "cache");
13911
14159
  const stranded = [];
13912
14160
  const invalidState = [];
13913
14161
  const staleMapUrls = [];
@@ -13923,7 +14171,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
13923
14171
  const hasSource = Boolean(entry.sourceUrl || entry.sourceUrls?.length);
13924
14172
  let hasCache = false;
13925
14173
  for (const output of entry.outputs) {
13926
- const cached = path22.join(
14174
+ const cached = path23.join(
13927
14175
  cacheDir,
13928
14176
  cacheFileName(output.sha256, output.mediaType ?? MediaType.PNG)
13929
14177
  );
@@ -14023,7 +14271,7 @@ async function nearestExistingDirectory(start) {
14023
14271
  if ((await stat2(current)).isDirectory()) return current;
14024
14272
  } catch {
14025
14273
  }
14026
- const parent = path22.dirname(current);
14274
+ const parent = path23.dirname(current);
14027
14275
  if (parent === current) return current;
14028
14276
  current = parent;
14029
14277
  }
@@ -14032,7 +14280,7 @@ async function nearestExistingDirectory(start) {
14032
14280
  // src/pipeline/adopt.ts
14033
14281
  import { existsSync as existsSync18 } from "fs";
14034
14282
  import { readFile as readFile14, writeFile as writeFile7 } from "fs/promises";
14035
- import path23 from "path";
14283
+ import path24 from "path";
14036
14284
  async function adopt(provider, specs, lock, lockPath, opts = {}) {
14037
14285
  const log = opts.onProgress ?? (() => {
14038
14286
  });
@@ -14171,7 +14419,7 @@ import { readFile as readFile17 } from "fs/promises";
14171
14419
  // src/pipeline/init.ts
14172
14420
  import { readdir as readdir3, readFile as readFile15, writeFile as writeFile8 } from "fs/promises";
14173
14421
  import { existsSync as existsSync19 } from "fs";
14174
- import path24 from "path";
14422
+ import path25 from "path";
14175
14423
  function pngSize(buf) {
14176
14424
  if (buf.length < 24) return null;
14177
14425
  const isPng = buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834;
@@ -14185,7 +14433,7 @@ function slugify(value) {
14185
14433
  async function walk(dir, exclude) {
14186
14434
  const out = [];
14187
14435
  for (const entry of await readdir3(dir, { withFileTypes: true })) {
14188
- const full = path24.join(dir, entry.name);
14436
+ const full = path25.join(dir, entry.name);
14189
14437
  if (exclude.some((x) => full.includes(x))) continue;
14190
14438
  if (entry.isDirectory()) out.push(...await walk(full, exclude));
14191
14439
  else if (entry.name.toLowerCase().endsWith(".png")) out.push(full);
@@ -14204,9 +14452,9 @@ async function scanAssets(root, opts = {}) {
14204
14452
  skipped.push(`${file} (not a readable PNG)`);
14205
14453
  continue;
14206
14454
  }
14207
- const rel = path24.relative(root, file);
14208
- const category = path24.dirname(rel) === "." ? "" : path24.dirname(rel);
14209
- let id = slugify(path24.basename(rel));
14455
+ const rel = path25.relative(root, file);
14456
+ const category = path25.dirname(rel) === "." ? "" : path25.dirname(rel);
14457
+ let id = slugify(path25.basename(rel));
14210
14458
  if (seen.has(id)) {
14211
14459
  const n = seen.get(id) + 1;
14212
14460
  seen.set(id, n);
@@ -14929,15 +15177,15 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
14929
15177
  }
14930
15178
 
14931
15179
  // src/gallery/snapshot.ts
14932
- import { createHash as createHash4 } from "crypto";
14933
- import { existsSync as existsSync25 } from "fs";
14934
- import { readFile as readFile23, stat as stat3 } from "fs/promises";
14935
- import path31 from "path";
15180
+ import { createHash as createHash5 } from "crypto";
15181
+ import { existsSync as existsSync26 } from "fs";
15182
+ import { readFile as readFile24, stat as stat3 } from "fs/promises";
15183
+ import path33 from "path";
14936
15184
 
14937
15185
  // src/workspace.ts
14938
15186
  import { mkdir as mkdir7, readFile as readFile18, rename as rename6, rm as rm7, writeFile as writeFile9 } from "fs/promises";
14939
15187
  import { existsSync as existsSync21 } from "fs";
14940
- import path25 from "path";
15188
+ import path26 from "path";
14941
15189
  import { z as z7 } from "zod";
14942
15190
  var WorkspaceProjectSchema = z7.object({
14943
15191
  id: z7.string().min(1),
@@ -14979,7 +15227,7 @@ async function saveWorkspace(workspacePath, ws) {
14979
15227
  version: 1,
14980
15228
  projects: [...ws.projects].sort((a, b) => a.id.localeCompare(b.id))
14981
15229
  };
14982
- await mkdir7(path25.dirname(path25.resolve(workspacePath)), { recursive: true });
15230
+ await mkdir7(path26.dirname(path26.resolve(workspacePath)), { recursive: true });
14983
15231
  const tmp = `${workspacePath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
14984
15232
  try {
14985
15233
  await writeFile9(tmp, JSON.stringify(sorted, null, 2) + "\n");
@@ -14989,12 +15237,12 @@ async function saveWorkspace(workspacePath, ws) {
14989
15237
  }
14990
15238
  }
14991
15239
  function toPortablePath(dir, absolute) {
14992
- return path25.relative(dir, absolute).split(path25.sep).join("/");
15240
+ return path26.relative(dir, absolute).split(path26.sep).join("/");
14993
15241
  }
14994
15242
  function resolveProject(dir, project) {
14995
15243
  return {
14996
- manifestPath: path25.resolve(dir, project.manifest.split("/").join(path25.sep)),
14997
- lockPath: path25.resolve(dir, project.lock.split("/").join(path25.sep))
15244
+ manifestPath: path26.resolve(dir, project.manifest.split("/").join(path26.sep)),
15245
+ lockPath: path26.resolve(dir, project.lock.split("/").join(path26.sep))
14998
15246
  };
14999
15247
  }
15000
15248
  function validateWorkspace(ws, dir) {
@@ -15007,7 +15255,7 @@ function validateWorkspace(ws, dir) {
15007
15255
  const { manifestPath, lockPath } = resolveProject(dir, project);
15008
15256
  lockOwners.set(lockPath, [...lockOwners.get(lockPath) ?? [], project.id]);
15009
15257
  manifestOwners.set(manifestPath, [...manifestOwners.get(manifestPath) ?? [], project.id]);
15010
- if (path25.isAbsolute(project.manifest) || path25.isAbsolute(project.lock)) {
15258
+ if (path26.isAbsolute(project.manifest) || path26.isAbsolute(project.lock)) {
15011
15259
  diagnostics.push({
15012
15260
  id: "absolute-path",
15013
15261
  level: "warning",
@@ -15068,15 +15316,16 @@ function validateWorkspace(ws, dir) {
15068
15316
  }
15069
15317
 
15070
15318
  // src/gallery/edit.ts
15071
- import { readFile as readFile22, rename as rename9, writeFile as writeFile14 } from "fs/promises";
15072
- import path30 from "path";
15073
- import { z as z12 } from "zod";
15319
+ import { readFile as readFile23, rename as rename9, writeFile as writeFile15 } from "fs/promises";
15320
+ import path32 from "path";
15321
+ import { z as z13 } from "zod";
15074
15322
 
15075
15323
  // src/manifest-edit.ts
15076
15324
  import { readFile as readFile19, rename as rename7, rm as rm8, writeFile as writeFile10 } from "fs/promises";
15077
- import path26 from "path";
15325
+ import path27 from "path";
15078
15326
  import { z as z8 } from "zod";
15079
15327
  var HexSha = z8.string().regex(/^[0-9a-f]{64}$/);
15328
+ var ProjectFileSchema = z8.string().min(1).refine((file) => !path27.isAbsolute(file) && !file.split(/[\\/]/).includes(".."), "must be a path inside the project");
15080
15329
  var AssetPatchSchema = z8.object({
15081
15330
  prompt: z8.string().optional(),
15082
15331
  /** Per-style override for the style being viewed; null removes it. */
@@ -15125,10 +15374,11 @@ var NewAssetSchema = z8.object({
15125
15374
  styles: z8.array(z8.string().min(1)).optional(),
15126
15375
  /**
15127
15376
  * A controlled regeneration of another asset in the same manifest,
15128
- * instead of a fresh generation. `inpaint` needs a mask upload and
15129
- * `interpolate` an ending-keyframe image the gallery does not offer, so
15130
- * those two are hand-edited; every other mode can be created here
15131
- * (`animate-skeleton` after the gallery writes its keypoints file).
15377
+ * instead of a fresh generation. Every mode but `outpaint` can be
15378
+ * created here: `inpaint` names a mask file the gallery's mask editor
15379
+ * wrote first, `interpolate` names an existing image as its ending
15380
+ * keyframe, and `animate-skeleton` names the keypoints file the gallery
15381
+ * wrote first.
15132
15382
  */
15133
15383
  revision: z8.discriminatedUnion("mode", [
15134
15384
  z8.object({
@@ -15145,15 +15395,38 @@ var NewAssetSchema = z8.object({
15145
15395
  dithering: RevisionDitheringSchema.optional(),
15146
15396
  ditheringStrength: z8.number().min(0).max(10).optional()
15147
15397
  }).strict(),
15398
+ z8.object({
15399
+ mode: z8.literal("inpaint"),
15400
+ from: z8.string().min(1),
15401
+ /** Black-and-white PNG the same size as the parent; white is repainted. */
15402
+ mask: ProjectFileSchema
15403
+ }).strict(),
15404
+ z8.object({
15405
+ mode: z8.literal("interpolate"),
15406
+ from: z8.string().min(1),
15407
+ /** The ending keyframe: an image already in the project, the parent's size. */
15408
+ lastFrame: ProjectFileSchema,
15409
+ fps: z8.number().int().min(1).max(60).optional()
15410
+ }).strict(),
15148
15411
  z8.object({ mode: z8.literal("correct-pixelart"), from: z8.string().min(1), strength: z8.number().min(0).max(1).optional() }).strict(),
15412
+ z8.object({
15413
+ mode: z8.literal("remove-background"),
15414
+ from: z8.string().min(1),
15415
+ removalTask: z8.enum(["simple", "complex"]).optional(),
15416
+ /** A short description of the foreground, sent as PixelLab's text hint. */
15417
+ description: z8.string().min(1).optional()
15418
+ }).strict(),
15149
15419
  z8.object({
15150
15420
  mode: z8.enum(["animate", "animate-pixminimax"]),
15151
15421
  from: z8.string().min(1),
15152
15422
  frames: z8.number().int().min(4).max(40).refine((n) => n % 2 === 0, "frames must be even").optional(),
15153
15423
  fps: z8.number().int().min(1).max(60).optional(),
15154
15424
  enhancePrompt: z8.boolean().optional(),
15155
- /** `animate-pixminimax` only; the loader refuses it on `animate`. */
15156
- direction: CharacterDirectionSchema.optional()
15425
+ driftThreshold: z8.number().min(0).optional(),
15426
+ /** `animate-pixminimax` only; the loader refuses these on `animate`. */
15427
+ direction: CharacterDirectionSchema.optional(),
15428
+ subjectDescription: z8.string().min(1).max(300).optional(),
15429
+ initialPose: z8.string().min(1).max(300).optional()
15157
15430
  }).strict(),
15158
15431
  z8.object({
15159
15432
  mode: z8.literal("edit-animation"),
@@ -15471,7 +15744,7 @@ function applyEdit(raw, edit) {
15471
15744
  }
15472
15745
  }
15473
15746
  async function withCandidate(manifestPath, edit, use, unchanged) {
15474
- const absolute = path26.resolve(manifestPath);
15747
+ const absolute = path27.resolve(manifestPath);
15475
15748
  const original = await readFile19(absolute, "utf8");
15476
15749
  if (sha256(original) !== edit.expectedSha256) throw new ManifestDriftError(absolute);
15477
15750
  let raw;
@@ -15485,9 +15758,9 @@ async function withCandidate(manifestPath, edit, use, unchanged) {
15485
15758
  applyEdit(raw, edit);
15486
15759
  const next = serializeLike(original, raw);
15487
15760
  if (next === original) return unchanged(absolute, original);
15488
- const tmp = path26.join(
15489
- path26.dirname(absolute),
15490
- `.${path26.basename(absolute)}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`
15761
+ const tmp = path27.join(
15762
+ path27.dirname(absolute),
15763
+ `.${path27.basename(absolute)}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`
15491
15764
  );
15492
15765
  try {
15493
15766
  await writeFile10(tmp, next);
@@ -15518,16 +15791,16 @@ async function applyManifestEdit(manifestPath, edit) {
15518
15791
  // src/pipeline/hand-edit.ts
15519
15792
  import { existsSync as existsSync22 } from "fs";
15520
15793
  import { copyFile, mkdir as mkdir8, readFile as readFile20, rename as rename8, rm as rm9, writeFile as writeFile11 } from "fs/promises";
15521
- import path27 from "path";
15794
+ import path28 from "path";
15522
15795
  import { z as z9 } from "zod";
15523
15796
  var HAND_EDIT_DIR = "edits";
15524
15797
  function handEditPath(loaded, spec) {
15525
15798
  const style = loaded.manifest.styles[spec.styleId];
15526
- const outDir = path27.resolve(loaded.root, style?.outDir ?? ".");
15527
- const relative = path27.relative(outDir, spec.outFile);
15528
- const inside = relative && !relative.startsWith("..") && !path27.isAbsolute(relative);
15529
- const file = inside ? relative : path27.basename(spec.outFile);
15530
- return path27.join(outDir, HAND_EDIT_DIR, file.replace(/\.gif$/i, ".png"));
15799
+ const outDir = path28.resolve(loaded.root, style?.outDir ?? ".");
15800
+ const relative = path28.relative(outDir, spec.outFile);
15801
+ const inside = relative && !relative.startsWith("..") && !path28.isAbsolute(relative);
15802
+ const file = inside ? relative : path28.basename(spec.outFile);
15803
+ return path28.join(outDir, HAND_EDIT_DIR, file.replace(/\.gif$/i, ".png"));
15531
15804
  }
15532
15805
  function handEditBases(spec, lock) {
15533
15806
  const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
@@ -15573,7 +15846,7 @@ async function startHandEdit(loaded, lock, spec, opts = {}) {
15573
15846
  `${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`
15574
15847
  );
15575
15848
  }
15576
- const existing = spec.source ? path27.resolve(loaded.root, spec.source) : null;
15849
+ const existing = spec.source ? path28.resolve(loaded.root, spec.source) : null;
15577
15850
  const editPath = existing ?? handEditPath(loaded, spec);
15578
15851
  const members = handEditMembers(loaded, spec, lock, editPath);
15579
15852
  let created = false;
@@ -15584,7 +15857,7 @@ async function startHandEdit(loaded, lock, spec, opts = {}) {
15584
15857
  `${spec.styleId}/${spec.assetId} has no generated PNG on disk to start from; generate or restore it first`
15585
15858
  );
15586
15859
  }
15587
- await mkdir8(path27.dirname(member.path), { recursive: true });
15860
+ await mkdir8(path28.dirname(member.path), { recursive: true });
15588
15861
  await copyFile(member.base, member.path);
15589
15862
  created = true;
15590
15863
  }
@@ -15722,7 +15995,7 @@ async function saveHandEdit(loaded, lock, spec, input) {
15722
15995
  editor: input.editor,
15723
15996
  protocol: input.protocol,
15724
15997
  savedAt: (input.now ?? /* @__PURE__ */ new Date()).toISOString(),
15725
- project: projectPath ? path27.basename(projectPath) : null,
15998
+ project: projectPath ? path28.basename(projectPath) : null,
15726
15999
  outputs: incoming.map((frame, index) => ({
15727
16000
  role: frame.role,
15728
16001
  basedOn: basedOnHash(entry, index),
@@ -15766,7 +16039,7 @@ function openInEditor(file, editor = process.env.PIXELKILN_EDITOR) {
15766
16039
  // src/gallery/upload.ts
15767
16040
  import { existsSync as existsSync23 } from "fs";
15768
16041
  import { mkdir as mkdir9, writeFile as writeFile12 } from "fs/promises";
15769
- import path28 from "path";
16042
+ import path29 from "path";
15770
16043
  import { z as z10 } from "zod";
15771
16044
  var MAX_UPLOAD_BYTES = 8 * 1024 * 1024;
15772
16045
  var UploadImageSchema = z10.object({
@@ -15779,12 +16052,12 @@ var UploadImageSchema = z10.object({
15779
16052
  overwrite: z10.boolean().optional()
15780
16053
  }).strict();
15781
16054
  async function saveProjectImage(root, request) {
15782
- const file = path28.resolve(root, request.path);
15783
- const relative = path28.relative(root, file);
15784
- if (relative.startsWith("..") || path28.isAbsolute(relative)) {
16055
+ const file = path29.resolve(root, request.path);
16056
+ const relative = path29.relative(root, file);
16057
+ if (relative.startsWith("..") || path29.isAbsolute(relative)) {
15785
16058
  throw new ManifestEditError("an upload must stay inside the project");
15786
16059
  }
15787
- const extension = path28.extname(file).toLowerCase();
16060
+ const extension = path29.extname(file).toLowerCase();
15788
16061
  const expected = extension === ".png" ? "png" : extension === ".jpg" || extension === ".jpeg" ? "jpeg" : null;
15789
16062
  if (!expected) throw new ManifestEditError("an upload must be a .png, .jpg, or .jpeg path");
15790
16063
  const bytes = Buffer.from(request.base64, "base64");
@@ -15797,69 +16070,125 @@ async function saveProjectImage(root, request) {
15797
16070
  throw new ManifestEditError(`the upload is a ${metadata.format.toUpperCase()}; name it ${metadata.format === "png" ? ".png" : ".jpg"}`);
15798
16071
  }
15799
16072
  if (existsSync23(file) && !request.overwrite) {
15800
- throw new ManifestEditError(`${relative.split(path28.sep).join("/")} already exists; choose another name, or replace it`);
16073
+ throw new ManifestEditError(`${relative.split(path29.sep).join("/")} already exists; choose another name, or replace it`);
15801
16074
  }
15802
- await mkdir9(path28.dirname(file), { recursive: true });
16075
+ await mkdir9(path29.dirname(file), { recursive: true });
15803
16076
  await writeFile12(file, bytes);
15804
- return relative.split(path28.sep).join("/");
16077
+ return relative.split(path29.sep).join("/");
15805
16078
  }
15806
16079
 
15807
- // src/gallery/skeleton.ts
16080
+ // src/gallery/image-revision.ts
15808
16081
  import { existsSync as existsSync24 } from "fs";
15809
- import { mkdir as mkdir10, readFile as readFile21, rm as rm10, writeFile as writeFile13 } from "fs/promises";
15810
- import path29 from "path";
16082
+ import { readFile as readFile21, rm as rm10, writeFile as writeFile13 } from "fs/promises";
16083
+ import path30 from "path";
15811
16084
  import { z as z11 } from "zod";
15812
- var SkeletonEstimateRequestSchema = z11.object({
15813
- project: z11.string().min(1).optional(),
15814
- styleId: z11.string().min(1),
15815
- assetId: z11.string().min(1),
15816
- frames: z11.number().int().min(3).max(15).optional()
15817
- }).strict();
15818
- var SkeletonAnimationRequestSchema = z11.object({
15819
- action: z11.literal("create-skeleton-animation"),
16085
+ var InpaintRevisionRequestSchema = z11.object({
16086
+ action: z11.literal("create-inpaint-revision"),
15820
16087
  project: z11.string().min(1).optional(),
15821
16088
  expectedSha256: z11.string().regex(/^[0-9a-f]{64}$/),
15822
16089
  styleId: z11.string().min(1),
15823
- /** The sprite the animation poses. */
16090
+ /** The sprite being repainted. */
15824
16091
  from: z11.string().min(1),
15825
16092
  assetId: z11.string().min(1).regex(/^[^/\\]+$/, "asset ids cannot contain slashes"),
15826
- /** The motion, sent as PixelLab's `action`. */
16093
+ /** What to paint into the masked area. */
15827
16094
  prompt: z11.string().min(1),
16095
+ /** Manifest-relative `.png` destination for the mask. */
16096
+ maskPath: z11.string().min(1),
16097
+ maskBase64: z11.string().regex(/^[A-Za-z0-9+/]*={0,2}$/),
16098
+ /** Replace a file already at `maskPath`. */
16099
+ overwrite: z11.boolean().optional()
16100
+ }).strict();
16101
+ async function createInpaintRevision(manifestPath, request) {
16102
+ const root = path30.dirname(path30.resolve(manifestPath));
16103
+ if (path30.extname(request.maskPath).toLowerCase() !== ".png") {
16104
+ throw new ManifestEditError("the mask must be a .png path inside the project");
16105
+ }
16106
+ const bytes = Buffer.from(request.maskBase64, "base64");
16107
+ const metadata = imageMetadata(bytes);
16108
+ if (!metadata || metadata.format !== "png") throw new ManifestEditError("the mask is not a readable PNG");
16109
+ const file = path30.resolve(root, request.maskPath);
16110
+ const previous = existsSync24(file) ? await readFile21(file) : null;
16111
+ const mask = await saveProjectImage(root, {
16112
+ action: "upload-image",
16113
+ path: request.maskPath,
16114
+ base64: request.maskBase64,
16115
+ ...request.overwrite ? { overwrite: true } : {}
16116
+ });
16117
+ try {
16118
+ await applyManifestEdit(manifestPath, {
16119
+ action: "add-asset",
16120
+ assetId: request.assetId,
16121
+ expectedSha256: request.expectedSha256,
16122
+ asset: {
16123
+ prompt: request.prompt,
16124
+ styles: [request.styleId],
16125
+ revision: { mode: "inpaint", from: request.from, mask }
16126
+ }
16127
+ });
16128
+ } catch (error) {
16129
+ if (previous) await writeFile13(file, previous);
16130
+ else await rm10(file, { force: true });
16131
+ throw error;
16132
+ }
16133
+ return mask;
16134
+ }
16135
+
16136
+ // src/gallery/skeleton.ts
16137
+ import { existsSync as existsSync25 } from "fs";
16138
+ import { mkdir as mkdir10, readFile as readFile22, rm as rm11, writeFile as writeFile14 } from "fs/promises";
16139
+ import path31 from "path";
16140
+ import { z as z12 } from "zod";
16141
+ var SkeletonEstimateRequestSchema = z12.object({
16142
+ project: z12.string().min(1).optional(),
16143
+ styleId: z12.string().min(1),
16144
+ assetId: z12.string().min(1),
16145
+ frames: z12.number().int().min(3).max(15).optional()
16146
+ }).strict();
16147
+ var SkeletonAnimationRequestSchema = z12.object({
16148
+ action: z12.literal("create-skeleton-animation"),
16149
+ project: z12.string().min(1).optional(),
16150
+ expectedSha256: z12.string().regex(/^[0-9a-f]{64}$/),
16151
+ styleId: z12.string().min(1),
16152
+ /** The sprite the animation poses. */
16153
+ from: z12.string().min(1),
16154
+ assetId: z12.string().min(1).regex(/^[^/\\]+$/, "asset ids cannot contain slashes"),
16155
+ /** The motion, sent as PixelLab's `action`. */
16156
+ prompt: z12.string().min(1),
15828
16157
  direction: CharacterDirectionSchema,
15829
- description: z11.string().min(1).optional(),
15830
- skeletonTemplate: z11.string().min(1).optional(),
16158
+ description: z12.string().min(1).optional(),
16159
+ skeletonTemplate: z12.string().min(1).optional(),
15831
16160
  /** Manifest-relative JSON path inside the project. */
15832
- keypointsFile: z11.string().min(1),
16161
+ keypointsFile: z12.string().min(1),
15833
16162
  /** Poses to write to `keypointsFile`; omitted to use a file that already exists. */
15834
16163
  set: SkeletonSetSchema.optional(),
15835
16164
  /** Replace an existing `keypointsFile` with `set`. */
15836
- overwrite: z11.boolean().optional()
16165
+ overwrite: z12.boolean().optional()
15837
16166
  }).strict();
15838
- var SkeletonKeypointsUpdateSchema = z11.object({
15839
- action: z11.literal("update-skeleton-keypoints"),
15840
- project: z11.string().min(1).optional(),
15841
- styleId: z11.string().min(1),
15842
- assetId: z11.string().min(1),
16167
+ var SkeletonKeypointsUpdateSchema = z12.object({
16168
+ action: z12.literal("update-skeleton-keypoints"),
16169
+ project: z12.string().min(1).optional(),
16170
+ styleId: z12.string().min(1),
16171
+ assetId: z12.string().min(1),
15843
16172
  /** The file's SHA-256 when the page loaded it; anything else on disk is a concurrent edit. */
15844
- expectedSha256: z11.string().regex(/^[0-9a-f]{64}$/),
16173
+ expectedSha256: z12.string().regex(/^[0-9a-f]{64}$/),
15845
16174
  set: SkeletonSetSchema
15846
16175
  }).strict();
15847
16176
  async function createSkeletonAnimation(manifestPath, request) {
15848
- const root = path29.dirname(path29.resolve(manifestPath));
15849
- const file = path29.resolve(root, request.keypointsFile);
15850
- const relative = path29.relative(root, file);
15851
- if (!request.keypointsFile.endsWith(".json") || relative.startsWith("..") || path29.isAbsolute(relative)) {
16177
+ const root = path31.dirname(path31.resolve(manifestPath));
16178
+ const file = path31.resolve(root, request.keypointsFile);
16179
+ const relative = path31.relative(root, file);
16180
+ if (!request.keypointsFile.endsWith(".json") || relative.startsWith("..") || path31.isAbsolute(relative)) {
15852
16181
  throw new ManifestEditError("the keypoints file must be a .json path inside the project");
15853
16182
  }
15854
- const existed = existsSync24(file);
16183
+ const existed = existsSync25(file);
15855
16184
  let previous = null;
15856
16185
  if (request.set) {
15857
16186
  if (existed && !request.overwrite) {
15858
16187
  throw new ManifestEditError(`${request.keypointsFile} already exists; choose another path, or replace it`);
15859
16188
  }
15860
- if (existed) previous = await readFile21(file);
15861
- await mkdir10(path29.dirname(file), { recursive: true });
15862
- await writeFile13(file, JSON.stringify(request.set, null, 2) + "\n");
16189
+ if (existed) previous = await readFile22(file);
16190
+ await mkdir10(path31.dirname(file), { recursive: true });
16191
+ await writeFile14(file, JSON.stringify(request.set, null, 2) + "\n");
15863
16192
  } else if (!existed) {
15864
16193
  throw new ManifestEditError(`${request.keypointsFile} does not exist; paste poses or estimate them from the sprite`);
15865
16194
  }
@@ -15874,7 +16203,7 @@ async function createSkeletonAnimation(manifestPath, request) {
15874
16203
  revision: {
15875
16204
  mode: "animate-skeleton",
15876
16205
  from: request.from,
15877
- keypointsFile: relative.split(path29.sep).join("/"),
16206
+ keypointsFile: relative.split(path31.sep).join("/"),
15878
16207
  direction: request.direction,
15879
16208
  ...request.description ? { description: request.description } : {},
15880
16209
  ...request.skeletonTemplate ? { skeletonTemplate: request.skeletonTemplate } : {}
@@ -15883,8 +16212,8 @@ async function createSkeletonAnimation(manifestPath, request) {
15883
16212
  });
15884
16213
  } catch (error) {
15885
16214
  if (request.set) {
15886
- if (previous) await writeFile13(file, previous);
15887
- else await rm10(file, { force: true });
16215
+ if (previous) await writeFile14(file, previous);
16216
+ else await rm11(file, { force: true });
15888
16217
  }
15889
16218
  throw error;
15890
16219
  }
@@ -15895,64 +16224,64 @@ async function updateSkeletonKeypoints(ctx, request) {
15895
16224
  if (!spec) throw new ManifestEditError(`"${key}" is not declared by the manifest`);
15896
16225
  const file = spec.revision?.mode === "animate-skeleton" ? spec.revision.keypointsFile : void 0;
15897
16226
  if (!file) throw new ManifestEditError(`${key} is not an animate-skeleton revision`);
15898
- const relative = path29.relative(ctx.loaded.root, file);
15899
- if (relative.startsWith("..") || path29.isAbsolute(relative)) {
16227
+ const relative = path31.relative(ctx.loaded.root, file);
16228
+ if (relative.startsWith("..") || path31.isAbsolute(relative)) {
15900
16229
  throw new ManifestEditError(`${key}'s keypoints file is outside the project; edit it by hand`);
15901
16230
  }
15902
- const current = existsSync24(file) ? sha256(await readFile21(file)) : null;
16231
+ const current = existsSync25(file) ? sha256(await readFile22(file)) : null;
15903
16232
  if (current !== request.expectedSha256) {
15904
16233
  throw Object.assign(
15905
- new Error(`${relative.split(path29.sep).join("/")} changed on disk since this page loaded it; press Refresh and edit again`),
16234
+ new Error(`${relative.split(path31.sep).join("/")} changed on disk since this page loaded it; press Refresh and edit again`),
15906
16235
  { status: 409 }
15907
16236
  );
15908
16237
  }
15909
- await writeFile13(file, JSON.stringify(request.set, null, 2) + "\n");
15910
- return relative.split(path29.sep).join("/");
16238
+ await writeFile14(file, JSON.stringify(request.set, null, 2) + "\n");
16239
+ return relative.split(path31.sep).join("/");
15911
16240
  }
15912
16241
 
15913
16242
  // src/gallery/edit.ts
15914
- var Base64 = z12.string().regex(/^[A-Za-z0-9+/]*={0,2}$/).max(Math.ceil(MAX_HAND_EDIT_BYTES / 3) * 4);
15915
- var HandEditRequestSchema = z12.discriminatedUnion("action", [
15916
- z12.object({
15917
- action: z12.literal("start-edit"),
15918
- project: z12.string().min(1).optional(),
15919
- assetId: z12.string().min(1),
15920
- styleId: z12.string().min(1),
15921
- expectedSha256: z12.string().regex(/^[0-9a-f]{64}$/),
16243
+ var Base64 = z13.string().regex(/^[A-Za-z0-9+/]*={0,2}$/).max(Math.ceil(MAX_HAND_EDIT_BYTES / 3) * 4);
16244
+ var HandEditRequestSchema = z13.discriminatedUnion("action", [
16245
+ z13.object({
16246
+ action: z13.literal("start-edit"),
16247
+ project: z13.string().min(1).optional(),
16248
+ assetId: z13.string().min(1),
16249
+ styleId: z13.string().min(1),
16250
+ expectedSha256: z13.string().regex(/^[0-9a-f]{64}$/),
15922
16251
  /** Also launch the author's editor on the file. */
15923
- open: z12.boolean().optional()
16252
+ open: z13.boolean().optional()
15924
16253
  }).strict(),
15925
- z12.object({
15926
- action: z12.literal("detach-edit"),
15927
- project: z12.string().min(1).optional(),
15928
- assetId: z12.string().min(1),
15929
- styleId: z12.string().min(1),
15930
- expectedSha256: z12.string().regex(/^[0-9a-f]{64}$/)
16254
+ z13.object({
16255
+ action: z13.literal("detach-edit"),
16256
+ project: z13.string().min(1).optional(),
16257
+ assetId: z13.string().min(1),
16258
+ styleId: z13.string().min(1),
16259
+ expectedSha256: z13.string().regex(/^[0-9a-f]{64}$/)
15931
16260
  }).strict(),
15932
16261
  /** From the in-browser editor: the page holds the bytes, the server validates and writes. */
15933
- z12.object({
15934
- action: z12.literal("save-edit"),
15935
- project: z12.string().min(1).optional(),
15936
- assetId: z12.string().min(1),
15937
- styleId: z12.string().min(1),
15938
- expectedSha256: z12.string().regex(/^[0-9a-f]{64}$/),
16262
+ z13.object({
16263
+ action: z13.literal("save-edit"),
16264
+ project: z13.string().min(1).optional(),
16265
+ assetId: z13.string().min(1),
16266
+ styleId: z13.string().min(1),
16267
+ expectedSha256: z13.string().regex(/^[0-9a-f]{64}$/),
15939
16268
  /** The flattened image; for a frame set, `frames` instead. */
15940
16269
  png: Base64.min(1).optional(),
15941
16270
  /** Every frame of a set, tagged with the role it was opened under (null for one added in the editor). */
15942
- frames: z12.array(z12.object({ role: z12.string().min(1).nullable(), png: Base64.min(1) }).strict()).min(1).max(256).optional(),
16271
+ frames: z13.array(z13.object({ role: z13.string().min(1).nullable(), png: Base64.min(1) }).strict()).min(1).max(256).optional(),
15943
16272
  /** The editor's layered project file, kept beside the edit. */
15944
16273
  pxo: Base64.optional(),
15945
16274
  /** Editor identity from the bridge's ready message. */
15946
- editor: z12.string().min(1).max(200),
15947
- protocol: z12.number().int().positive()
16275
+ editor: z13.string().min(1).max(200),
16276
+ protocol: z13.number().int().positive()
15948
16277
  }).strict()
15949
16278
  ]);
15950
16279
  var UNDO_DEPTH = 20;
15951
- var UndoRequestSchema = z12.object({
15952
- action: z12.literal("undo-edit"),
15953
- project: z12.string().min(1).optional(),
16280
+ var UndoRequestSchema = z13.object({
16281
+ action: z13.literal("undo-edit"),
16282
+ project: z13.string().min(1).optional(),
15954
16283
  /** The manifest as the page last saw it; undo refuses a manifest someone else has changed since. */
15955
- expectedSha256: z12.string().regex(/^[0-9a-f]{64}$/)
16284
+ expectedSha256: z13.string().regex(/^[0-9a-f]{64}$/)
15956
16285
  }).strict();
15957
16286
  function createGalleryEditHandler(opts) {
15958
16287
  const log = opts.onProgress ?? (() => {
@@ -15961,7 +16290,7 @@ function createGalleryEditHandler(opts) {
15961
16290
  const history = /* @__PURE__ */ new Map();
15962
16291
  const manifestOf = async (project) => {
15963
16292
  try {
15964
- return path30.resolve(await opts.manifestFor(typeof project === "string" ? project : void 0));
16293
+ return path32.resolve(await opts.manifestFor(typeof project === "string" ? project : void 0));
15965
16294
  } catch {
15966
16295
  return null;
15967
16296
  }
@@ -15971,7 +16300,7 @@ function createGalleryEditHandler(opts) {
15971
16300
  if (undo.success) {
15972
16301
  const manifestPath2 = await manifestOf(undo.data.project);
15973
16302
  if (!manifestPath2) throw new ManifestEditError("no manifest to undo an edit in");
15974
- const current = await readFile22(manifestPath2, "utf8");
16303
+ const current = await readFile23(manifestPath2, "utf8");
15975
16304
  if (sha256(current) !== undo.data.expectedSha256) throw new ManifestDriftError(manifestPath2);
15976
16305
  const stack = history.get(manifestPath2) ?? [];
15977
16306
  const last = stack[stack.length - 1];
@@ -15981,17 +16310,17 @@ function createGalleryEditHandler(opts) {
15981
16310
  throw Object.assign(new Error("the manifest changed outside the gallery since its last edit; undo would discard that change"), { status: 409 });
15982
16311
  }
15983
16312
  const tmp = manifestPath2 + ".pixelkiln-undo";
15984
- await writeFile14(tmp, last.before);
16313
+ await writeFile15(tmp, last.before);
15985
16314
  await rename9(tmp, manifestPath2);
15986
16315
  stack.pop();
15987
- log(` manifest edit undone in ${path30.relative(process.cwd(), manifestPath2) || manifestPath2}`);
16316
+ log(` manifest edit undone in ${path32.relative(process.cwd(), manifestPath2) || manifestPath2}`);
15988
16317
  return opts.reload();
15989
16318
  }
15990
16319
  const manifestPath = await manifestOf(body?.project);
15991
- const before = manifestPath ? await readFile22(manifestPath, "utf8").catch(() => null) : null;
16320
+ const before = manifestPath ? await readFile23(manifestPath, "utf8").catch(() => null) : null;
15992
16321
  const build = await apply(body);
15993
16322
  if (manifestPath && before !== null) {
15994
- const after = await readFile22(manifestPath, "utf8").catch(() => null);
16323
+ const after = await readFile23(manifestPath, "utf8").catch(() => null);
15995
16324
  if (after !== null && after !== before) {
15996
16325
  const stack = history.get(manifestPath) ?? [];
15997
16326
  stack.push({ before, afterSha: sha256(after) });
@@ -16046,7 +16375,7 @@ function createEditApplier(opts, log) {
16046
16375
  } catch (error) {
16047
16376
  throw new ManifestEditError(error instanceof Error ? error.message : String(error));
16048
16377
  }
16049
- const written = await saveProjectImage(path30.dirname(path30.resolve(manifestPath2)), upload.data);
16378
+ const written = await saveProjectImage(path32.dirname(path32.resolve(manifestPath2)), upload.data);
16050
16379
  log(` image added: ${written}`);
16051
16380
  return opts.reload();
16052
16381
  }
@@ -16082,6 +16411,23 @@ function createEditApplier(opts, log) {
16082
16411
  "invalid skeleton animation: " + skeleton.error.issues.slice(0, 3).map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")
16083
16412
  );
16084
16413
  }
16414
+ const inpaint = InpaintRevisionRequestSchema.safeParse(body);
16415
+ if (inpaint.success) {
16416
+ let manifestPath2;
16417
+ try {
16418
+ manifestPath2 = await opts.manifestFor(inpaint.data.project);
16419
+ } catch (error) {
16420
+ throw new ManifestEditError(error instanceof Error ? error.message : String(error));
16421
+ }
16422
+ const mask = await createInpaintRevision(manifestPath2, inpaint.data);
16423
+ log(` manifest edited: added inpaint revision ${inpaint.data.assetId} (mask ${mask})`);
16424
+ return opts.reload();
16425
+ }
16426
+ if (body?.action === "create-inpaint-revision") {
16427
+ throw new ManifestEditError(
16428
+ "invalid inpaint revision: " + inpaint.error.issues.slice(0, 3).map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")
16429
+ );
16430
+ }
16085
16431
  const parsed = ManifestEditSchema.safeParse(body);
16086
16432
  if (!parsed.success) {
16087
16433
  throw new ManifestEditError(
@@ -16096,7 +16442,7 @@ function createEditApplier(opts, log) {
16096
16442
  }
16097
16443
  const result = await applyManifestEdit(manifestPath, parsed.data);
16098
16444
  if (result.changed) {
16099
- const relative = path30.relative(process.cwd(), result.manifestPath);
16445
+ const relative = path32.relative(process.cwd(), result.manifestPath);
16100
16446
  const shown = !relative || relative.startsWith("..") ? result.manifestPath : relative;
16101
16447
  log(` manifest edited: ${describeEdit(parsed.data)} in ${shown}`);
16102
16448
  }
@@ -16124,7 +16470,7 @@ function describeEdit(edit) {
16124
16470
  // src/gallery/snapshot.ts
16125
16471
  var PROJECT_FILE_TYPE = "application/octet-stream";
16126
16472
  function galleryMediaId(absolutePath) {
16127
- return createHash4("sha256").update(path31.resolve(absolutePath)).digest("hex").slice(0, 24);
16473
+ return createHash5("sha256").update(path33.resolve(absolutePath)).digest("hex").slice(0, 24);
16128
16474
  }
16129
16475
  function galleryMediaRoute(id) {
16130
16476
  return `/media/${id}`;
@@ -16143,9 +16489,9 @@ function describeHistory(media, entry, cacheDir) {
16143
16489
  return (entry.history ?? []).map((generation, i) => {
16144
16490
  const outputs = generation.outputs.map((output) => {
16145
16491
  const mediaType = output.mediaType ?? MediaType.PNG;
16146
- const file = path31.join(cacheDir, cacheFileName(output.sha256, mediaType));
16492
+ const file = path33.join(cacheDir, cacheFileName(output.sha256, mediaType));
16147
16493
  let url = null;
16148
- if (existsSync25(file)) {
16494
+ if (existsSync26(file)) {
16149
16495
  const id = galleryMediaId(file);
16150
16496
  media.set(id, { path: file, contentType: mediaType });
16151
16497
  url = `${galleryMediaRoute(id)}?v=${output.sha256.slice(0, 24)}`;
@@ -16177,7 +16523,7 @@ function describeHistory(media, entry, cacheDir) {
16177
16523
  var MAX_COMPARED_BYTES = 8 * 1024 * 1024;
16178
16524
  async function samePixels(a, b) {
16179
16525
  try {
16180
- const [left, right] = await Promise.all([readFile23(a), readFile23(b)]);
16526
+ const [left, right] = await Promise.all([readFile24(a), readFile24(b)]);
16181
16527
  if (left.length > MAX_COMPARED_BYTES || right.length > MAX_COMPARED_BYTES) return false;
16182
16528
  const x = decodePng(left);
16183
16529
  const y = decodePng(right);
@@ -16275,10 +16621,10 @@ async function describeOutput(media, root, absolutePath, recorded = {}) {
16275
16621
  async function describeQuality(media, root, inspection) {
16276
16622
  const outputPaths = inspection.outputs ?? [inspection.output];
16277
16623
  const outputs = await Promise.all(
16278
- outputPaths.map((file) => describeOutput(media, root, path31.resolve(root, file)))
16624
+ outputPaths.map((file) => describeOutput(media, root, path33.resolve(root, file)))
16279
16625
  );
16280
- const recordPath = path31.resolve(root, inspection.record);
16281
- const recordExists = existsSync25(recordPath);
16626
+ const recordPath = path33.resolve(root, inspection.record);
16627
+ const recordExists = existsSync26(recordPath);
16282
16628
  let options = null;
16283
16629
  let check = null;
16284
16630
  if (recordExists) {
@@ -16297,7 +16643,7 @@ async function describeQuality(media, root, inspection) {
16297
16643
  return {
16298
16644
  state: inspection.state,
16299
16645
  reason: inspection.reason,
16300
- output: portableOutputPath(path31.resolve(root, inspection.output), root),
16646
+ output: portableOutputPath(path33.resolve(root, inspection.output), root),
16301
16647
  record: portableOutputPath(recordPath, root),
16302
16648
  recordExists,
16303
16649
  outputs,
@@ -16335,9 +16681,9 @@ async function buildGallerySnapshot(opts) {
16335
16681
  const media = /* @__PURE__ */ new Map();
16336
16682
  const plan = await buildPlan(specs, lock);
16337
16683
  const items = [];
16338
- const manifestText = await readFile23(loaded.path, "utf8");
16684
+ const manifestText = await readFile24(loaded.path, "utf8");
16339
16685
  const manifestSha256 = sha256(manifestText);
16340
- const cacheDir = path31.resolve(path31.dirname(opts.lockPath), ".pixelkiln", "cache");
16686
+ const cacheDir = path33.resolve(path33.dirname(opts.lockPath), ".pixelkiln", "cache");
16341
16687
  let rawStyles = {};
16342
16688
  try {
16343
16689
  const raw = JSON.parse(manifestText);
@@ -16361,12 +16707,12 @@ async function buildGallerySnapshot(opts) {
16361
16707
  )
16362
16708
  );
16363
16709
  if (spec.source) {
16364
- const editPath = path31.resolve(root, spec.source);
16710
+ const editPath = path33.resolve(root, spec.source);
16365
16711
  const memberCount = sourceIsStem(spec.source, entry, root) ? entry.outputs.length : 1;
16366
16712
  const editShas = await Promise.all(
16367
16713
  Array.from({ length: memberCount }, async (_, index) => {
16368
16714
  const file = sourceOutputPath(spec.source, entry, index, root);
16369
- return { file, sha256: existsSync25(file) ? await sha256File(file) : null };
16715
+ return { file, sha256: existsSync26(file) ? await sha256File(file) : null };
16370
16716
  })
16371
16717
  );
16372
16718
  edits = await Promise.all(editShas.map(({ file, sha256: editSha }) => describeOutput(media, root, file, { sha256: editSha })));
@@ -16374,7 +16720,7 @@ async function buildGallerySnapshot(opts) {
16374
16720
  const companion = await readHandEditCompanion(editPath);
16375
16721
  if (companion) {
16376
16722
  const projectPath = handEditProjectPath(editPath);
16377
- const project = companion.project && existsSync25(projectPath) ? await fileInfo(projectPath) : null;
16723
+ const project = companion.project && existsSync26(projectPath) ? await fileInfo(projectPath) : null;
16378
16724
  let projectUrl = null;
16379
16725
  if (project) {
16380
16726
  const id = galleryMediaId(projectPath);
@@ -16410,8 +16756,8 @@ async function buildGallerySnapshot(opts) {
16410
16756
  editStatus = edits.some((member) => !member.exists) ? "missing" : editChanged.every((changed) => !changed) ? "same" : regenerated ? "regenerated-since" : "edited";
16411
16757
  }
16412
16758
  } else if (spec.source) {
16413
- outputs = [await describeOutput(media, root, path31.resolve(root, spec.source))];
16414
- } else if (!entry && existsSync25(spec.outFile)) {
16759
+ outputs = [await describeOutput(media, root, path33.resolve(root, spec.source))];
16760
+ } else if (!entry && existsSync26(spec.outFile)) {
16415
16761
  outputs = [await describeOutput(media, root, spec.outFile)];
16416
16762
  } else {
16417
16763
  outputs = [];
@@ -16614,7 +16960,7 @@ async function buildGallerySnapshot(opts) {
16614
16960
  id: loaded.manifest.name,
16615
16961
  name: loaded.manifest.name,
16616
16962
  manifest: loaded.path,
16617
- lock: path31.resolve(opts.lockPath),
16963
+ lock: path33.resolve(opts.lockPath),
16618
16964
  root,
16619
16965
  provider: loaded.manifest.provider,
16620
16966
  account: null,
@@ -16640,7 +16986,7 @@ async function buildGallerySnapshot(opts) {
16640
16986
  return { snapshot, media };
16641
16987
  }
16642
16988
  async function buildWorkspaceGallerySnapshot(opts) {
16643
- const dir = path31.dirname(path31.resolve(opts.workspacePath));
16989
+ const dir = path33.dirname(path33.resolve(opts.workspacePath));
16644
16990
  const media = /* @__PURE__ */ new Map();
16645
16991
  const projects = [];
16646
16992
  const items = [];
@@ -16683,11 +17029,11 @@ async function buildWorkspaceGallerySnapshot(opts) {
16683
17029
  id: project.id,
16684
17030
  name: loaded.manifest.name,
16685
17031
  manifest: manifestPath,
16686
- lock: path31.resolve(lockPath),
17032
+ lock: path33.resolve(lockPath),
16687
17033
  root: loaded.root,
16688
17034
  provider: loaded.manifest.provider,
16689
17035
  account: project.account ?? null,
16690
- manifestSha256: manifestSha256 ?? sha256(await readFile23(manifestPath, "utf8")),
17036
+ manifestSha256: manifestSha256 ?? sha256(await readFile24(manifestPath, "utf8")),
16691
17037
  historyLimit: historyLimit(loaded.manifest),
16692
17038
  entries: Object.keys(lock.entries).length,
16693
17039
  items: projectItems.length,
@@ -16699,8 +17045,8 @@ async function buildWorkspaceGallerySnapshot(opts) {
16699
17045
  id: project.id,
16700
17046
  name: project.id,
16701
17047
  manifest: manifestPath,
16702
- lock: path31.resolve(lockPath),
16703
- root: path31.dirname(manifestPath),
17048
+ lock: path33.resolve(lockPath),
17049
+ root: path33.dirname(manifestPath),
16704
17050
  provider: project.provider,
16705
17051
  account: project.account ?? null,
16706
17052
  manifestSha256: null,
@@ -16727,7 +17073,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
16727
17073
  version: 1,
16728
17074
  generatedAt: (opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
16729
17075
  project: null,
16730
- workspace: { path: path31.resolve(opts.workspacePath), dir, projects },
17076
+ workspace: { path: path33.resolve(opts.workspacePath), dir, projects },
16731
17077
  filter,
16732
17078
  totals: {
16733
17079
  entries: projects.reduce((sum, project) => sum + project.entries, 0),
@@ -16744,7 +17090,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
16744
17090
  }
16745
17091
 
16746
17092
  // src/gallery/page.ts
16747
- import { existsSync as existsSync26, readFileSync as readFileSync5 } from "fs";
17093
+ import { existsSync as existsSync27, readFileSync as readFileSync6 } from "fs";
16748
17094
 
16749
17095
  // src/gallery/client-bundle.ts
16750
17096
  import { createRequire } from "module";
@@ -16784,8 +17130,8 @@ var client;
16784
17130
  function clientAssets() {
16785
17131
  const prebuilt = new URL("gallery.js", CLIENT_DIR);
16786
17132
  client ??= {
16787
- css: readFileSync5(new URL("gallery.css", CLIENT_DIR), "utf8"),
16788
- js: existsSync26(prebuilt) ? readFileSync5(prebuilt, "utf8") : bundleGalleryClient()
17133
+ css: readFileSync6(new URL("gallery.css", CLIENT_DIR), "utf8"),
17134
+ js: existsSync27(prebuilt) ? readFileSync6(prebuilt, "utf8") : bundleGalleryClient()
16789
17135
  };
16790
17136
  return client;
16791
17137
  }
@@ -16927,11 +17273,11 @@ var EDITOR_PIN = pin_default;
16927
17273
  var EDITOR_RELEASE_BASE = "https://github.com/gfargo/pixelkiln/releases/download";
16928
17274
 
16929
17275
  // src/editor/install.ts
16930
- import { createHash as createHash5 } from "crypto";
16931
- import { existsSync as existsSync27 } from "fs";
16932
- import { mkdir as mkdir11, readFile as readFile24, rename as rename10, rm as rm11, stat as stat4, writeFile as writeFile15 } from "fs/promises";
16933
- import os2 from "os";
16934
- import path32 from "path";
17276
+ import { createHash as createHash6 } from "crypto";
17277
+ import { existsSync as existsSync28 } from "fs";
17278
+ import { mkdir as mkdir11, readFile as readFile25, rename as rename10, rm as rm12, stat as stat4, writeFile as writeFile16 } from "fs/promises";
17279
+ import os3 from "os";
17280
+ import path34 from "path";
16935
17281
  var EditorInstallError = class extends Error {
16936
17282
  constructor(message6, file) {
16937
17283
  super(message6);
@@ -16942,15 +17288,15 @@ var EditorInstallError = class extends Error {
16942
17288
  };
16943
17289
  function editorToolsRoot() {
16944
17290
  const override = process.env.PIXELKILN_TOOLS_DIR?.trim();
16945
- if (override) return path32.resolve(override);
16946
- if (process.platform === "darwin") return path32.join(os2.homedir(), "Library", "Caches", "pixelkiln", "tools");
17291
+ if (override) return path34.resolve(override);
17292
+ if (process.platform === "darwin") return path34.join(os3.homedir(), "Library", "Caches", "pixelkiln", "tools");
16947
17293
  if (process.platform === "win32") {
16948
- return path32.join(process.env.LOCALAPPDATA ?? path32.join(os2.homedir(), "AppData", "Local"), "pixelkiln", "tools");
17294
+ return path34.join(process.env.LOCALAPPDATA ?? path34.join(os3.homedir(), "AppData", "Local"), "pixelkiln", "tools");
16949
17295
  }
16950
- return path32.join(process.env.XDG_CACHE_HOME ?? path32.join(os2.homedir(), ".cache"), "pixelkiln", "tools");
17296
+ return path34.join(process.env.XDG_CACHE_HOME ?? path34.join(os3.homedir(), ".cache"), "pixelkiln", "tools");
16951
17297
  }
16952
17298
  function editorDir(pin = EDITOR_PIN, root = editorToolsRoot()) {
16953
- return path32.join(root, "pixelorama", pin.release ?? "unpublished");
17299
+ return path34.join(root, "pixelorama", pin.release ?? "unpublished");
16954
17300
  }
16955
17301
  function editorBaseUrl(pin = EDITOR_PIN) {
16956
17302
  const override = process.env.PIXELKILN_EDITOR_URL?.trim();
@@ -16960,7 +17306,7 @@ function editorBaseUrl(pin = EDITOR_PIN) {
16960
17306
  }
16961
17307
  async function sha256Of(file) {
16962
17308
  try {
16963
- return createHash5("sha256").update(await readFile24(file)).digest("hex");
17309
+ return createHash6("sha256").update(await readFile25(file)).digest("hex");
16964
17310
  } catch {
16965
17311
  return null;
16966
17312
  }
@@ -16973,8 +17319,8 @@ async function editorStatus(opts = {}) {
16973
17319
  let totalBytes = 0;
16974
17320
  for (const [name, expected] of Object.entries(pin.files)) {
16975
17321
  totalBytes += expected.bytes;
16976
- const file = path32.join(dir, name);
16977
- const info = existsSync27(file) ? await stat4(file).catch(() => null) : null;
17322
+ const file = path34.join(dir, name);
17323
+ const info = existsSync28(file) ? await stat4(file).catch(() => null) : null;
16978
17324
  if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
16979
17325
  installedBytes += expected.bytes;
16980
17326
  } else {
@@ -17027,17 +17373,17 @@ async function installEditor(opts = {}) {
17027
17373
  if (bytes.length !== expected.bytes) {
17028
17374
  throw new EditorInstallError(`${name}: expected ${expected.bytes} bytes, got ${bytes.length}`, name);
17029
17375
  }
17030
- const digest2 = createHash5("sha256").update(bytes).digest("hex");
17376
+ const digest2 = createHash6("sha256").update(bytes).digest("hex");
17031
17377
  if (digest2 !== expected.sha256) {
17032
17378
  throw new EditorInstallError(`${name}: sha256 ${digest2.slice(0, 12)}\u2026 does not match the pinned ${expected.sha256.slice(0, 12)}\u2026`, name);
17033
17379
  }
17034
- const final = path32.join(dir, name);
17380
+ const final = path34.join(dir, name);
17035
17381
  const tmp = `${final}.${process.pid}.part`;
17036
17382
  try {
17037
- await writeFile15(tmp, bytes);
17383
+ await writeFile16(tmp, bytes);
17038
17384
  await rename10(tmp, final);
17039
17385
  } finally {
17040
- await rm11(tmp, { force: true });
17386
+ await rm12(tmp, { force: true });
17041
17387
  }
17042
17388
  fetchedBytes += bytes.length;
17043
17389
  downloaded.push(name);
@@ -17077,12 +17423,12 @@ var EDITOR_CONTENT_TYPES = {
17077
17423
  ".json": "application/json; charset=utf-8"
17078
17424
  };
17079
17425
  function editorContentType(name) {
17080
- return EDITOR_CONTENT_TYPES[path32.extname(name).toLowerCase()] ?? null;
17426
+ return EDITOR_CONTENT_TYPES[path34.extname(name).toLowerCase()] ?? null;
17081
17427
  }
17082
17428
 
17083
17429
  // src/gallery/editor.ts
17084
- import { readFile as readFile25 } from "fs/promises";
17085
- import path33 from "path";
17430
+ import { readFile as readFile26 } from "fs/promises";
17431
+ import path35 from "path";
17086
17432
  function editorRoute(release, name = "index.html") {
17087
17433
  return `/editor/${encodeURIComponent(release)}/${encodeURIComponent(name)}`;
17088
17434
  }
@@ -17155,7 +17501,7 @@ function createGalleryEditorHandlers(opts = {}) {
17155
17501
  if (!status.installed) return null;
17156
17502
  let bytes;
17157
17503
  try {
17158
- bytes = await readFile25(path33.join(dir, name));
17504
+ bytes = await readFile26(path35.join(dir, name));
17159
17505
  } catch {
17160
17506
  verified = null;
17161
17507
  return null;
@@ -17172,7 +17518,7 @@ function createGalleryEditorHandlers(opts = {}) {
17172
17518
  // src/gallery/server.ts
17173
17519
  import { randomBytes } from "crypto";
17174
17520
  import { createServer as createServer2 } from "http";
17175
- import { readFile as readFile26 } from "fs/promises";
17521
+ import { readFile as readFile27 } from "fs/promises";
17176
17522
  var MAX_EDIT_BYTES = 48 * 1024 * 1024;
17177
17523
  var EDITOR_CSP = [
17178
17524
  "default-src 'self'",
@@ -17353,7 +17699,7 @@ async function serveGallery(opts) {
17353
17699
  const asset = reviewAssets.get(jobId)?.get(url2.pathname);
17354
17700
  if (!asset) return fail(404, "no such review asset");
17355
17701
  try {
17356
- const bytes = await readFile26(asset.path);
17702
+ const bytes = await readFile27(asset.path);
17357
17703
  res.writeHead(200, {
17358
17704
  "Content-Type": asset.contentType,
17359
17705
  "Cache-Control": "no-store",
@@ -17436,7 +17782,7 @@ async function serveGallery(opts) {
17436
17782
  if (!asset) return fail(404, "no such gallery media");
17437
17783
  let bytes;
17438
17784
  try {
17439
- bytes = await readFile26(asset.path);
17785
+ bytes = await readFile27(asset.path);
17440
17786
  } catch {
17441
17787
  return fail(404, "gallery media is no longer on disk; refresh the page");
17442
17788
  }
@@ -17480,27 +17826,27 @@ async function serveGallery(opts) {
17480
17826
 
17481
17827
  // src/gallery/generate.ts
17482
17828
  import { randomBytes as randomBytes2 } from "crypto";
17483
- import { z as z13 } from "zod";
17484
- var GenerateRequestSchema = z13.object({
17485
- project: z13.string().min(1).optional(),
17486
- keys: z13.array(z13.string().min(1)).min(1).max(500),
17829
+ import { z as z14 } from "zod";
17830
+ var GenerateRequestSchema = z14.object({
17831
+ project: z14.string().min(1).optional(),
17832
+ keys: z14.array(z14.string().min(1)).min(1).max(500),
17487
17833
  /**
17488
17834
  * Regenerate work that is up to date, as `gen --force` does, and let the
17489
17835
  * fetch replace a locally modified or untracked file. With `restore`, put
17490
17836
  * the recorded bytes back over a modified file.
17491
17837
  */
17492
- force: z13.boolean().optional(),
17838
+ force: z14.boolean().optional(),
17493
17839
  /** Advance in-flight, review, or selected work without submitting anything. */
17494
- resume: z13.boolean().optional(),
17840
+ resume: z14.boolean().optional(),
17495
17841
  /** Re-download downloaded outputs and replace files whose object changed upstream. */
17496
- refresh: z13.boolean().optional(),
17842
+ refresh: z14.boolean().optional(),
17497
17843
  /** Put recorded outputs back on disk from the cache or the provider, without generating. */
17498
- restore: z13.boolean().optional(),
17844
+ restore: z14.boolean().optional(),
17499
17845
  /**
17500
17846
  * Bring a replaced generation back: 1-based position in the record's
17501
17847
  * history (newest first) or an output hash prefix. One key at a time.
17502
17848
  */
17503
- revert: z13.string().min(1).max(64).optional()
17849
+ revert: z14.string().min(1).max(64).optional()
17504
17850
  }).strict().refine((request) => [request.resume, request.refresh, request.restore, request.revert !== void 0].filter(Boolean).length <= 1, {
17505
17851
  message: "resume, refresh, restore, and revert are exclusive"
17506
17852
  }).refine((request) => request.revert === void 0 || request.keys.length === 1, { message: "revert takes one key" });
@@ -17846,19 +18192,19 @@ function createGenerateHandlers(opts) {
17846
18192
  }
17847
18193
 
17848
18194
  // src/pipeline/salvage.ts
17849
- import { readFile as readFile27 } from "fs/promises";
17850
- import { existsSync as existsSync28 } from "fs";
17851
- import path34 from "path";
18195
+ import { readFile as readFile28 } from "fs/promises";
18196
+ import { existsSync as existsSync29 } from "fs";
18197
+ import path36 from "path";
17852
18198
  function providerClaimId(provider, objectId) {
17853
18199
  return `${encodeURIComponent(provider)}:${encodeURIComponent(objectId)}`;
17854
18200
  }
17855
18201
  async function loadClaims(lockPaths, opts = {}) {
17856
18202
  const claimed = /* @__PURE__ */ new Set();
17857
18203
  for (const p of lockPaths) {
17858
- if (!existsSync28(p)) throw new Error(`Claim lockfile not found: ${p}`);
18204
+ if (!existsSync29(p)) throw new Error(`Claim lockfile not found: ${p}`);
17859
18205
  let parsed;
17860
18206
  try {
17861
- parsed = parseLock(JSON.parse(await readFile27(p, "utf8")));
18207
+ parsed = parseLock(JSON.parse(await readFile28(p, "utf8")));
17862
18208
  } catch {
17863
18209
  throw new Error(`Claim lockfile is malformed: ${p}`);
17864
18210
  }
@@ -17962,20 +18308,20 @@ function matchOrphanStyle(prompt, manifest) {
17962
18308
  return matchStyleByPattern(prompt, manifest);
17963
18309
  }
17964
18310
  async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, claimPaths) {
17965
- const own = path34.resolve(ownManifestPath);
18311
+ const own = path36.resolve(ownManifestPath);
17966
18312
  const siblingManifestPaths = [
17967
18313
  .../* @__PURE__ */ new Set([
17968
18314
  ...workspaceManifestPaths,
17969
- ...claimPaths.map((c) => path34.join(path34.dirname(path34.resolve(c)), "pixelkiln.manifest.json"))
18315
+ ...claimPaths.map((c) => path36.join(path36.dirname(path36.resolve(c)), "pixelkiln.manifest.json"))
17970
18316
  ])
17971
18317
  ];
17972
18318
  const siblings = [];
17973
18319
  for (const siblingManifestPath of siblingManifestPaths) {
17974
- if (path34.resolve(siblingManifestPath) === own) continue;
17975
- if (!existsSync28(siblingManifestPath)) continue;
18320
+ if (path36.resolve(siblingManifestPath) === own) continue;
18321
+ if (!existsSync29(siblingManifestPath)) continue;
17976
18322
  try {
17977
18323
  const { manifest } = await loadManifest(siblingManifestPath);
17978
- siblings.push({ label: path34.basename(path34.dirname(siblingManifestPath)), manifest });
18324
+ siblings.push({ label: path36.basename(path36.dirname(siblingManifestPath)), manifest });
17979
18325
  } catch {
17980
18326
  }
17981
18327
  }
@@ -18095,8 +18441,8 @@ async function applyTags(provider, decisions, existing, opts = {}) {
18095
18441
  }
18096
18442
 
18097
18443
  // src/pick/salvage-server.ts
18098
- import { mkdir as mkdir12, writeFile as writeFile16, readFile as readFile28 } from "fs/promises";
18099
- import path35 from "path";
18444
+ import { mkdir as mkdir12, writeFile as writeFile17, readFile as readFile29 } from "fs/promises";
18445
+ import path37 from "path";
18100
18446
 
18101
18447
  // src/pick/salvage-sheet.ts
18102
18448
  var escapeHtml3 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
@@ -18262,7 +18608,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
18262
18608
  });
18263
18609
  const html = renderSalvageSheet(orphans, {
18264
18610
  styleId: ctx.styleId,
18265
- importDir: path35.relative(process.cwd(), ctx.importDir) || "."
18611
+ importDir: path37.relative(process.cwd(), ctx.importDir) || "."
18266
18612
  });
18267
18613
  const byId = new Map(orphans.map((o) => [o.id, o]));
18268
18614
  const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
@@ -18318,10 +18664,10 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
18318
18664
  if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
18319
18665
  decodePng(buf);
18320
18666
  const assetId = idFromPrompt(orphan.prompt, taken);
18321
- const rel = path35.join("_salvaged", `${assetId}.png`);
18322
- const outFile = path35.resolve(ctx.importDir, rel);
18323
- await mkdir12(path35.dirname(outFile), { recursive: true });
18324
- await writeFile16(outFile, buf);
18667
+ const rel = path37.join("_salvaged", `${assetId}.png`);
18668
+ const outFile = path37.resolve(ctx.importDir, rel);
18669
+ await mkdir12(path37.dirname(outFile), { recursive: true });
18670
+ await writeFile17(outFile, buf);
18325
18671
  ctx.manifest.assets[assetId] = {
18326
18672
  prompt: orphan.prompt,
18327
18673
  promptByStyle: {},
@@ -18353,7 +18699,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
18353
18699
  sourceUrl: durableSource,
18354
18700
  sourceUrls: durableSource ? [{ url: durableSource }] : [],
18355
18701
  outputs: [{
18356
- path: portableOutputPath(outFile, path35.dirname(ctx.manifestPath)),
18702
+ path: portableOutputPath(outFile, path37.dirname(ctx.manifestPath)),
18357
18703
  sha256: sha256(buf)
18358
18704
  }],
18359
18705
  submittedAt: orphan.createdAt,
@@ -18377,9 +18723,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
18377
18723
  }
18378
18724
  }
18379
18725
  await applyTags(provider, decisions, existingTags, { onProgress: log, characters: characterMembers });
18380
- const raw = JSON.parse(await readFile28(ctx.manifestPath, "utf8"));
18726
+ const raw = JSON.parse(await readFile29(ctx.manifestPath, "utf8"));
18381
18727
  for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
18382
- await writeFile16(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
18728
+ await writeFile17(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
18383
18729
  await saveLock(ctx.lockPath, ctx.lock);
18384
18730
  if (characterAssetIds.length) result.characterAssetIds = characterAssetIds;
18385
18731
  return result;