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.cjs CHANGED
@@ -334,6 +334,7 @@ var RevisionModeSchema = import_zod.z.enum([
334
334
  "outpaint",
335
335
  "reduce-colors",
336
336
  "correct-pixelart",
337
+ "remove-background",
337
338
  "animate",
338
339
  "animate-pixminimax",
339
340
  "animate-skeleton",
@@ -665,7 +666,22 @@ var RevisionSchema = import_zod.z.object({
665
666
  * `interpolate`: required, the ending keyframe; the parent is the start.
666
667
  */
667
668
  lastFrame: import_zod.z.string().min(1).optional(),
668
- /** `animate-pixminimax` only: facing direction, used only alongside `enhancePrompt` to hold the sprite's facing. */
669
+ /**
670
+ * `animate`/`animate-pixminimax` only: colour de-flicker sensitivity.
671
+ * Frames whose foreground drifts from the first frame beyond this are
672
+ * corrected toward it; 0 corrects every frame and higher values correct
673
+ * fewer. Omit for PixelLab's default.
674
+ */
675
+ driftThreshold: import_zod.z.number().min(0).optional(),
676
+ /**
677
+ * `animate-pixminimax` only: one noun phrase for what the parent shows.
678
+ * PixelLab reads it off the image when omitted, and can decline some
679
+ * images, failing the job until it is sent.
680
+ */
681
+ subjectDescription: import_zod.z.string().min(1).max(300).optional(),
682
+ /** `animate-pixminimax` only: the pose the parent is in, read off the image by PixelLab when omitted. */
683
+ initialPose: import_zod.z.string().min(1).max(300).optional(),
684
+ /** `animate-pixminimax` only: facing direction, one of the model's caption fields, so it also holds the sprite's facing. */
669
685
  direction: CharacterDirectionSchema.optional(),
670
686
  /** `animate`/`animate-pixminimax` only: let PixelLab expand the action into a fuller motion description first. */
671
687
  enhancePrompt: import_zod.z.boolean().optional(),
@@ -685,12 +701,15 @@ var RevisionSchema = import_zod.z.object({
685
701
  * same `mannequin`/`bear`/`cat`/`dog`/`horse`/`lion` vocabulary.
686
702
  */
687
703
  skeletonTemplate: import_zod.z.string().min(1).optional(),
704
+ /** `remove-background` only: `simple` is faster and suits flat backgrounds; `complex` handles detailed edges. PixelLab defaults to `simple`. */
705
+ removalTask: import_zod.z.enum(["simple", "complex"]).optional(),
688
706
  /**
689
- * `animate-skeleton` only: what the character *looks like*, a noun
707
+ * `animate-skeleton`: what the character *looks like*, a noun
690
708
  * phrase (colours, clothing, held items) — distinct from the asset's
691
709
  * own `prompt`, which this mode sends as the motion's short label
692
710
  * (PixelLab's `action`), the same `prompt`-as-motion convention
693
- * `animate`/`animate-pixminimax` already use.
711
+ * `animate`/`animate-pixminimax` already use. `remove-background`: a
712
+ * short description of the foreground, which PixelLab uses as a hint.
694
713
  */
695
714
  description: import_zod.z.string().min(1).optional(),
696
715
  /**
@@ -807,7 +826,44 @@ var RevisionSchema = import_zod.z.object({
807
826
  path: ["keypointsFile"]
808
827
  });
809
828
  }
810
- for (const field of ["keypointsFile", "skeletonTemplate", "description"]) {
829
+ if (revision.driftThreshold !== void 0 && revision.mode !== "animate" && revision.mode !== "animate-pixminimax") {
830
+ context.addIssue({
831
+ code: import_zod.z.ZodIssueCode.custom,
832
+ message: "driftThreshold applies to animate/animate-pixminimax revisions only",
833
+ path: ["driftThreshold"]
834
+ });
835
+ }
836
+ for (const field of ["subjectDescription", "initialPose"]) {
837
+ if (revision[field] !== void 0 && revision.mode !== "animate-pixminimax") {
838
+ context.addIssue({
839
+ code: import_zod.z.ZodIssueCode.custom,
840
+ message: `${field} applies to animate-pixminimax revisions only`,
841
+ path: [field]
842
+ });
843
+ }
844
+ }
845
+ if (revision.removalTask !== void 0 && revision.mode !== "remove-background") {
846
+ context.addIssue({
847
+ code: import_zod.z.ZodIssueCode.custom,
848
+ message: "removalTask applies to remove-background revisions only",
849
+ path: ["removalTask"]
850
+ });
851
+ }
852
+ if (revision.strength !== void 0 && revision.mode === "remove-background") {
853
+ context.addIssue({
854
+ code: import_zod.z.ZodIssueCode.custom,
855
+ message: "remove-background revisions do not take a strength",
856
+ path: ["strength"]
857
+ });
858
+ }
859
+ if (revision.description !== void 0 && revision.mode !== "animate-skeleton" && revision.mode !== "remove-background") {
860
+ context.addIssue({
861
+ code: import_zod.z.ZodIssueCode.custom,
862
+ message: "description applies to animate-skeleton/remove-background revisions only",
863
+ path: ["description"]
864
+ });
865
+ }
866
+ for (const field of ["keypointsFile", "skeletonTemplate"]) {
811
867
  if (revision[field] !== void 0 && revision.mode !== "animate-skeleton") {
812
868
  context.addIssue({
813
869
  code: import_zod.z.ZodIssueCode.custom,
@@ -1461,6 +1517,10 @@ var LockEntrySchema = import_zod.z.object({
1461
1517
  keypointsSha256: import_zod.z.string().regex(/^[0-9a-f]{64}$/).optional(),
1462
1518
  skeletonTemplate: import_zod.z.string().min(1).optional(),
1463
1519
  description: import_zod.z.string().min(1).optional(),
1520
+ removalTask: import_zod.z.enum(["simple", "complex"]).optional(),
1521
+ driftThreshold: import_zod.z.number().min(0).optional(),
1522
+ subjectDescription: import_zod.z.string().min(1).max(300).optional(),
1523
+ initialPose: import_zod.z.string().min(1).max(300).optional(),
1464
1524
  engine: import_zod.z.enum(["pro-flash"]).optional()
1465
1525
  }).strict().nullable().default(null),
1466
1526
  /** For a mirror: the asset it flips and a hash over that asset's output hashes when this was made. */
@@ -1889,6 +1949,7 @@ var CorrectPixelartResponseSchema = import_zod3.z.object({
1889
1949
  images: import_zod3.z.array(import_zod3.z.object({ base64: import_zod3.z.string().min(1) }).passthrough()).min(1),
1890
1950
  usage: import_zod3.z.unknown().optional()
1891
1951
  }).passthrough();
1952
+ var RemoveBackgroundResponseSchema = import_zod3.z.object({ image: import_zod3.z.object({ base64: import_zod3.z.string().min(1) }).passthrough(), usage: import_zod3.z.unknown().optional() }).passthrough();
1892
1953
  var ProFlashImageSubmitSchema = import_zod3.z.object({
1893
1954
  background_job_id: import_zod3.z.string().min(1),
1894
1955
  image_id: import_zod3.z.string().min(1),
@@ -2080,9 +2141,9 @@ var PixelLabClient = class {
2080
2141
  apiKey;
2081
2142
  http;
2082
2143
  /** Retries transport failures, 429, and 5xx; see http.ts for the policy. */
2083
- async request(path36, init) {
2144
+ async request(path38, init) {
2084
2145
  const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
2085
- const res = await this.http(`${BASE}${path36}`, {
2146
+ const res = await this.http(`${BASE}${path38}`, {
2086
2147
  ...init,
2087
2148
  headers: {
2088
2149
  Authorization: auth,
@@ -2092,13 +2153,13 @@ var PixelLabClient = class {
2092
2153
  });
2093
2154
  const text = await res.text();
2094
2155
  if (!res.ok) {
2095
- throw new PixelLabError(`${init?.method ?? "GET"} ${path36} \u2192 ${res.status}`, res.status, text);
2156
+ throw new PixelLabError(`${init?.method ?? "GET"} ${path38} \u2192 ${res.status}`, res.status, text);
2096
2157
  }
2097
2158
  if (!text) return {};
2098
2159
  try {
2099
2160
  return JSON.parse(text);
2100
2161
  } catch {
2101
- throw new Error(`${init?.method ?? "GET"} ${path36} returned invalid JSON`);
2162
+ throw new Error(`${init?.method ?? "GET"} ${path38} returned invalid JSON`);
2102
2163
  }
2103
2164
  }
2104
2165
  async balance() {
@@ -2503,10 +2564,10 @@ var PixelLabClient = class {
2503
2564
  const palette = args.paletteSwatchBase64 ? { color_image: { type: "base64", base64: args.paletteSwatchBase64, format: "png" }, force_colors: true } : {};
2504
2565
  const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
2505
2566
  const south = args.reference?.south;
2506
- let path36;
2567
+ let path38;
2507
2568
  let body;
2508
2569
  if (args.mode === "v3") {
2509
- path36 = "/create-character-v3";
2570
+ path38 = "/create-character-v3";
2510
2571
  body = {
2511
2572
  description: args.description,
2512
2573
  image_size: imageSize,
@@ -2520,7 +2581,7 @@ var PixelLabClient = class {
2520
2581
  ...args.enhancePrompt ? { enhance_prompt: true } : {}
2521
2582
  };
2522
2583
  } else if (args.mode === "pro-flash") {
2523
- path36 = "/create-character-pro-flash";
2584
+ path38 = "/create-character-pro-flash";
2524
2585
  const traits = args.styleTraits;
2525
2586
  body = {
2526
2587
  description: args.description,
@@ -2544,7 +2605,7 @@ var PixelLabClient = class {
2544
2605
  } : {}
2545
2606
  };
2546
2607
  } else if (args.mode === "pro") {
2547
- path36 = "/create-character-pro";
2608
+ path38 = "/create-character-pro";
2548
2609
  body = {
2549
2610
  description: args.description,
2550
2611
  image_size: imageSize,
@@ -2558,7 +2619,7 @@ var PixelLabClient = class {
2558
2619
  ...!south && args.styleCharacterId ? { style_character_id: args.styleCharacterId } : {}
2559
2620
  };
2560
2621
  } else {
2561
- path36 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
2622
+ path38 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
2562
2623
  body = {
2563
2624
  description: args.description,
2564
2625
  image_size: imageSize,
@@ -2575,8 +2636,8 @@ var PixelLabClient = class {
2575
2636
  ...palette
2576
2637
  };
2577
2638
  }
2578
- const raw = await this.request(path36, { method: "POST", body: JSON.stringify(body) });
2579
- return validateResponse(CharacterSubmitSchema, raw, path36.slice(1));
2639
+ const raw = await this.request(path38, { method: "POST", body: JSON.stringify(body) });
2640
+ return validateResponse(CharacterSubmitSchema, raw, path38.slice(1));
2580
2641
  }
2581
2642
  /** A text edit of an existing character, applied to every direction and stored as a sibling. */
2582
2643
  async createCharacterState(args) {
@@ -2953,6 +3014,27 @@ var PixelLabClient = class {
2953
3014
  const pngs = cleanupResults(res.images, images.length, "correct-pixelart");
2954
3015
  return { png: pngs[0], pngs, usage: res.usage };
2955
3016
  }
3017
+ /**
3018
+ * `/remove-background`: a transparent PNG from one pixel-art image,
3019
+ * synchronously, like the rest of the Cleanup tier. The API takes at most
3020
+ * 400x400 worth of area, one image per call. `simple` is faster and suits
3021
+ * flat backgrounds; `complex` is slower and handles detailed edges. The
3022
+ * schema's response example is dollar-denominated (`usd: 0.01`); the
3023
+ * subscription cost measured 1 generation (docs/ENDPOINTS.md), and the
3024
+ * default `simple` task is the one measured.
3025
+ */
3026
+ async removeBackground(args) {
3027
+ const body = { image: args.image, image_size: { width: args.width, height: args.height } };
3028
+ if (args.task) body.background_removal_task = args.task === "complex" ? "remove_complex_background" : "remove_simple_background";
3029
+ if (args.text) body.text = args.text;
3030
+ if (args.seed != null) body.seed = args.seed;
3031
+ const res = validateResponse(
3032
+ RemoveBackgroundResponseSchema,
3033
+ await this.request("/remove-background", { method: "POST", body: JSON.stringify(body) }),
3034
+ "remove-background"
3035
+ );
3036
+ return { png: Buffer.from(res.image.base64, "base64"), usage: res.usage };
3037
+ }
2956
3038
  /**
2957
3039
  * `/unzoom`: recover native-resolution pixel art from an upscaled image (a
2958
3040
  * 32x32 sprite saved at 512x512). Synchronous like the Cleanup tier.
@@ -3061,6 +3143,7 @@ var PixelLabClient = class {
3061
3143
  */
3062
3144
  async animateWithTextV3(args) {
3063
3145
  const body = { first_frame: args.firstFrame, action: args.action };
3146
+ if (args.driftThreshold != null) body.drift_threshold = args.driftThreshold;
3064
3147
  if (args.lastFrame) body.last_frame = args.lastFrame;
3065
3148
  if (args.frameCount != null) body.frame_count = args.frameCount;
3066
3149
  if (args.seed != null) body.seed = args.seed;
@@ -3080,6 +3163,10 @@ var PixelLabClient = class {
3080
3163
  */
3081
3164
  async animatePixminimax(args) {
3082
3165
  const body = { first_frame: args.firstFrame, description: args.description };
3166
+ if (args.view) body.view = args.view;
3167
+ if (args.subjectDescription) body.subject_description = args.subjectDescription;
3168
+ if (args.initialPose) body.initial_pose = args.initialPose;
3169
+ if (args.driftThreshold != null) body.drift_threshold = args.driftThreshold;
3083
3170
  if (args.lastFrame) body.last_frame = args.lastFrame;
3084
3171
  if (args.frameCount != null) body.frame_count = args.frameCount;
3085
3172
  if (args.seed != null) body.seed = args.seed;
@@ -3115,6 +3202,7 @@ var PixelLabClient = class {
3115
3202
  keypoints: args.keypoints,
3116
3203
  direction: args.direction
3117
3204
  };
3205
+ if (args.view) body.view = args.view;
3118
3206
  if (args.templateId) body.template_id = args.templateId;
3119
3207
  if (args.action) body.action = args.action;
3120
3208
  if (args.description) body.description = args.description;
@@ -3762,10 +3850,10 @@ var import_node_fs = require("fs");
3762
3850
  function sha256(data) {
3763
3851
  return (0, import_node_crypto.createHash)("sha256").update(data).digest("hex");
3764
3852
  }
3765
- async function sha256File(path36) {
3853
+ async function sha256File(path38) {
3766
3854
  return new Promise((resolve, reject) => {
3767
3855
  const hash = (0, import_node_crypto.createHash)("sha256");
3768
- const stream = (0, import_node_fs.createReadStream)(path36);
3856
+ const stream = (0, import_node_fs.createReadStream)(path38);
3769
3857
  stream.on("data", (chunk2) => hash.update(chunk2));
3770
3858
  stream.on("error", reject);
3771
3859
  stream.on("end", () => resolve(hash.digest("hex")));
@@ -3889,6 +3977,10 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
3889
3977
  keypointsSha256: spec.revision.keypointsSha256,
3890
3978
  skeletonTemplate: spec.revision.skeletonTemplate,
3891
3979
  description: spec.revision.description,
3980
+ removalTask: spec.revision.removalTask,
3981
+ driftThreshold: spec.revision.driftThreshold,
3982
+ subjectDescription: spec.revision.subjectDescription,
3983
+ initialPose: spec.revision.initialPose,
3892
3984
  engine: spec.revision.engine
3893
3985
  } : void 0
3894
3986
  })
@@ -4064,7 +4156,7 @@ var PixelLabProvider = class _PixelLabProvider {
4064
4156
  * outpaint path yet.
4065
4157
  */
4066
4158
  supportsRevision(mode2) {
4067
- 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";
4159
+ 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";
4068
4160
  }
4069
4161
  /** PixelLab's own constraints: submissions must be >2s apart, and
4070
4162
  * background jobs in flight are capped by subscription tier (Tier 1=8,
@@ -4092,6 +4184,9 @@ var PixelLabProvider = class _PixelLabProvider {
4092
4184
  if (spec.batch?.role === "member") {
4093
4185
  return { unit: "generations", amount: 0, candidates: 1 };
4094
4186
  }
4187
+ if (spec.revision?.mode === "remove-background") {
4188
+ return { unit: "generations", amount: 1, candidates: 1 };
4189
+ }
4095
4190
  if (spec.revision?.mode === "reduce-colors" || spec.revision?.mode === "correct-pixelart") {
4096
4191
  return { unit: "generations", amount: 0.1, candidates: 1 };
4097
4192
  }
@@ -4180,6 +4275,12 @@ var PixelLabProvider = class _PixelLabProvider {
4180
4275
  throw new Error(`PixelLab correct-pixelart source is ${width}x${height}; the API takes at most 1024 pixels per side`);
4181
4276
  }
4182
4277
  }
4278
+ if (spec.revision?.mode === "remove-background") {
4279
+ const { sourceWidth: width, sourceHeight: height } = spec.revision;
4280
+ if (width != null && height != null && (width > 400 || height > 400)) {
4281
+ throw new Error(`PixelLab remove-background source is ${width}x${height}; the API takes at most 400x400`);
4282
+ }
4283
+ }
4183
4284
  if (spec.revision?.mode === "animate-skeleton") {
4184
4285
  const { sourceWidth: width, sourceHeight: height, frames, skeletonTemplate } = spec.revision;
4185
4286
  if (width != null && height != null && (width > 256 || height > 256)) {
@@ -4200,6 +4301,9 @@ var PixelLabProvider = class _PixelLabProvider {
4200
4301
  if (mode2 === "animate" && frames != null && frames > 16) {
4201
4302
  throw new Error(`PixelLab animate takes 4 to 16 frames; ${frames} is too many (use animate-pixminimax for up to 40)`);
4202
4303
  }
4304
+ if (mode2 === "animate-pixminimax" && frames != null && frames % 4 !== 0) {
4305
+ throw new Error(`PixelLab animate-pixminimax takes a multiple of 4 frames (4 to 40); ${frames} is not one`);
4306
+ }
4203
4307
  if (lastFrameWidth != null && lastFrameHeight != null && width != null && height != null && (lastFrameWidth !== width || lastFrameHeight !== height)) {
4204
4308
  throw new Error(
4205
4309
  `PixelLab ${mode2} lastFrame is ${lastFrameWidth}x${lastFrameHeight}; source is ${width}x${height} \u2014 they must match`
@@ -5291,6 +5395,18 @@ var PixelLabProvider = class _PixelLabProvider {
5291
5395
  const res2 = await this.client.correctPixelart({ images, strength: revision.strength });
5292
5396
  return this.cacheCleanupResult(res2.pngs, res2.usage, roles, revision.sourceFps);
5293
5397
  }
5398
+ if (revision.mode === "remove-background") {
5399
+ if (width == null || height == null) throw new Error(`${spec.styleId}/${spec.assetId}: remove-background needs the source's size`);
5400
+ const res2 = await this.client.removeBackground({
5401
+ image,
5402
+ width,
5403
+ height,
5404
+ task: revision.removalTask,
5405
+ text: revision.description,
5406
+ seed: spec.seed
5407
+ });
5408
+ return this.cacheCleanupResult([res2.png], res2.usage, roles, revision.sourceFps);
5409
+ }
5294
5410
  if (revision.mode === "interpolate") {
5295
5411
  const lastFrame = this.readLastFrame(spec);
5296
5412
  if (!lastFrame) throw new Error(`${spec.styleId}/${spec.assetId}: interpolate needs lastFrame, the ending keyframe`);
@@ -5326,7 +5442,8 @@ var PixelLabProvider = class _PixelLabProvider {
5326
5442
  frameCount: revision.frames,
5327
5443
  seed: spec.seed,
5328
5444
  noBackground: spec.noBackground,
5329
- enhancePrompt: revision.enhancePrompt
5445
+ enhancePrompt: revision.enhancePrompt,
5446
+ driftThreshold: revision.driftThreshold
5330
5447
  });
5331
5448
  return { jobId: res3.background_job_id };
5332
5449
  }
@@ -5338,7 +5455,11 @@ var PixelLabProvider = class _PixelLabProvider {
5338
5455
  seed: spec.seed,
5339
5456
  noBackground: spec.noBackground,
5340
5457
  enhancePrompt: revision.enhancePrompt,
5341
- direction: revision.direction
5458
+ direction: revision.direction,
5459
+ ...spec.view === "side" || spec.view === "low top-down" || spec.view === "high top-down" ? { view: spec.view } : {},
5460
+ subjectDescription: revision.subjectDescription,
5461
+ initialPose: revision.initialPose,
5462
+ driftThreshold: revision.driftThreshold
5342
5463
  });
5343
5464
  return { jobId: res2.background_job_id };
5344
5465
  }
@@ -5355,6 +5476,9 @@ var PixelLabProvider = class _PixelLabProvider {
5355
5476
  firstFrameKeypoints: revision.skeleton.firstFrameKeypoints,
5356
5477
  keypoints: revision.skeleton.frames,
5357
5478
  direction: revision.direction,
5479
+ // The style's camera, when it is one this endpoint knows; its enum is
5480
+ // narrower than a style's free-string `view`.
5481
+ ...spec.view === "side" || spec.view === "low top-down" || spec.view === "high top-down" ? { view: spec.view } : {},
5358
5482
  templateId: revision.skeletonTemplate,
5359
5483
  action: spec.prompt,
5360
5484
  description: revision.description,
@@ -5425,7 +5549,7 @@ var PixelLabProvider = class _PixelLabProvider {
5425
5549
  }
5426
5550
  async poll(jobId, generator, context) {
5427
5551
  const revisionMode = context?.spec?.revision?.mode;
5428
- if (revisionMode === "reduce-colors" || revisionMode === "correct-pixelart") {
5552
+ if (revisionMode === "reduce-colors" || revisionMode === "correct-pixelart" || revisionMode === "remove-background") {
5429
5553
  return this.pollCachedRevision(jobId, context);
5430
5554
  }
5431
5555
  if (revisionMode === "animate" || revisionMode === "animate-pixminimax" || revisionMode === "animate-skeleton" || revisionMode === "interpolate" || revisionMode === "edit-animation") {
@@ -6133,9 +6257,9 @@ var RetroDiffusionClient = class {
6133
6257
  if (!response.ok) throw new ProviderError("retrodiffusion", `Retro Diffusion download failed (${response.status})`, { status: response.status });
6134
6258
  return Buffer.from(await response.arrayBuffer());
6135
6259
  }
6136
- async call(path36, init = {}) {
6260
+ async call(path38, init = {}) {
6137
6261
  if (!this.token) throw new Error("RD_API_KEY is not set");
6138
- const response = await this.request(`${this.baseUrl}${path36}`, {
6262
+ const response = await this.request(`${this.baseUrl}${path38}`, {
6139
6263
  ...init,
6140
6264
  headers: {
6141
6265
  "Content-Type": "application/json",
@@ -6444,8 +6568,13 @@ function retroError(value) {
6444
6568
  }
6445
6569
 
6446
6570
  // src/providers/scenario.ts
6571
+ var import_node_crypto3 = require("crypto");
6572
+ var import_node_fs3 = require("fs");
6573
+ var import_node_os2 = __toESM(require("os"), 1);
6574
+ var import_node_path2 = __toESM(require("path"), 1);
6447
6575
  var DEFAULT_BASE_URL2 = "https://api.cloud.scenario.com/v1";
6448
6576
  var SOURCE_PROTOCOL2 = "scenario:";
6577
+ var SUBMIT_RETRIES = 8;
6449
6578
  var PROTECTED_PARAMETERS = /* @__PURE__ */ new Set([
6450
6579
  "dryRun",
6451
6580
  "height",
@@ -6492,7 +6621,11 @@ var ScenarioClient = class {
6492
6621
  async submit(modelId, body, projectId) {
6493
6622
  const response = await this.call(
6494
6623
  `/generate/custom/${encodeURIComponent(modelId)}`,
6495
- { method: "POST", body: JSON.stringify(body) },
6624
+ // A 429 here means this account's concurrent-job limit refused the
6625
+ // request, so nothing was accepted or billed; wait it out instead of
6626
+ // failing the asset. The default four retries give up after about half
6627
+ // a minute, before a running job has finished.
6628
+ { method: "POST", body: JSON.stringify(body), retries: SUBMIT_RETRIES },
6496
6629
  { projectId }
6497
6630
  );
6498
6631
  const job = response?.job;
@@ -6500,6 +6633,29 @@ var ScenarioClient = class {
6500
6633
  if (!jobId) throw new Error("Scenario did not return an async job id");
6501
6634
  return jobId;
6502
6635
  }
6636
+ /**
6637
+ * Uploads one image (POST /assets) and returns its asset id, which a model's
6638
+ * reference input takes. Free, and idempotent per image: the id is kept by
6639
+ * content hash so a rerun does not upload the same bytes again.
6640
+ */
6641
+ async uploadImage(image, projectId) {
6642
+ const bytes = Buffer.from(image.base64, "base64");
6643
+ const digest2 = sha256(bytes);
6644
+ const cached = readUploadCache()[uploadCacheKey(this.baseUrl, projectId, digest2)];
6645
+ if (cached) return cached;
6646
+ const response = await this.call(
6647
+ "/assets",
6648
+ {
6649
+ method: "POST",
6650
+ body: JSON.stringify({ image: image.base64, name: `pixelkiln-${digest2.slice(0, 12)}.${image.format === "png" ? "png" : "jpg"}` })
6651
+ },
6652
+ { projectId }
6653
+ );
6654
+ const id = response?.asset?.id;
6655
+ if (typeof id !== "string" || !id) throw new Error("Scenario did not return an uploaded asset id");
6656
+ writeUploadCache(uploadCacheKey(this.baseUrl, projectId, digest2), id);
6657
+ return id;
6658
+ }
6503
6659
  async job(id, projectId) {
6504
6660
  const response = await this.call(`/jobs/${encodeURIComponent(id)}`, {}, { projectId });
6505
6661
  const raw = response?.job;
@@ -6626,6 +6782,14 @@ var ScenarioProvider = class _ScenarioProvider {
6626
6782
  supports(generator) {
6627
6783
  return generator === "map";
6628
6784
  }
6785
+ /**
6786
+ * An edit is a generation with the parent as a reference image, which the
6787
+ * editing models (GPT Image, Gemini, FLUX) take. Masked inpainting and the
6788
+ * PixelLab-specific modes have no common Scenario input.
6789
+ */
6790
+ supportsRevision(mode2) {
6791
+ return mode2 === "image-to-image";
6792
+ }
6629
6793
  estimate(spec) {
6630
6794
  const options = scenarioOptions(spec);
6631
6795
  return {
@@ -6646,11 +6810,31 @@ var ScenarioProvider = class _ScenarioProvider {
6646
6810
  if (!Number.isInteger(count) || count < 1 || count > 4) {
6647
6811
  throw new Error("Scenario numOutputs must be a whole number from 1 to 4");
6648
6812
  }
6649
- if (spec.width < 128 || spec.width > 2048 || spec.width % 16 !== 0 || spec.height < 128 || spec.height > 2048 || spec.height % 16 !== 0) {
6650
- throw new Error("Scenario width and height must be multiples of 16 from 128 to 2048");
6813
+ if (!Number.isInteger(spec.width) || spec.width < 16 || spec.width > 4096 || !Number.isInteger(spec.height) || spec.height < 16 || spec.height > 4096) {
6814
+ throw new Error("Scenario width and height must be whole numbers from 16 to 4096");
6651
6815
  }
6652
- if (styleImages.length) {
6653
- throw new Error("Scenario styleImages are not supported yet; use a reusable Scenario model");
6816
+ const revision = spec.revision;
6817
+ if (revision) {
6818
+ if (revision.sourceMembers?.length) {
6819
+ throw new Error(
6820
+ `Scenario edits one image; ${revision.sourceAssetId} is a ${revision.sourceMembers.length}-member set`
6821
+ );
6822
+ }
6823
+ if (revision.strength != null) {
6824
+ throw new Error("Scenario image-to-image takes no strength; use the model's own parameters");
6825
+ }
6826
+ }
6827
+ const parameter = referenceParameter(options);
6828
+ if ((styleImages.length || revision) && Object.hasOwn(options.parameters ?? {}, parameter)) {
6829
+ throw new Error(
6830
+ `Scenario parameters.${parameter} is the reference-image input and is filled from styleImages and revisions`
6831
+ );
6832
+ }
6833
+ if (styleImages.length + (revision ? 1 : 0) > 1 && options.referenceArray === false) {
6834
+ throw new Error(`Scenario ${parameter} takes one image; use one style image or a revision, not both`);
6835
+ }
6836
+ if (options.referenceParameter != null && (typeof options.referenceParameter !== "string" || !options.referenceParameter.trim())) {
6837
+ throw new Error("Scenario referenceParameter must be a non-empty string");
6654
6838
  }
6655
6839
  if (options.projectId != null && (typeof options.projectId !== "string" || !options.projectId.trim())) {
6656
6840
  throw new Error("Scenario projectId must be a non-empty string");
@@ -6663,7 +6847,12 @@ var ScenarioProvider = class _ScenarioProvider {
6663
6847
  async submit(spec, styleImages) {
6664
6848
  this.validate(spec, styleImages);
6665
6849
  const options = scenarioOptions(spec);
6666
- const body = requestBody(spec, options);
6850
+ const references = [];
6851
+ if (spec.revision) references.push(readRevisionSource(spec));
6852
+ for (const image of styleImages) references.push({ base64: image.base64, format: image.format });
6853
+ const referenceAssetIds = [];
6854
+ for (const image of references) referenceAssetIds.push(await this.client.uploadImage(image, options.projectId));
6855
+ const body = requestBody(spec, options, referenceAssetIds);
6667
6856
  const quote = await this.client.quote(options.modelId, body, options.projectId);
6668
6857
  if (quote.total > options.maxComputeUnits + Number.EPSILON) {
6669
6858
  throw new Error(
@@ -6680,7 +6869,8 @@ var ScenarioProvider = class _ScenarioProvider {
6680
6869
  quotedComputeUnits: quote.total,
6681
6870
  creativeUnitsCost: quote.creativeUnitsCost,
6682
6871
  costDetails: quote.costDetails,
6683
- ipDetectionCost: quote.ipDetectionCost
6872
+ ipDetectionCost: quote.ipDetectionCost,
6873
+ ...referenceAssetIds.length ? { referenceAssetIds } : {}
6684
6874
  }
6685
6875
  };
6686
6876
  }
@@ -6702,7 +6892,13 @@ var ScenarioProvider = class _ScenarioProvider {
6702
6892
  }
6703
6893
  const assetIds = scenarioAssetIds(job);
6704
6894
  if (!assetIds.length) return { status: "failed", error: "Scenario job returned no asset IDs" };
6705
- const assets = await this.assets(assetIds, reference.projectId);
6895
+ let assets;
6896
+ try {
6897
+ assets = await this.assets(assetIds, reference.projectId);
6898
+ } catch (error) {
6899
+ if (error instanceof UnsupportedScenarioOutput) return { status: "failed", error: error.message };
6900
+ throw error;
6901
+ }
6706
6902
  if (assets.length > 1) {
6707
6903
  return {
6708
6904
  status: "review",
@@ -6757,6 +6953,32 @@ var ScenarioProvider = class _ScenarioProvider {
6757
6953
  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));
6758
6954
  }
6759
6955
  };
6956
+ function uploadCachePath() {
6957
+ return process.env.SCENARIO_UPLOAD_CACHE ?? import_node_path2.default.join(import_node_os2.default.tmpdir(), "pixelkiln-scenario", "uploads.json");
6958
+ }
6959
+ function uploadCacheKey(baseUrl, projectId, digest2) {
6960
+ return (0, import_node_crypto3.createHash)("sha256").update(`${baseUrl}
6961
+ ${projectId ?? ""}
6962
+ ${digest2}`).digest("hex");
6963
+ }
6964
+ function readUploadCache() {
6965
+ try {
6966
+ const value = JSON.parse((0, import_node_fs3.readFileSync)(uploadCachePath(), "utf8"));
6967
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
6968
+ } catch {
6969
+ return {};
6970
+ }
6971
+ }
6972
+ function writeUploadCache(key, assetId) {
6973
+ try {
6974
+ const file = uploadCachePath();
6975
+ (0, import_node_fs3.mkdirSync)(import_node_path2.default.dirname(file), { recursive: true });
6976
+ const tmp = `${file}.${process.pid}.tmp`;
6977
+ (0, import_node_fs3.writeFileSync)(tmp, JSON.stringify({ ...readUploadCache(), [key]: assetId }));
6978
+ (0, import_node_fs3.renameSync)(tmp, file);
6979
+ } catch {
6980
+ }
6981
+ }
6760
6982
  function scenarioOptions(spec) {
6761
6983
  return spec.providerOptions;
6762
6984
  }
@@ -6772,9 +6994,25 @@ function scenarioBaseUrl(value) {
6772
6994
  }
6773
6995
  return url.href.replace(/\/$/, "");
6774
6996
  }
6775
- function requestBody(spec, options) {
6997
+ function referenceParameter(options) {
6998
+ return options.referenceParameter?.trim() || "referenceImages";
6999
+ }
7000
+ function readRevisionSource(spec) {
7001
+ const revision = spec.revision;
7002
+ if (!revision.sourceFile || !revision.sourceSha256 || !revision.sourceFormat) {
7003
+ throw new Error(`${spec.styleId}/${spec.assetId}: revision source is not ready`);
7004
+ }
7005
+ const bytes = (0, import_node_fs3.readFileSync)(revision.sourceFile);
7006
+ if (sha256(bytes) !== revision.sourceSha256) {
7007
+ throw new Error(`${spec.styleId}/${spec.assetId}: revision source changed after the manifest was resolved`);
7008
+ }
7009
+ return { base64: bytes.toString("base64"), format: revision.sourceFormat };
7010
+ }
7011
+ function requestBody(spec, options, referenceAssetIds = []) {
7012
+ const parameter = referenceParameter(options);
6776
7013
  return {
6777
7014
  ...options.parameters ?? {},
7015
+ ...referenceAssetIds.length ? { [parameter]: options.referenceArray === false ? referenceAssetIds[0] : referenceAssetIds } : {},
6778
7016
  prompt: spec.prompt,
6779
7017
  width: spec.width,
6780
7018
  height: spec.height,
@@ -6864,9 +7102,11 @@ function scenarioAssetIds(job) {
6864
7102
  function assetDownloadUrl(asset) {
6865
7103
  return asset.originalFileUrl || asset.url;
6866
7104
  }
7105
+ var UnsupportedScenarioOutput = class extends Error {
7106
+ };
6867
7107
  function assertPngAsset(asset) {
6868
7108
  if (asset.originalMimeType && asset.originalMimeType !== MediaType.PNG) {
6869
- throw new Error(
7109
+ throw new UnsupportedScenarioOutput(
6870
7110
  `Scenario asset ${asset.id} is ${asset.originalMimeType}; PixelKiln's Scenario MVP requires PNG output`
6871
7111
  );
6872
7112
  }
@@ -6931,9 +7171,9 @@ function redactScenarioError(value, secrets) {
6931
7171
  }
6932
7172
 
6933
7173
  // src/providers/comfyui.ts
6934
- var import_node_crypto3 = require("crypto");
7174
+ var import_node_crypto4 = require("crypto");
6935
7175
  var import_promises = require("fs/promises");
6936
- var import_node_path2 = __toESM(require("path"), 1);
7176
+ var import_node_path3 = __toESM(require("path"), 1);
6937
7177
  var DEFAULT_BASE_URL3 = "http://127.0.0.1:8188";
6938
7178
  var BUILTIN_BINDINGS = /* @__PURE__ */ new Set([
6939
7179
  "prompt",
@@ -6960,7 +7200,7 @@ var ComfyUIClient = class {
6960
7200
  async submit(workflow) {
6961
7201
  const value = await this.json("prompt", {
6962
7202
  method: "POST",
6963
- body: JSON.stringify({ prompt: workflow, client_id: (0, import_node_crypto3.randomUUID)() })
7203
+ body: JSON.stringify({ prompt: workflow, client_id: (0, import_node_crypto4.randomUUID)() })
6964
7204
  });
6965
7205
  if (!isObject(value)) throw new Error("ComfyUI returned an invalid queue response");
6966
7206
  const promptId = value.prompt_id;
@@ -7070,7 +7310,7 @@ var ComfyUIProvider = class _ComfyUIProvider {
7070
7310
  }
7071
7311
  async resolveOptions(value, context) {
7072
7312
  const options = parseOptions(value);
7073
- const workflowPath = import_node_path2.default.resolve(context.root, options.workflowFile);
7313
+ const workflowPath = import_node_path3.default.resolve(context.root, options.workflowFile);
7074
7314
  let raw;
7075
7315
  try {
7076
7316
  raw = await (0, import_promises.readFile)(workflowPath, "utf8");
@@ -7125,7 +7365,7 @@ var ComfyUIProvider = class _ComfyUIProvider {
7125
7365
  if (typeof item !== "string") {
7126
7366
  throw new Error(`ComfyUI ${label} must be a manifest-relative PNG or JPEG path`);
7127
7367
  }
7128
- const file = import_node_path2.default.resolve(context.root, item);
7368
+ const file = import_node_path3.default.resolve(context.root, item);
7129
7369
  let bytes;
7130
7370
  try {
7131
7371
  bytes = await (0, import_promises.readFile)(file);
@@ -7837,7 +8077,7 @@ function canonical(value) {
7837
8077
  }
7838
8078
 
7839
8079
  // src/providers/fake.ts
7840
- var import_node_crypto4 = require("crypto");
8080
+ var import_node_crypto5 = require("crypto");
7841
8081
  var FAKE_PNG = Buffer.from(
7842
8082
  "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
7843
8083
  "base64"
@@ -7939,7 +8179,7 @@ var FakeProvider = class {
7939
8179
  }
7940
8180
  async download(url) {
7941
8181
  if (!url.startsWith("fake://")) throw new Error(`fake provider cannot download ${url}`);
7942
- const salt = (0, import_node_crypto4.createHash)("sha256").update(url).digest();
8182
+ const salt = (0, import_node_crypto5.createHash)("sha256").update(url).digest();
7943
8183
  return Buffer.concat([FAKE_PNG, salt.subarray(0, 4)]);
7944
8184
  }
7945
8185
  async balance() {
@@ -8087,8 +8327,8 @@ registerProvider({
8087
8327
 
8088
8328
  // src/manifest.ts
8089
8329
  var import_promises3 = require("fs/promises");
8090
- var import_node_fs5 = require("fs");
8091
- var import_node_path5 = __toESM(require("path"), 1);
8330
+ var import_node_fs6 = require("fs");
8331
+ var import_node_path6 = __toESM(require("path"), 1);
8092
8332
 
8093
8333
  // src/loop-directions.ts
8094
8334
  var SINGLE_OUTPUT_FIELDS = ["file", "cell", "remoteId", "source", "sourceByStyle", "outputRole"];
@@ -8176,15 +8416,15 @@ function isObject2(value) {
8176
8416
  }
8177
8417
 
8178
8418
  // src/outputs.ts
8179
- var import_node_fs4 = require("fs");
8180
- var import_node_path4 = __toESM(require("path"), 1);
8419
+ var import_node_fs5 = require("fs");
8420
+ var import_node_path5 = __toESM(require("path"), 1);
8181
8421
 
8182
8422
  // src/lock.ts
8183
8423
  var import_promises2 = require("fs/promises");
8184
- var import_node_fs3 = require("fs");
8185
- var import_node_path3 = __toESM(require("path"), 1);
8424
+ var import_node_fs4 = require("fs");
8425
+ var import_node_path4 = __toESM(require("path"), 1);
8186
8426
  async function loadLock(lockPath) {
8187
- if (!(0, import_node_fs3.existsSync)(lockPath)) return { version: 2, entries: {} };
8427
+ if (!(0, import_node_fs4.existsSync)(lockPath)) return { version: 2, entries: {} };
8188
8428
  try {
8189
8429
  return parseLock(JSON.parse(await (0, import_promises2.readFile)(lockPath, "utf8")));
8190
8430
  } catch (err) {
@@ -8199,7 +8439,7 @@ var saveQueues = /* @__PURE__ */ new Map();
8199
8439
  var dirtyPatches = /* @__PURE__ */ new WeakMap();
8200
8440
  var dirtyDeletes = /* @__PURE__ */ new WeakMap();
8201
8441
  function saveLock(lockPath, lock) {
8202
- const queueKey = import_node_path3.default.resolve(lockPath);
8442
+ const queueKey = import_node_path4.default.resolve(lockPath);
8203
8443
  const previous = saveQueues.get(queueKey) ?? Promise.resolve();
8204
8444
  const next = previous.catch(() => {
8205
8445
  }).then(() => writeLockNow(lockPath, lock));
@@ -8211,7 +8451,7 @@ function saveLock(lockPath, lock) {
8211
8451
  return next;
8212
8452
  }
8213
8453
  async function writeLockNow(lockPath, lock) {
8214
- await (0, import_promises2.mkdir)(import_node_path3.default.dirname(import_node_path3.default.resolve(lockPath)), { recursive: true });
8454
+ await (0, import_promises2.mkdir)(import_node_path4.default.dirname(import_node_path4.default.resolve(lockPath)), { recursive: true });
8215
8455
  const release = await acquireFileLock(lockPath);
8216
8456
  try {
8217
8457
  await writeLockWhileHeld(lockPath, lock);
@@ -8221,7 +8461,7 @@ async function writeLockNow(lockPath, lock) {
8221
8461
  }
8222
8462
  async function writeLockWhileHeld(file, lock) {
8223
8463
  let disk = { version: 2, entries: {} };
8224
- if ((0, import_node_fs3.existsSync)(file)) {
8464
+ if ((0, import_node_fs4.existsSync)(file)) {
8225
8465
  try {
8226
8466
  disk = parseLock(JSON.parse(await (0, import_promises2.readFile)(file, "utf8")));
8227
8467
  } catch (err) {
@@ -8356,22 +8596,22 @@ function totalSpend(lock, unit = "generations") {
8356
8596
 
8357
8597
  // src/outputs.ts
8358
8598
  function portableOutputPath(file, manifestDir) {
8359
- const root = import_node_path4.default.resolve(manifestDir);
8360
- const absolute = import_node_path4.default.isAbsolute(file) ? import_node_path4.default.normalize(file) : import_node_path4.default.resolve(root, file);
8361
- const relative = import_node_path4.default.relative(root, absolute);
8362
- const value = import_node_path4.default.isAbsolute(relative) ? absolute : relative;
8363
- return value.split(import_node_path4.default.sep).join("/");
8599
+ const root = import_node_path5.default.resolve(manifestDir);
8600
+ const absolute = import_node_path5.default.isAbsolute(file) ? import_node_path5.default.normalize(file) : import_node_path5.default.resolve(root, file);
8601
+ const relative = import_node_path5.default.relative(root, absolute);
8602
+ const value = import_node_path5.default.isAbsolute(relative) ? absolute : relative;
8603
+ return value.split(import_node_path5.default.sep).join("/");
8364
8604
  }
8365
8605
  function resolveOutputPath(recordedPath, manifestDir) {
8366
- if (import_node_path4.default.isAbsolute(recordedPath)) return import_node_path4.default.normalize(recordedPath);
8367
- if (import_node_path4.default.win32.isAbsolute(recordedPath)) return recordedPath;
8368
- return import_node_path4.default.resolve(manifestDir, recordedPath.split(/[\\/]/).join(import_node_path4.default.sep));
8606
+ if (import_node_path5.default.isAbsolute(recordedPath)) return import_node_path5.default.normalize(recordedPath);
8607
+ if (import_node_path5.default.win32.isAbsolute(recordedPath)) return recordedPath;
8608
+ return import_node_path5.default.resolve(manifestDir, recordedPath.split(/[\\/]/).join(import_node_path5.default.sep));
8369
8609
  }
8370
8610
  function expectedOutputPath(spec, role, index, total, mediaType) {
8371
8611
  return memberPath(spec.outFile, role, index, total, mediaType);
8372
8612
  }
8373
8613
  function memberPath(file, role, index, total, mediaType) {
8374
- const originalExt = import_node_path4.default.extname(file);
8614
+ const originalExt = import_node_path5.default.extname(file);
8375
8615
  const ext = mediaType ? mediaExtension(mediaType) : originalExt || ".png";
8376
8616
  const stem = originalExt ? file.slice(0, -originalExt.length) : file;
8377
8617
  if (total === 1) return `${stem}${ext}`;
@@ -8385,10 +8625,10 @@ function isFrameSetEntry(entry) {
8385
8625
  return entry.generator === "frames" && isMemberSetEntry(entry);
8386
8626
  }
8387
8627
  function sourceIsStem(source, entry, manifestDir) {
8388
- return isMemberSetEntry(entry) && !(0, import_node_fs4.existsSync)(import_node_path4.default.resolve(manifestDir, source));
8628
+ return isMemberSetEntry(entry) && !(0, import_node_fs5.existsSync)(import_node_path5.default.resolve(manifestDir, source));
8389
8629
  }
8390
8630
  function sourceOutputPath(source, entry, index, manifestDir) {
8391
- const absolute = import_node_path4.default.resolve(manifestDir, source);
8631
+ const absolute = import_node_path5.default.resolve(manifestDir, source);
8392
8632
  if (!sourceIsStem(source, entry, manifestDir)) return absolute;
8393
8633
  const output = entry.outputs[index];
8394
8634
  return memberPath(absolute, output.role, index, entry.outputs.length, output.mediaType);
@@ -8475,7 +8715,7 @@ function resolveSpecOutputs(spec, lock, manifestDir) {
8475
8715
  id: spec.assetId,
8476
8716
  index: 0,
8477
8717
  path: spec.outFile,
8478
- absolutePath: import_node_path4.default.resolve(spec.outFile),
8718
+ absolutePath: import_node_path5.default.resolve(spec.outFile),
8479
8719
  sha256: ""
8480
8720
  }];
8481
8721
  }
@@ -8491,8 +8731,8 @@ function frameSetFps(entry) {
8491
8731
 
8492
8732
  // src/manifest.ts
8493
8733
  async function loadManifest(manifestPath) {
8494
- const abs = import_node_path5.default.resolve(manifestPath);
8495
- if (!(0, import_node_fs5.existsSync)(abs)) throw new ProjectError(`No manifest at ${abs}`);
8734
+ const abs = import_node_path6.default.resolve(manifestPath);
8735
+ if (!(0, import_node_fs6.existsSync)(abs)) throw new ProjectError(`No manifest at ${abs}`);
8496
8736
  const expansion = expandLoopDirections(JSON.parse(await (0, import_promises3.readFile)(abs, "utf8")));
8497
8737
  if (expansion.issues.length) {
8498
8738
  throw new ProjectError(`Manifest at ${abs} is invalid:
@@ -8584,7 +8824,7 @@ ${issues}`);
8584
8824
  ${unknownReferences.map((i) => ` ${i}`).join("\n")}`);
8585
8825
  }
8586
8826
  const loopFamilies = Object.keys(expansion.families).length ? { loopFamilies: expansion.families } : {};
8587
- return { manifest: parsed.data, root: import_node_path5.default.dirname(abs), path: abs, ...loopFamilies };
8827
+ return { manifest: parsed.data, root: import_node_path6.default.dirname(abs), path: abs, ...loopFamilies };
8588
8828
  }
8589
8829
  function parentAssetId(asset) {
8590
8830
  return asset?.revision?.from ?? asset?.state?.of ?? asset?.animation?.of ?? asset?.portrait?.of ?? asset?.outfit?.of;
@@ -8685,10 +8925,10 @@ async function resolveSpecs(loaded, filter) {
8685
8925
  }
8686
8926
  const styleImageCache = /* @__PURE__ */ new Map();
8687
8927
  async function loadStyleImage(rel, what = "Style image") {
8688
- const abs = import_node_path5.default.resolve(root, rel);
8928
+ const abs = import_node_path6.default.resolve(root, rel);
8689
8929
  let hit = styleImageCache.get(abs);
8690
8930
  if (!hit) {
8691
- if (!(0, import_node_fs5.existsSync)(abs)) throw new Error(`${what} not found: ${abs}`);
8931
+ if (!(0, import_node_fs6.existsSync)(abs)) throw new Error(`${what} not found: ${abs}`);
8692
8932
  const buf = await (0, import_promises3.readFile)(abs);
8693
8933
  const metadata = imageMetadata(buf);
8694
8934
  if (!metadata) throw new Error(`${what} is not a readable PNG or JPEG: ${abs}`);
@@ -8715,7 +8955,7 @@ async function resolveSpecs(loaded, filter) {
8715
8955
  styleImageDimensions.push(loadedImage);
8716
8956
  }
8717
8957
  const resolvedImages = style.styleImages.map((image) => {
8718
- const hit = styleImageCache.get(import_node_path5.default.resolve(root, image.path));
8958
+ const hit = styleImageCache.get(import_node_path6.default.resolve(root, image.path));
8719
8959
  return { base64: hit.base64, width: hit.width, height: hit.height, format: hit.format };
8720
8960
  });
8721
8961
  const styleSpecs = /* @__PURE__ */ new Map();
@@ -8816,9 +9056,9 @@ async function resolveSpecs(loaded, filter) {
8816
9056
  } else {
8817
9057
  prompt = [style.promptPrefix, subject, style.promptSuffix].map((p) => p.trim()).filter(Boolean).join(", ");
8818
9058
  }
8819
- const relFile = asset.file ?? import_node_path5.default.join(asset.category ?? "", `${assetId}.png`);
8820
- const outFile = import_node_path5.default.resolve(root, style.outDir, relFile);
8821
- const qualityOutFile = style.quality ? import_node_path5.default.resolve(root, style.quality.outDir, pngPath(relFile)) : void 0;
9059
+ const relFile = asset.file ?? import_node_path6.default.join(asset.category ?? "", `${assetId}.png`);
9060
+ const outFile = import_node_path6.default.resolve(root, style.outDir, relFile);
9061
+ const qualityOutFile = style.quality ? import_node_path6.default.resolve(root, style.quality.outDir, pngPath(relFile)) : void 0;
8822
9062
  const tileSize = generator === "tiles" ? size : style.tileSize ?? 32;
8823
9063
  const tileVariations = tileFeatureOutputCount(generator === "tiles" ? style.tileFeature : void 0) ?? tileVariationCount(countNumberedDescriptions(prompt));
8824
9064
  const terrainTiles = generator === "terrain" ? terrainTileCount(style.terrainTransitionSize) : 0;
@@ -8913,7 +9153,7 @@ async function resolveSpecs(loaded, filter) {
8913
9153
  minGridConfidence: style.quality.minGridConfidence,
8914
9154
  ...style.quality.minTransparency == null ? {} : { minTransparency: style.quality.minTransparency },
8915
9155
  ...style.quality.fixerRevision ? { fixerRevision: style.quality.fixerRevision } : {},
8916
- ...style.quality.fixerPython ? { fixerPython: import_node_path5.default.resolve(root, style.quality.fixerPython) } : {},
9156
+ ...style.quality.fixerPython ? { fixerPython: import_node_path6.default.resolve(root, style.quality.fixerPython) } : {},
8917
9157
  ...style.quality.fps == null ? {} : { fps: style.quality.fps }
8918
9158
  }
8919
9159
  } : {},
@@ -8996,7 +9236,7 @@ async function resolveSpecs(loaded, filter) {
8996
9236
  const file = memberPath(anchorSpec.outFile, "south", 0, anchorSpec.character.directions, MediaType.PNG);
8997
9237
  resolved.character = {
8998
9238
  ...resolved.character,
8999
- styleAnchor: { assetId: anchorId, spec: anchorSpec, file, sha256: (0, import_node_fs5.existsSync)(file) ? await sha256File(file) : null }
9239
+ styleAnchor: { assetId: anchorId, spec: anchorSpec, file, sha256: (0, import_node_fs6.existsSync)(file) ? await sha256File(file) : null }
9000
9240
  };
9001
9241
  }
9002
9242
  const characterParent = asset.state?.of ?? asset.animation?.of ?? asset.portrait?.of;
@@ -9009,7 +9249,7 @@ async function resolveSpecs(loaded, filter) {
9009
9249
  parentAssetId: characterParent,
9010
9250
  parentSpec,
9011
9251
  parentFile,
9012
- parentSha256: (0, import_node_fs5.existsSync)(parentFile) ? await sha256File(parentFile) : null,
9252
+ parentSha256: (0, import_node_fs6.existsSync)(parentFile) ? await sha256File(parentFile) : null,
9013
9253
  // A state and its animations keep the engine and rotations of the base.
9014
9254
  mode: resolved.character.kind === "base" ? resolved.character.mode : parentSpec.character?.mode ?? resolved.character.mode,
9015
9255
  directions: resolved.character.kind === "base" ? resolved.character.directions : parentSpec.character?.directions ?? resolved.character.directions
@@ -9027,7 +9267,7 @@ async function resolveSpecs(loaded, filter) {
9027
9267
  parentAssetId: characterParent,
9028
9268
  parentSpec,
9029
9269
  parentFile,
9030
- parentSha256: (0, import_node_fs5.existsSync)(parentFile) ? await sha256File(parentFile) : null,
9270
+ parentSha256: (0, import_node_fs6.existsSync)(parentFile) ? await sha256File(parentFile) : null,
9031
9271
  // A state and its animations keep the base's own rotation count.
9032
9272
  directions: resolved.objectPro.kind === "base" ? resolved.objectPro.directions : parentSpec.objectPro.directions
9033
9273
  };
@@ -9049,7 +9289,7 @@ async function resolveSpecs(loaded, filter) {
9049
9289
  animation: sourceSpec.character.animation,
9050
9290
  outfit: {
9051
9291
  reference: {
9052
- path: import_node_path5.default.resolve(root, asset.outfit.reference),
9292
+ path: import_node_path6.default.resolve(root, asset.outfit.reference),
9053
9293
  sha256: referenceImage.hash,
9054
9294
  width: referenceImage.width,
9055
9295
  height: referenceImage.height,
@@ -9071,23 +9311,23 @@ async function resolveSpecs(loaded, filter) {
9071
9311
  );
9072
9312
  }
9073
9313
  const sourceSpec = await finalize(asset.revision.from);
9074
- const sourceStem = sourceSpec.quality?.outFile ?? (sourceSpec.source ? import_node_path5.default.resolve(root, sourceSpec.source) : sourceSpec.outFile);
9314
+ const sourceStem = sourceSpec.quality?.outFile ?? (sourceSpec.source ? import_node_path6.default.resolve(root, sourceSpec.source) : sourceSpec.outFile);
9075
9315
  const sourceMembers = await revisionSourceMembers(sourceStem, `revision source ${asset.revision.from}`);
9076
9316
  const sourceFile = sourceMembers?.[0]?.file ?? sourceStem;
9077
9317
  const sourceImage = sourceMembers ? { ...sourceMembers[0], hash: sha256(JSON.stringify(sourceMembers.map((member) => member.sha256))) } : await optionalRevisionImage(sourceFile, "revision source");
9078
9318
  const sourceFps = sourceSpec.character?.animation?.fps ?? sourceSpec.objectPro?.animation?.fps ?? sourceSpec.revision?.fps ?? sourceSpec.revision?.sourceFps;
9079
- const maskFile = asset.revision.mask ? import_node_path5.default.resolve(root, asset.revision.mask) : void 0;
9319
+ const maskFile = asset.revision.mask ? import_node_path6.default.resolve(root, asset.revision.mask) : void 0;
9080
9320
  const maskImage = maskFile ? await optionalRevisionImage(maskFile, "revision mask", "png") : null;
9081
9321
  if (sourceImage && maskImage && (sourceImage.width !== maskImage.width || sourceImage.height !== maskImage.height)) {
9082
9322
  throw new Error(
9083
9323
  `Revision mask for ${styleId}/${assetId} is ${maskImage.width}x${maskImage.height}; source ${asset.revision.from} is ${sourceImage.width}x${sourceImage.height}`
9084
9324
  );
9085
9325
  }
9086
- const paletteImageFile = asset.revision.paletteImage ? import_node_path5.default.resolve(root, asset.revision.paletteImage) : void 0;
9326
+ const paletteImageFile = asset.revision.paletteImage ? import_node_path6.default.resolve(root, asset.revision.paletteImage) : void 0;
9087
9327
  const paletteImage = paletteImageFile ? await optionalRevisionImage(paletteImageFile, "revision palette image", "png") : null;
9088
- const lastFrameFile = asset.revision.lastFrame ? import_node_path5.default.resolve(root, asset.revision.lastFrame) : void 0;
9328
+ const lastFrameFile = asset.revision.lastFrame ? import_node_path6.default.resolve(root, asset.revision.lastFrame) : void 0;
9089
9329
  const lastFrame = lastFrameFile ? await optionalRevisionImage(lastFrameFile, "revision last frame") : null;
9090
- const keypointsFile = asset.revision.keypointsFile ? import_node_path5.default.resolve(root, asset.revision.keypointsFile) : void 0;
9330
+ const keypointsFile = asset.revision.keypointsFile ? import_node_path6.default.resolve(root, asset.revision.keypointsFile) : void 0;
9091
9331
  const keypoints = keypointsFile ? await optionalSkeletonSet(keypointsFile) : null;
9092
9332
  resolved.revision = {
9093
9333
  mode: asset.revision.mode,
@@ -9141,6 +9381,10 @@ async function resolveSpecs(loaded, filter) {
9141
9381
  } : {},
9142
9382
  ...asset.revision.skeletonTemplate ? { skeletonTemplate: asset.revision.skeletonTemplate } : {},
9143
9383
  ...asset.revision.description ? { description: asset.revision.description } : {},
9384
+ ...asset.revision.removalTask ? { removalTask: asset.revision.removalTask } : {},
9385
+ ...asset.revision.driftThreshold == null ? {} : { driftThreshold: asset.revision.driftThreshold },
9386
+ ...asset.revision.subjectDescription ? { subjectDescription: asset.revision.subjectDescription } : {},
9387
+ ...asset.revision.initialPose ? { initialPose: asset.revision.initialPose } : {},
9144
9388
  ...asset.revision.engine ? { engine: asset.revision.engine } : {}
9145
9389
  };
9146
9390
  }
@@ -9250,7 +9494,7 @@ async function resolveSpecs(loaded, filter) {
9250
9494
  return specs;
9251
9495
  }
9252
9496
  function pngPath(file) {
9253
- const extension = import_node_path5.default.extname(file);
9497
+ const extension = import_node_path6.default.extname(file);
9254
9498
  return extension ? `${file.slice(0, -extension.length)}.png` : `${file}.png`;
9255
9499
  }
9256
9500
  async function resolveMapScene(root, styleId, assetId, generator, scene, provider) {
@@ -9260,12 +9504,12 @@ async function resolveMapScene(root, styleId, assetId, generator, scene, provide
9260
9504
  if (!provider.supportsMapScene) {
9261
9505
  throw new Error(`assets.${assetId}.scene: provider "${provider.id}" cannot draw into a scene; PixelLab's /map-objects can`);
9262
9506
  }
9263
- const file = import_node_path5.default.resolve(root, scene.image);
9507
+ const file = import_node_path6.default.resolve(root, scene.image);
9264
9508
  const image = await optionalRevisionImage(file, "scene image", "png");
9265
9509
  const base = { file, sha256: image?.hash ?? null, width: image?.width ?? null, height: image?.height ?? null };
9266
9510
  const placement = scene.placement;
9267
9511
  if ("mask" in placement) {
9268
- const maskFile = import_node_path5.default.resolve(root, placement.mask);
9512
+ const maskFile = import_node_path6.default.resolve(root, placement.mask);
9269
9513
  const mask = await optionalRevisionImage(maskFile, "scene mask", "png");
9270
9514
  if (image && mask && (image.width !== mask.width || image.height !== mask.height)) {
9271
9515
  throw new Error(
@@ -9280,7 +9524,7 @@ async function resolveMapScene(root, styleId, assetId, generator, scene, provide
9280
9524
  return "oval" in placement ? { ...base, placement: { type: "oval", fraction: placement.oval } } : { ...base, placement: { type: "rectangle", fraction: placement.rectangle } };
9281
9525
  }
9282
9526
  async function optionalRevisionImage(file, label, requiredFormat) {
9283
- if (!(0, import_node_fs5.existsSync)(file)) return null;
9527
+ if (!(0, import_node_fs6.existsSync)(file)) return null;
9284
9528
  const bytes = await (0, import_promises3.readFile)(file);
9285
9529
  const metadata = imageMetadata(bytes);
9286
9530
  if (!metadata) throw new Error(`${label} is not a readable PNG or JPEG: ${file}`);
@@ -9290,7 +9534,7 @@ async function optionalRevisionImage(file, label, requiredFormat) {
9290
9534
  return { hash: sha256(bytes), ...metadata };
9291
9535
  }
9292
9536
  async function optionalSkeletonSet(file) {
9293
- if (!(0, import_node_fs5.existsSync)(file)) return null;
9537
+ if (!(0, import_node_fs6.existsSync)(file)) return null;
9294
9538
  const bytes = await (0, import_promises3.readFile)(file);
9295
9539
  let json;
9296
9540
  try {
@@ -9301,17 +9545,17 @@ async function optionalSkeletonSet(file) {
9301
9545
  return { hash: sha256(bytes), skeleton: parseSkeletonSet(json, file) };
9302
9546
  }
9303
9547
  async function revisionSourceMembers(stem, label) {
9304
- if ((0, import_node_fs5.existsSync)(stem)) return null;
9548
+ if ((0, import_node_fs6.existsSync)(stem)) return null;
9305
9549
  const at = (role) => memberPath(stem, role, 0, 2, MediaType.PNG);
9306
9550
  let roles = [];
9307
9551
  for (const set of [CHARACTER_DIRECTIONS_8, CHARACTER_DIRECTIONS_4]) {
9308
- if (set.every((direction) => (0, import_node_fs5.existsSync)(at(direction)))) {
9552
+ if (set.every((direction) => (0, import_node_fs6.existsSync)(at(direction)))) {
9309
9553
  roles = [...set];
9310
9554
  break;
9311
9555
  }
9312
9556
  }
9313
9557
  if (!roles.length) {
9314
- for (let index = 0; (0, import_node_fs5.existsSync)(at(`frame-${String(index).padStart(2, "0")}`)); index++) {
9558
+ for (let index = 0; (0, import_node_fs6.existsSync)(at(`frame-${String(index).padStart(2, "0")}`)); index++) {
9315
9559
  roles.push(`frame-${String(index).padStart(2, "0")}`);
9316
9560
  }
9317
9561
  }
@@ -9337,7 +9581,7 @@ async function resolveStyleImages(loaded, styleId) {
9337
9581
  if (!style) throw new Error(`Unknown style "${styleId}"`);
9338
9582
  const out = [];
9339
9583
  for (const img of style.styleImages) {
9340
- const file = import_node_path5.default.resolve(loaded.root, img.path);
9584
+ const file = import_node_path6.default.resolve(loaded.root, img.path);
9341
9585
  const buf = await (0, import_promises3.readFile)(file);
9342
9586
  const metadata = imageMetadata(buf);
9343
9587
  if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${file}`);
@@ -9368,7 +9612,7 @@ async function resolveCharacterShape(asset, style, kind, files) {
9368
9612
  };
9369
9613
  if (asset.concept) {
9370
9614
  const image = await files.load(asset.concept, "Concept image");
9371
- shape.concept = { path: import_node_path5.default.resolve(files.root, asset.concept), sha256: image.hash, width: image.width, height: image.height, format: image.format };
9615
+ shape.concept = { path: import_node_path6.default.resolve(files.root, asset.concept), sha256: image.hash, width: image.width, height: image.height, format: image.format };
9372
9616
  }
9373
9617
  if (asset.reference) {
9374
9618
  const byDirection = typeof asset.reference === "string" ? { south: asset.reference } : asset.reference;
@@ -9376,7 +9620,7 @@ async function resolveCharacterShape(asset, style, kind, files) {
9376
9620
  for (const [direction, rel] of Object.entries(byDirection)) {
9377
9621
  const image = await files.load(rel, `Reference sprite (${direction})`);
9378
9622
  shape.reference[direction] = {
9379
- path: import_node_path5.default.resolve(files.root, rel),
9623
+ path: import_node_path6.default.resolve(files.root, rel),
9380
9624
  sha256: image.hash,
9381
9625
  width: image.width,
9382
9626
  height: image.height,
@@ -9397,7 +9641,7 @@ async function resolveCharacterShape(asset, style, kind, files) {
9397
9641
  const animationMode = asset.animation.mode ?? (asset.animation.template ? "template" : "v3");
9398
9642
  const pose = async (rel, what) => {
9399
9643
  const image = await files.load(rel, what);
9400
- return { path: import_node_path5.default.resolve(files.root, rel), sha256: image.hash, width: image.width, height: image.height, format: image.format };
9644
+ return { path: import_node_path6.default.resolve(files.root, rel), sha256: image.hash, width: image.width, height: image.height, format: image.format };
9401
9645
  };
9402
9646
  shape.animation = {
9403
9647
  mode: animationMode,
@@ -9428,7 +9672,7 @@ async function resolveObjectProShape(assetId, asset, style, kind, files) {
9428
9672
  const south = typeof asset.reference === "string" ? asset.reference : asset.reference.south;
9429
9673
  if (south) {
9430
9674
  const image = await files.load(south, "Reference sprite");
9431
- shape.reference = { path: import_node_path5.default.resolve(files.root, south), sha256: image.hash, width: image.width, height: image.height, format: image.format };
9675
+ shape.reference = { path: import_node_path6.default.resolve(files.root, south), sha256: image.hash, width: image.width, height: image.height, format: image.format };
9432
9676
  }
9433
9677
  }
9434
9678
  if (asset.state) {
@@ -9456,7 +9700,7 @@ async function resolveObjectProShape(assetId, asset, style, kind, files) {
9456
9700
  }
9457
9701
  const pose = async (rel, what) => {
9458
9702
  const image = await files.load(rel, what);
9459
- return { path: import_node_path5.default.resolve(files.root, rel), sha256: image.hash, width: image.width, height: image.height, format: image.format };
9703
+ return { path: import_node_path6.default.resolve(files.root, rel), sha256: image.hash, width: image.width, height: image.height, format: image.format };
9460
9704
  };
9461
9705
  shape.animation = {
9462
9706
  mode: asset.animation.mode ?? "v3",
@@ -9476,11 +9720,11 @@ function characterDirections(directions) {
9476
9720
  }
9477
9721
 
9478
9722
  // src/project.ts
9479
- var import_node_path13 = __toESM(require("path"), 1);
9723
+ var import_node_path14 = __toESM(require("path"), 1);
9480
9724
 
9481
9725
  // src/env.ts
9482
- var import_node_fs6 = require("fs");
9483
- var import_node_path6 = __toESM(require("path"), 1);
9726
+ var import_node_fs7 = require("fs");
9727
+ var import_node_path7 = __toESM(require("path"), 1);
9484
9728
  function loadEnvFiles(dir) {
9485
9729
  const loaded = [];
9486
9730
  for (const [file, vars] of envFiles(dir)) {
@@ -9494,10 +9738,10 @@ function loadEnvFiles(dir) {
9494
9738
  function envFiles(dir) {
9495
9739
  const out = [];
9496
9740
  for (const name of [".env.local", ".env"]) {
9497
- const file = import_node_path6.default.join(dir, name);
9498
- if (!(0, import_node_fs6.existsSync)(file)) continue;
9741
+ const file = import_node_path7.default.join(dir, name);
9742
+ if (!(0, import_node_fs7.existsSync)(file)) continue;
9499
9743
  try {
9500
- out.push([file, parseEnv((0, import_node_fs6.readFileSync)(file, "utf8"))]);
9744
+ out.push([file, parseEnv((0, import_node_fs7.readFileSync)(file, "utf8"))]);
9501
9745
  } catch {
9502
9746
  }
9503
9747
  }
@@ -9523,12 +9767,12 @@ function parseEnv(contents) {
9523
9767
  }
9524
9768
 
9525
9769
  // src/pipeline/plan.ts
9526
- var import_node_fs9 = require("fs");
9527
- var import_node_path12 = __toESM(require("path"), 1);
9770
+ var import_node_fs10 = require("fs");
9771
+ var import_node_path13 = __toESM(require("path"), 1);
9528
9772
 
9529
9773
  // src/pipeline/mirror.ts
9530
9774
  var import_promises4 = require("fs/promises");
9531
- var import_node_path7 = __toESM(require("path"), 1);
9775
+ var import_node_path8 = __toESM(require("path"), 1);
9532
9776
 
9533
9777
  // src/palette.ts
9534
9778
  function normalizePalette(values) {
@@ -9667,7 +9911,7 @@ async function deriveMirror(spec, lock, lockPath, opts = {}) {
9667
9911
  for (const [index, member] of members.entries()) {
9668
9912
  const bytes = flipHorizontal(await (0, import_promises4.readFile)(member.file));
9669
9913
  const target = expectedOutputPath(spec, member.role, index, members.length, MediaType.PNG);
9670
- await (0, import_promises4.mkdir)(import_node_path7.default.dirname(target), { recursive: true });
9914
+ await (0, import_promises4.mkdir)(import_node_path8.default.dirname(target), { recursive: true });
9671
9915
  const tmp = `${target}.pixelkiln.tmp`;
9672
9916
  await (0, import_promises4.writeFile)(tmp, bytes);
9673
9917
  await (0, import_promises4.rename)(tmp, target);
@@ -9729,29 +9973,29 @@ async function deriveMirror(spec, lock, lockPath, opts = {}) {
9729
9973
  }
9730
9974
 
9731
9975
  // src/pipeline/quality-profile.ts
9732
- var import_node_fs7 = require("fs");
9733
- var import_node_path10 = __toESM(require("path"), 1);
9976
+ var import_node_fs8 = require("fs");
9977
+ var import_node_path11 = __toESM(require("path"), 1);
9734
9978
 
9735
9979
  // src/pipeline/refine.ts
9736
- var import_node_path9 = __toESM(require("path"), 1);
9980
+ var import_node_path10 = __toESM(require("path"), 1);
9737
9981
  var import_node_child_process2 = require("child_process");
9738
9982
  var import_promises6 = require("fs/promises");
9739
- var import_node_os2 = require("os");
9983
+ var import_node_os3 = require("os");
9740
9984
  var import_node_util = require("util");
9741
9985
 
9742
9986
  // src/artifacts.ts
9743
- var import_node_path8 = __toESM(require("path"), 1);
9744
- var import_node_crypto5 = require("crypto");
9987
+ var import_node_path9 = __toESM(require("path"), 1);
9988
+ var import_node_crypto6 = require("crypto");
9745
9989
  var import_promises5 = require("fs/promises");
9746
9990
  var activeTransactions = /* @__PURE__ */ new Set();
9747
9991
  function message(error) {
9748
9992
  return error instanceof Error ? error.message : String(error);
9749
9993
  }
9750
9994
  function digest(data) {
9751
- return (0, import_node_crypto5.createHash)("sha256").update(data).digest("hex");
9995
+ return (0, import_node_crypto6.createHash)("sha256").update(data).digest("hex");
9752
9996
  }
9753
9997
  function portableRelative(from, to) {
9754
- return import_node_path8.default.relative(from, import_node_path8.default.resolve(to)).split(import_node_path8.default.sep).join("/") || ".";
9998
+ return import_node_path9.default.relative(from, import_node_path9.default.resolve(to)).split(import_node_path9.default.sep).join("/") || ".";
9755
9999
  }
9756
10000
  function canonical2(value) {
9757
10001
  if (value === null || typeof value === "string" || typeof value === "boolean") return value;
@@ -9801,7 +10045,7 @@ function parseArtifactManifest(absolute, data) {
9801
10045
  return raw;
9802
10046
  }
9803
10047
  async function readArtifactBundleManifest(manifestPath) {
9804
- const absolute = import_node_path8.default.resolve(manifestPath);
10048
+ const absolute = import_node_path9.default.resolve(manifestPath);
9805
10049
  return parseArtifactManifest(absolute, await (0, import_promises5.readFile)(absolute, "utf8"));
9806
10050
  }
9807
10051
  async function readOptional(file) {
@@ -9825,8 +10069,8 @@ function processIsAlive(pid) {
9825
10069
  }
9826
10070
  }
9827
10071
  function validTemporaryPath(candidate, destination, type) {
9828
- return import_node_path8.default.dirname(candidate) === import_node_path8.default.dirname(destination) && import_node_path8.default.basename(candidate).startsWith(
9829
- `.${import_node_path8.default.basename(destination)}.pixelkiln-${type}-`
10072
+ return import_node_path9.default.dirname(candidate) === import_node_path9.default.dirname(destination) && import_node_path9.default.basename(candidate).startsWith(
10073
+ `.${import_node_path9.default.basename(destination)}.pixelkiln-${type}-`
9830
10074
  );
9831
10075
  }
9832
10076
  function parseTransaction(journal, data) {
@@ -9846,7 +10090,7 @@ async function removeTransactionFiles(journal) {
9846
10090
  await (0, import_promises5.rm)(transactionMarker(journal), { force: true });
9847
10091
  }
9848
10092
  async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
9849
- const journal = import_node_path8.default.resolve(recoveryFile);
10093
+ const journal = import_node_path9.default.resolve(recoveryFile);
9850
10094
  const bytes = await readOptional(journal);
9851
10095
  if (!bytes) {
9852
10096
  await (0, import_promises5.rm)(transactionMarker(journal), { force: true });
@@ -9862,7 +10106,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
9862
10106
  );
9863
10107
  }
9864
10108
  for (const entry of transaction.entries) {
9865
- if (typeof entry.destination !== "string" || typeof entry.stage !== "string" || typeof entry.sha256 !== "string" || entry.backup !== void 0 && typeof entry.backup !== "string" || !allowedDestinations.has(import_node_path8.default.resolve(entry.destination)) || !validTemporaryPath(entry.stage, entry.destination, "stage") || entry.backup !== void 0 && !validTemporaryPath(entry.backup, entry.destination, "backup")) {
10109
+ if (typeof entry.destination !== "string" || typeof entry.stage !== "string" || typeof entry.sha256 !== "string" || entry.backup !== void 0 && typeof entry.backup !== "string" || !allowedDestinations.has(import_node_path9.default.resolve(entry.destination)) || !validTemporaryPath(entry.stage, entry.destination, "stage") || entry.backup !== void 0 && !validTemporaryPath(entry.backup, entry.destination, "backup")) {
9866
10110
  throw new Error(
9867
10111
  `Refusing unsafe artifact recovery from ${journal}; its destinations or temporary paths do not match the current bundle.`
9868
10112
  );
@@ -9908,7 +10152,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
9908
10152
  await removeTransactionFiles(journal);
9909
10153
  }
9910
10154
  function createArtifactBundleManifest(manifestPath, outputs, provenance) {
9911
- const root = import_node_path8.default.dirname(import_node_path8.default.resolve(manifestPath));
10155
+ const root = import_node_path9.default.dirname(import_node_path9.default.resolve(manifestPath));
9912
10156
  const sources = provenance.sources.map((source) => ({
9913
10157
  ...source,
9914
10158
  path: portableRelative(root, source.path)
@@ -9938,11 +10182,11 @@ function withArtifactManifest(manifestPath, outputs, provenance) {
9938
10182
  ];
9939
10183
  }
9940
10184
  async function writeManagedArtifactBundle(manifestPath, outputs, provenance, options = {}) {
9941
- const absoluteManifest = import_node_path8.default.resolve(manifestPath);
10185
+ const absoluteManifest = import_node_path9.default.resolve(manifestPath);
9942
10186
  const recoveryFile = `${absoluteManifest}.transaction`;
9943
10187
  const allowedDestinations = /* @__PURE__ */ new Set([
9944
10188
  absoluteManifest,
9945
- ...outputs.map((output) => import_node_path8.default.resolve(output.path))
10189
+ ...outputs.map((output) => import_node_path9.default.resolve(output.path))
9946
10190
  ]);
9947
10191
  await recoverArtifactTransaction(recoveryFile, allowedDestinations);
9948
10192
  const existingManifestBytes = await readOptional(absoluteManifest);
@@ -9960,13 +10204,13 @@ async function writeManagedArtifactBundle(manifestPath, outputs, provenance, opt
9960
10204
  }
9961
10205
  }
9962
10206
  if (!options.force) {
9963
- const root = import_node_path8.default.dirname(absoluteManifest);
10207
+ const root = import_node_path9.default.dirname(absoluteManifest);
9964
10208
  const recorded = new Map(
9965
- previous?.outputs.map((output) => [import_node_path8.default.resolve(root, output.path), output.sha256]) ?? []
10209
+ previous?.outputs.map((output) => [import_node_path9.default.resolve(root, output.path), output.sha256]) ?? []
9966
10210
  );
9967
10211
  const conflicts = [];
9968
10212
  for (const output of outputs) {
9969
- const destination = import_node_path8.default.resolve(output.path);
10213
+ const destination = import_node_path9.default.resolve(output.path);
9970
10214
  const current = await readOptional(destination);
9971
10215
  if (!current || digest(current) === digest(output.data)) continue;
9972
10216
  const expected = recorded.get(destination);
@@ -9988,15 +10232,15 @@ Pass { force: true } (CLI: --force) to take ownership and replace the complete b
9988
10232
  );
9989
10233
  }
9990
10234
  async function verifyArtifactBundle(manifestPath) {
9991
- const absolute = import_node_path8.default.resolve(manifestPath);
10235
+ const absolute = import_node_path9.default.resolve(manifestPath);
9992
10236
  const raw = await readArtifactBundleManifest(absolute);
9993
- const root = import_node_path8.default.dirname(absolute);
10237
+ const root = import_node_path9.default.dirname(absolute);
9994
10238
  const changedSources = [];
9995
10239
  const changedOutputs = [];
9996
10240
  for (const source of raw.sources) {
9997
10241
  let actual = null;
9998
10242
  try {
9999
- actual = digest(await (0, import_promises5.readFile)(import_node_path8.default.resolve(root, source.path)));
10243
+ actual = digest(await (0, import_promises5.readFile)(import_node_path9.default.resolve(root, source.path)));
10000
10244
  } catch (error) {
10001
10245
  if (!isCode(error, "ENOENT")) throw error;
10002
10246
  }
@@ -10004,7 +10248,7 @@ async function verifyArtifactBundle(manifestPath) {
10004
10248
  }
10005
10249
  for (const output of raw.outputs) {
10006
10250
  try {
10007
- if (digest(await (0, import_promises5.readFile)(import_node_path8.default.resolve(root, output.path))) !== output.sha256) {
10251
+ if (digest(await (0, import_promises5.readFile)(import_node_path9.default.resolve(root, output.path))) !== output.sha256) {
10008
10252
  changedOutputs.push(output.path);
10009
10253
  }
10010
10254
  } catch (error) {
@@ -10060,7 +10304,7 @@ async function writeArtifactBundle(files, options = {}) {
10060
10304
  if (!files.length) throw new Error("An artifact bundle must contain at least one file.");
10061
10305
  const normalized = files.map((file) => {
10062
10306
  if (!file.path.trim()) throw new Error("Artifact paths cannot be empty.");
10063
- return { destination: import_node_path8.default.resolve(file.path), data: Buffer.from(file.data) };
10307
+ return { destination: import_node_path9.default.resolve(file.path), data: Buffer.from(file.data) };
10064
10308
  });
10065
10309
  const destinations = /* @__PURE__ */ new Set();
10066
10310
  for (const artifact of normalized) {
@@ -10088,23 +10332,23 @@ async function writeArtifactBundle(files, options = {}) {
10088
10332
  }
10089
10333
  }
10090
10334
  if (!changed.length) return { changed: [], unchanged };
10091
- const token = (0, import_node_crypto5.randomUUID)();
10335
+ const token = (0, import_node_crypto6.randomUUID)();
10092
10336
  for (const [index, artifact] of changed.entries()) {
10093
- const basename = import_node_path8.default.basename(artifact.destination);
10094
- artifact.stage = import_node_path8.default.join(
10095
- import_node_path8.default.dirname(artifact.destination),
10337
+ const basename = import_node_path9.default.basename(artifact.destination);
10338
+ artifact.stage = import_node_path9.default.join(
10339
+ import_node_path9.default.dirname(artifact.destination),
10096
10340
  `.${basename}.pixelkiln-stage-${token}-${index}`
10097
10341
  );
10098
10342
  if (artifact.existed) {
10099
- artifact.backup = import_node_path8.default.join(
10100
- import_node_path8.default.dirname(artifact.destination),
10343
+ artifact.backup = import_node_path9.default.join(
10344
+ import_node_path9.default.dirname(artifact.destination),
10101
10345
  `.${basename}.pixelkiln-backup-${token}-${index}`
10102
10346
  );
10103
10347
  }
10104
10348
  }
10105
- const journal = options.recoveryFile ? import_node_path8.default.resolve(options.recoveryFile) : null;
10349
+ const journal = options.recoveryFile ? import_node_path9.default.resolve(options.recoveryFile) : null;
10106
10350
  if (journal) {
10107
- await (0, import_promises5.mkdir)(import_node_path8.default.dirname(journal), { recursive: true });
10351
+ await (0, import_promises5.mkdir)(import_node_path9.default.dirname(journal), { recursive: true });
10108
10352
  const transaction = {
10109
10353
  format: "pixelkiln-artifact-transaction",
10110
10354
  version: 1,
@@ -10129,7 +10373,7 @@ async function writeArtifactBundle(files, options = {}) {
10129
10373
  }
10130
10374
  try {
10131
10375
  for (const [index, artifact] of changed.entries()) {
10132
- await (0, import_promises5.mkdir)(import_node_path8.default.dirname(artifact.destination), { recursive: true });
10376
+ await (0, import_promises5.mkdir)(import_node_path9.default.dirname(artifact.destination), { recursive: true });
10133
10377
  await options.beforeStage?.(artifact.destination, index);
10134
10378
  await (0, import_promises5.writeFile)(artifact.stage, artifact.data, { flag: "wx" });
10135
10379
  }
@@ -10395,10 +10639,10 @@ function sameFrameGrid(first, next) {
10395
10639
  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;
10396
10640
  }
10397
10641
  async function refineAsset(options) {
10398
- const source = import_node_path9.default.resolve(options.source);
10399
- const output = import_node_path9.default.resolve(options.output);
10642
+ const source = import_node_path10.default.resolve(options.source);
10643
+ const output = import_node_path10.default.resolve(options.output);
10400
10644
  if (source === output) throw new Error("Refinement source and output must be different files.");
10401
- const record = import_node_path9.default.resolve(qualityRecordPath(output));
10645
+ const record = import_node_path10.default.resolve(qualityRecordPath(output));
10402
10646
  const palette = normalizePalette(options.palette).map((color) => color.hex);
10403
10647
  const minGridConfidence = requireConfidence(options.minGridConfidence ?? "high");
10404
10648
  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.");
@@ -10421,8 +10665,8 @@ async function refineAsset(options) {
10421
10665
  );
10422
10666
  }
10423
10667
  }
10424
- const temp = await (0, import_promises6.mkdtemp)(import_node_path9.default.join((0, import_node_os2.tmpdir)(), "pixelkiln-refine-"));
10425
- const recovered = import_node_path9.default.join(temp, "native.png");
10668
+ const temp = await (0, import_promises6.mkdtemp)(import_node_path10.default.join((0, import_node_os3.tmpdir)(), "pixelkiln-refine-"));
10669
+ const recovered = import_node_path10.default.join(temp, "native.png");
10426
10670
  const fixerCommand = compatibilityCommand ?? options.fixerPython ?? process.env.PIXELKILN_PIXEL_FIXER_PYTHON ?? "python3";
10427
10671
  const args = compatibilityCommand ? [...options.fixerArgsPrefix ?? [], source, "--extract", recovered] : ["-c", PYTHON_FIXER_BRIDGE, source, recovered];
10428
10672
  try {
@@ -10507,18 +10751,18 @@ async function refineFrameSet(options) {
10507
10751
  if (roles.some((role) => !role.trim()) || new Set(roles).size !== roles.length) {
10508
10752
  throw new Error("Frame-set roles must be non-empty and unique.");
10509
10753
  }
10510
- const output = import_node_path9.default.resolve(options.output);
10511
- const record = import_node_path9.default.resolve(qualityRecordPath(output));
10754
+ const output = import_node_path10.default.resolve(options.output);
10755
+ const record = import_node_path10.default.resolve(qualityRecordPath(output));
10512
10756
  const palette = normalizeRefinementPalette(options.palette);
10513
10757
  const finalOutputs = options.sources.map((source, index) => qualityFrameOutputPath(output, source.role, index));
10514
10758
  if (new Set(finalOutputs).size !== finalOutputs.length) {
10515
10759
  throw new Error("Frame-set roles resolve to duplicate output filenames.");
10516
10760
  }
10517
- const sourceSet = new Set(options.sources.map((source) => import_node_path9.default.resolve(source.path)));
10761
+ const sourceSet = new Set(options.sources.map((source) => import_node_path10.default.resolve(source.path)));
10518
10762
  if (finalOutputs.some((file) => sourceSet.has(file))) {
10519
10763
  throw new Error("Frame-set refinement sources and outputs must be different files.");
10520
10764
  }
10521
- const temp = await (0, import_promises6.mkdtemp)(import_node_path9.default.join((0, import_node_os2.tmpdir)(), "pixelkiln-refine-frames-"));
10765
+ const temp = await (0, import_promises6.mkdtemp)(import_node_path10.default.join((0, import_node_os3.tmpdir)(), "pixelkiln-refine-frames-"));
10522
10766
  try {
10523
10767
  const frames = [];
10524
10768
  const outputs = [];
@@ -10526,8 +10770,8 @@ async function refineFrameSet(options) {
10526
10770
  const sourcePaths = [];
10527
10771
  let firstOptions = null;
10528
10772
  for (const [index, sourceEntry] of options.sources.entries()) {
10529
- const source = import_node_path9.default.resolve(sourceEntry.path);
10530
- const temporaryOutput = import_node_path9.default.join(temp, `${String(index).padStart(2, "0")}.png`);
10773
+ const source = import_node_path10.default.resolve(sourceEntry.path);
10774
+ const temporaryOutput = import_node_path10.default.join(temp, `${String(index).padStart(2, "0")}.png`);
10531
10775
  const refined = await refineAsset({
10532
10776
  source,
10533
10777
  output: temporaryOutput,
@@ -10602,7 +10846,7 @@ async function refineFrameSet(options) {
10602
10846
  );
10603
10847
  return {
10604
10848
  sources: sourcePaths,
10605
- outputs: outputs.map((item) => import_node_path9.default.resolve(item.path)),
10849
+ outputs: outputs.map((item) => import_node_path10.default.resolve(item.path)),
10606
10850
  record,
10607
10851
  frames,
10608
10852
  palette,
@@ -10613,12 +10857,12 @@ async function refineFrameSet(options) {
10613
10857
  }
10614
10858
  }
10615
10859
  async function checkQualityRecord(recordPath) {
10616
- const record = import_node_path9.default.resolve(recordPath);
10860
+ const record = import_node_path10.default.resolve(recordPath);
10617
10861
  const manifest = await readArtifactBundleManifest(record);
10618
10862
  const options = parseRecordOptions(manifest, record);
10619
10863
  const verification = await verifyArtifactBundle(record);
10620
- const sources = manifest.sources.map((source2) => import_node_path9.default.resolve(import_node_path9.default.dirname(record), source2.path));
10621
- const outputs = manifest.outputs.map((output2) => import_node_path9.default.resolve(import_node_path9.default.dirname(record), output2.path));
10864
+ const sources = manifest.sources.map((source2) => import_node_path10.default.resolve(import_node_path10.default.dirname(record), source2.path));
10865
+ const outputs = manifest.outputs.map((output2) => import_node_path10.default.resolve(import_node_path10.default.dirname(record), output2.path));
10622
10866
  const source = sources[0] ?? "";
10623
10867
  const output = outputs[0] ?? "";
10624
10868
  const expectedCount = options.frameSet?.count ?? 1;
@@ -10651,7 +10895,7 @@ async function checkQualityRecord(recordPath) {
10651
10895
  async function approveQualityRecord(recordPath, approval) {
10652
10896
  const reviewer = approval.reviewer.trim();
10653
10897
  if (!reviewer) throw new Error("A human reviewer name is required.");
10654
- const record = import_node_path9.default.resolve(recordPath);
10898
+ const record = import_node_path10.default.resolve(recordPath);
10655
10899
  const manifest = await readArtifactBundleManifest(record);
10656
10900
  const options = parseRecordOptions(manifest, record);
10657
10901
  const verification = await verifyArtifactBundle(record);
@@ -10672,14 +10916,14 @@ async function approveQualityRecord(recordPath, approval) {
10672
10916
  `A refinement record must contain exactly ${expectedCount} source(s) and output(s).`
10673
10917
  );
10674
10918
  }
10675
- const root = import_node_path9.default.dirname(record);
10919
+ const root = import_node_path10.default.dirname(record);
10676
10920
  const outputs = await Promise.all(manifest.outputs.map(async (entry) => {
10677
- const outputPath = import_node_path9.default.resolve(root, entry.path);
10921
+ const outputPath = import_node_path10.default.resolve(root, entry.path);
10678
10922
  return { path: outputPath, data: await (0, import_promises6.readFile)(outputPath) };
10679
10923
  }));
10680
10924
  const sources = manifest.sources.map((source) => ({
10681
10925
  ...source,
10682
- path: import_node_path9.default.resolve(root, source.path)
10926
+ path: import_node_path10.default.resolve(root, source.path)
10683
10927
  }));
10684
10928
  const approved = {
10685
10929
  ...options,
@@ -10711,8 +10955,8 @@ async function qualitySource(spec, lock) {
10711
10955
  if (spec.generator === "frames") {
10712
10956
  return { items: null, reason: "frame-set quality requires ordered downloaded provider outputs" };
10713
10957
  }
10714
- const source = import_node_path10.default.resolve(spec.root, spec.source);
10715
- return (0, import_node_fs7.existsSync)(source) ? { items: [{ role: "source", path: source }] } : { items: null, reason: `declared source is missing: ${spec.source}` };
10958
+ const source = import_node_path11.default.resolve(spec.root, spec.source);
10959
+ return (0, import_node_fs8.existsSync)(source) ? { items: [{ role: "source", path: source }] } : { items: null, reason: `declared source is missing: ${spec.source}` };
10716
10960
  }
10717
10961
  const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
10718
10962
  if (!entry) return { items: null, reason: "raw provider output is not in the lockfile" };
@@ -10741,7 +10985,7 @@ async function qualitySource(spec, lock) {
10741
10985
  }
10742
10986
  if (role) roles.add(role);
10743
10987
  const source = currentEntryOutputPath(entry, spec, index);
10744
- if (!(0, import_node_fs7.existsSync)(source)) return { items: null, reason: `raw provider output is missing: ${source}` };
10988
+ if (!(0, import_node_fs8.existsSync)(source)) return { items: null, reason: `raw provider output is missing: ${source}` };
10745
10989
  if (await sha256File(source) !== output.sha256) {
10746
10990
  return { items: null, reason: "raw provider output was modified after download" };
10747
10991
  }
@@ -10760,7 +11004,7 @@ async function inspectQualityProfile(spec, lock) {
10760
11004
  }
10761
11005
  const sourcePaths = source.items.map((item) => item.path);
10762
11006
  const expectedOutputs = spec.generator === "frames" ? source.items.map((item, index) => qualityFrameOutputPath(output, item.role, index)) : [output];
10763
- if (!(0, import_node_fs7.existsSync)(record)) {
11007
+ if (!(0, import_node_fs8.existsSync)(record)) {
10764
11008
  return {
10765
11009
  key,
10766
11010
  state: "needs-refinement",
@@ -10778,7 +11022,7 @@ async function inspectQualityProfile(spec, lock) {
10778
11022
  const expectedRevision = spec.quality.fixerRevision ?? PIXEL_ART_FIXER_REVISION;
10779
11023
  const expectedTransparency = spec.quality.minTransparency ?? null;
10780
11024
  const expectedFrameSet = spec.generator === "frames" ? { fps: spec.quality.fps ?? 12, count: source.items.length, roles: source.items.map((item) => item.role) } : null;
10781
- const contractChanged = JSON.stringify(check.sources.map((item) => import_node_path10.default.resolve(item))) !== JSON.stringify(sourcePaths.map((item) => import_node_path10.default.resolve(item))) || JSON.stringify(check.outputs.map((item) => import_node_path10.default.resolve(item))) !== JSON.stringify(expectedOutputs.map((item) => import_node_path10.default.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);
11025
+ const contractChanged = JSON.stringify(check.sources.map((item) => import_node_path11.default.resolve(item))) !== JSON.stringify(sourcePaths.map((item) => import_node_path11.default.resolve(item))) || JSON.stringify(check.outputs.map((item) => import_node_path11.default.resolve(item))) !== JSON.stringify(expectedOutputs.map((item) => import_node_path11.default.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);
10782
11026
  if (contractChanged) {
10783
11027
  return {
10784
11028
  key,
@@ -10941,8 +11185,8 @@ async function requireApprovedQualitySources(specs, lock, assetIds, outputRoles)
10941
11185
  }
10942
11186
 
10943
11187
  // src/pipeline/revision.ts
10944
- var import_node_fs8 = require("fs");
10945
- var import_node_path11 = __toESM(require("path"), 1);
11188
+ var import_node_fs9 = require("fs");
11189
+ var import_node_path12 = __toESM(require("path"), 1);
10946
11190
  async function inspectRevisionReadiness(spec, lock) {
10947
11191
  if (spec.mirror) return inspectMirror(spec, lock, /* @__PURE__ */ new Set());
10948
11192
  if (spec.character?.kind === "outfit") return inspectOutfit(spec, lock, /* @__PURE__ */ new Set());
@@ -10958,7 +11202,7 @@ async function inspectStyleAnchor(spec, lock, seen) {
10958
11202
  const label = `style anchor ${spec.styleId}/${anchor.assetId}`;
10959
11203
  const dependency = await inspectParent(anchor.spec, lock, seen);
10960
11204
  if (!dependency.ready) return { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
10961
- if (!anchor.sha256 || !(0, import_node_fs8.existsSync)(anchor.file)) return { ready: false, reason: `${label} has no generated south-facing file yet` };
11205
+ if (!anchor.sha256 || !(0, import_node_fs9.existsSync)(anchor.file)) return { ready: false, reason: `${label} has no generated south-facing file yet` };
10962
11206
  if (await sha256File(anchor.file) !== anchor.sha256) {
10963
11207
  return { ready: false, reason: `${label} changed after the manifest was resolved` };
10964
11208
  }
@@ -10972,7 +11216,7 @@ async function inspectScene(spec) {
10972
11216
  const files = [{ label: "scene image", file: scene.file, sha256: scene.sha256 }];
10973
11217
  if (scene.placement.type === "mask") files.push({ label: "scene mask", file: scene.placement.file, sha256: scene.placement.sha256 });
10974
11218
  for (const { label, file, sha256: sha2562 } of files) {
10975
- if (!sha2562 || !(0, import_node_fs8.existsSync)(file)) return { ready: false, reason: `${label} does not exist yet: ${file}` };
11219
+ if (!sha2562 || !(0, import_node_fs9.existsSync)(file)) return { ready: false, reason: `${label} does not exist yet: ${file}` };
10976
11220
  if (await sha256File(file) !== sha2562) return { ready: false, reason: `${label} changed after the manifest was resolved` };
10977
11221
  }
10978
11222
  return { ready: true, reason: "scene is current" };
@@ -10989,7 +11233,7 @@ async function inspectOutfit(spec, lock, seen) {
10989
11233
  const label = `outfit source ${spec.styleId}/${character.parentAssetId}`;
10990
11234
  const dependency = await inspectParent(character.parentSpec, lock, seen);
10991
11235
  if (!dependency.ready) return { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
10992
- if (!(0, import_node_fs8.existsSync)(outfit.reference.path)) {
11236
+ if (!(0, import_node_fs9.existsSync)(outfit.reference.path)) {
10993
11237
  return { ready: false, reason: `outfit reference is missing: ${outfit.reference.path}` };
10994
11238
  }
10995
11239
  if (await sha256File(outfit.reference.path) !== outfit.reference.sha256) {
@@ -11003,7 +11247,7 @@ async function inspectCharacterParent(spec, lock, seen) {
11003
11247
  const label = `${character.kind} parent ${spec.styleId}/${character.parentAssetId}`;
11004
11248
  const dependency = await inspectParent(parentSpec, lock, seen);
11005
11249
  if (!dependency.ready) return { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
11006
- if (!character.parentSha256 || !character.parentFile || !(0, import_node_fs8.existsSync)(character.parentFile)) {
11250
+ if (!character.parentSha256 || !character.parentFile || !(0, import_node_fs9.existsSync)(character.parentFile)) {
11007
11251
  return { ready: false, reason: `${label} has no generated south-facing file yet` };
11008
11252
  }
11009
11253
  if (await sha256File(character.parentFile) !== character.parentSha256) {
@@ -11021,7 +11265,7 @@ async function inspectObjectProParent(spec, lock, seen) {
11021
11265
  const label = `${object.kind} parent ${spec.styleId}/${object.parentAssetId}`;
11022
11266
  const dependency = await inspectParent(parentSpec, lock, seen);
11023
11267
  if (!dependency.ready) return { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
11024
- if (!object.parentSha256 || !object.parentFile || !(0, import_node_fs8.existsSync)(object.parentFile)) {
11268
+ if (!object.parentSha256 || !object.parentFile || !(0, import_node_fs9.existsSync)(object.parentFile)) {
11025
11269
  return { ready: false, reason: `${label} has no generated south-facing file yet` };
11026
11270
  }
11027
11271
  if (await sha256File(object.parentFile) !== object.parentSha256) {
@@ -11047,7 +11291,7 @@ async function inspectRevision(spec, lock, seen) {
11047
11291
  const members = await inspectSourceMembers(spec, lock);
11048
11292
  if (!members.ready) return members;
11049
11293
  } else {
11050
- if (!revision.sourceSha256 || !(0, import_node_fs8.existsSync)(revision.sourceFile)) {
11294
+ if (!revision.sourceSha256 || !(0, import_node_fs9.existsSync)(revision.sourceFile)) {
11051
11295
  return { ready: false, reason: `revision source is missing: ${revision.sourceFile}` };
11052
11296
  }
11053
11297
  if (await sha256File(revision.sourceFile) !== revision.sourceSha256) {
@@ -11055,7 +11299,7 @@ async function inspectRevision(spec, lock, seen) {
11055
11299
  }
11056
11300
  }
11057
11301
  if (revision.maskFile) {
11058
- if (!revision.maskSha256 || !(0, import_node_fs8.existsSync)(revision.maskFile)) {
11302
+ if (!revision.maskSha256 || !(0, import_node_fs9.existsSync)(revision.maskFile)) {
11059
11303
  return { ready: false, reason: `revision mask is missing: ${revision.maskFile}` };
11060
11304
  }
11061
11305
  if (await sha256File(revision.maskFile) !== revision.maskSha256) {
@@ -11063,7 +11307,7 @@ async function inspectRevision(spec, lock, seen) {
11063
11307
  }
11064
11308
  }
11065
11309
  if (revision.lastFrameFile) {
11066
- if (!revision.lastFrameSha256 || !(0, import_node_fs8.existsSync)(revision.lastFrameFile)) {
11310
+ if (!revision.lastFrameSha256 || !(0, import_node_fs9.existsSync)(revision.lastFrameFile)) {
11067
11311
  return { ready: false, reason: `revision last frame is missing: ${revision.lastFrameFile}` };
11068
11312
  }
11069
11313
  if (await sha256File(revision.lastFrameFile) !== revision.lastFrameSha256) {
@@ -11076,7 +11320,7 @@ async function inspectSourceMembers(spec, lock) {
11076
11320
  const revision = spec.revision;
11077
11321
  const members = revision.sourceMembers;
11078
11322
  for (const member of members) {
11079
- if (!(0, import_node_fs8.existsSync)(member.file)) return { ready: false, reason: `revision source member is missing: ${member.file}` };
11323
+ if (!(0, import_node_fs9.existsSync)(member.file)) return { ready: false, reason: `revision source member is missing: ${member.file}` };
11080
11324
  if (await sha256File(member.file) !== member.sha256) {
11081
11325
  return { ready: false, reason: `revision source member changed after the manifest was resolved: ${member.file}` };
11082
11326
  }
@@ -11085,8 +11329,8 @@ async function inspectSourceMembers(spec, lock) {
11085
11329
  if (parent.source) return { ready: true, reason: "declared source set is present" };
11086
11330
  const entry = lock.entries[lockKey(parent.styleId, parent.assetId)];
11087
11331
  if (!entry) return { ready: false, reason: "revision source is not in the lockfile" };
11088
- const recorded = entry.outputs.map((_, index) => import_node_path11.default.resolve(currentEntryOutputPath(entry, parent, index))).sort();
11089
- const found = members.map((member) => import_node_path11.default.resolve(member.file)).sort();
11332
+ const recorded = entry.outputs.map((_, index) => import_node_path12.default.resolve(currentEntryOutputPath(entry, parent, index))).sort();
11333
+ const found = members.map((member) => import_node_path12.default.resolve(member.file)).sort();
11090
11334
  if (recorded.length !== found.length || recorded.some((file, index) => file !== found[index])) {
11091
11335
  return {
11092
11336
  ready: false,
@@ -11135,8 +11379,8 @@ async function inspectParent(spec, lock, seen) {
11135
11379
  };
11136
11380
  }
11137
11381
  if (spec.source) {
11138
- const source = import_node_path11.default.resolve(spec.root, spec.source);
11139
- return (0, import_node_fs8.existsSync)(source) ? { ready: true, reason: "declared source is present" } : { ready: false, reason: `declared source is missing: ${spec.source}` };
11382
+ const source = import_node_path12.default.resolve(spec.root, spec.source);
11383
+ return (0, import_node_fs9.existsSync)(source) ? { ready: true, reason: "declared source is present" } : { ready: false, reason: `declared source is missing: ${spec.source}` };
11140
11384
  }
11141
11385
  const entry = lock.entries[key];
11142
11386
  if (!entry) return { ready: false, reason: "parent is not in the lockfile" };
@@ -11150,7 +11394,7 @@ async function inspectParent(spec, lock, seen) {
11150
11394
  for (let index = 0; index < entry.outputs.length; index++) {
11151
11395
  const output = entry.outputs[index];
11152
11396
  const file = currentEntryOutputPath(entry, spec, index);
11153
- if (!(0, import_node_fs8.existsSync)(file)) return { ready: false, reason: `parent output is missing: ${file}` };
11397
+ if (!(0, import_node_fs9.existsSync)(file)) return { ready: false, reason: `parent output is missing: ${file}` };
11154
11398
  if (await sha256File(file) !== output.sha256) {
11155
11399
  return { ready: false, reason: `parent output was modified: ${file}` };
11156
11400
  }
@@ -11214,7 +11458,7 @@ async function buildPlan(specs, lock, opts = {}) {
11214
11458
  state = "missing";
11215
11459
  reason = "--force";
11216
11460
  } else if (!entry && spec.source && spec.generator !== "frames") {
11217
- if ((0, import_node_fs9.existsSync)(import_node_path12.default.resolve(spec.root, spec.source))) {
11461
+ if ((0, import_node_fs10.existsSync)(import_node_path13.default.resolve(spec.root, spec.source))) {
11218
11462
  state = "ok";
11219
11463
  reason = `placed from ${spec.source}; not generated`;
11220
11464
  } else {
@@ -11222,7 +11466,7 @@ async function buildPlan(specs, lock, opts = {}) {
11222
11466
  reason = `declared source is missing: ${spec.source}`;
11223
11467
  }
11224
11468
  } else if (!entry) {
11225
- if ((0, import_node_fs9.existsSync)(spec.outFile)) {
11469
+ if ((0, import_node_fs10.existsSync)(spec.outFile)) {
11226
11470
  state = "untracked";
11227
11471
  reason = "file on disk, no provenance recorded";
11228
11472
  } else {
@@ -11235,7 +11479,7 @@ async function buildPlan(specs, lock, opts = {}) {
11235
11479
  } else if (spec.mirror && !mirrorCurrent(entry, lock.entries[lockKey(spec.styleId, spec.mirror.sourceAssetId)])) {
11236
11480
  state = "stale";
11237
11481
  reason = `mirror source ${spec.mirror.sourceAssetId} changed; flip it again (no generation cost)`;
11238
- } else if (spec.mirror && entry.status === "downloaded" && entry.outputs.some((_output, index) => !(0, import_node_fs9.existsSync)(currentEntryOutputPath(entry, spec, index)))) {
11482
+ } else if (spec.mirror && entry.status === "downloaded" && entry.outputs.some((_output, index) => !(0, import_node_fs10.existsSync)(currentEntryOutputPath(entry, spec, index)))) {
11239
11483
  state = "stale";
11240
11484
  reason = "mirror output missing on disk; flip it again (no generation cost)";
11241
11485
  } else if (spec.character?.kind === "outfit" && entry.outfit?.sourceSha256 !== mirrorSourceHash(lock.entries[lockKey(spec.styleId, spec.character.parentAssetId)] ?? { outputs: [] })) {
@@ -11263,9 +11507,9 @@ async function buildPlan(specs, lock, opts = {}) {
11263
11507
  } else if (entry.status !== "downloaded") {
11264
11508
  state = "in-flight";
11265
11509
  reason = `awaiting ${entry.status}`;
11266
- } else if (entry.outputs.length === 0 || entry.outputs.some((_output, index) => !(0, import_node_fs9.existsSync)(currentEntryOutputPath(entry, spec, index)))) {
11510
+ } else if (entry.outputs.length === 0 || entry.outputs.some((_output, index) => !(0, import_node_fs10.existsSync)(currentEntryOutputPath(entry, spec, index)))) {
11267
11511
  state = "orphaned";
11268
- reason = entry.outputs.length === 0 ? "no recorded output files" : `missing on disk: ${entry.outputs.filter((_output, index) => !(0, import_node_fs9.existsSync)(currentEntryOutputPath(entry, spec, index))).map((o) => o.role ?? "asset").join(", ")}`;
11512
+ reason = entry.outputs.length === 0 ? "no recorded output files" : `missing on disk: ${entry.outputs.filter((_output, index) => !(0, import_node_fs10.existsSync)(currentEntryOutputPath(entry, spec, index))).map((o) => o.role ?? "asset").join(", ")}`;
11269
11513
  } else if (await anyOutputModified(entry, spec)) {
11270
11514
  state = "orphaned";
11271
11515
  reason = "output modified since download";
@@ -11356,13 +11600,13 @@ function summarize(plan) {
11356
11600
 
11357
11601
  // src/project.ts
11358
11602
  async function openProject(manifestPath = "pixelkiln.manifest.json", opts = {}) {
11359
- const absolute = import_node_path13.default.resolve(manifestPath);
11360
- const root = import_node_path13.default.dirname(absolute);
11603
+ const absolute = import_node_path14.default.resolve(manifestPath);
11604
+ const root = import_node_path14.default.dirname(absolute);
11361
11605
  if (opts.env !== false) {
11362
11606
  loadEnvFiles(root);
11363
- if (import_node_path13.default.resolve(process.cwd()) !== root) loadEnvFiles(process.cwd());
11607
+ if (import_node_path14.default.resolve(process.cwd()) !== root) loadEnvFiles(process.cwd());
11364
11608
  }
11365
- const lockPath = import_node_path13.default.resolve(opts.lockPath ?? import_node_path13.default.join(root, "pixelkiln.lock.json"));
11609
+ const lockPath = import_node_path14.default.resolve(opts.lockPath ?? import_node_path14.default.join(root, "pixelkiln.lock.json"));
11366
11610
  const loaded = await loadManifest(absolute);
11367
11611
  const specs = await resolveSpecs(loaded, { styles: opts.styles ?? [], assets: opts.assets ?? [] });
11368
11612
  const lock = await loadLock(lockPath);
@@ -11429,8 +11673,8 @@ function shouldPersistSourceUrl(value) {
11429
11673
  }
11430
11674
 
11431
11675
  // src/recipes.ts
11432
- var import_node_path14 = __toESM(require("path"), 1);
11433
- var import_node_fs10 = require("fs");
11676
+ var import_node_path15 = __toESM(require("path"), 1);
11677
+ var import_node_fs11 = require("fs");
11434
11678
  var import_promises7 = require("fs/promises");
11435
11679
  var import_node_url = require("url");
11436
11680
  var import_zod4 = require("zod");
@@ -11440,7 +11684,7 @@ var VERSION_RE = /^\d+\.\d+\.\d+$/;
11440
11684
  var RECIPE_FILE = "pixelkiln.recipe.json";
11441
11685
  var RECIPE_DIR_TOKEN = "{{recipeDir}}";
11442
11686
  var PortablePathSchema = import_zod4.z.string().min(1).refine((value) => {
11443
- if (value.includes("\\") || import_node_path14.default.posix.isAbsolute(value)) return false;
11687
+ if (value.includes("\\") || import_node_path15.default.posix.isAbsolute(value)) return false;
11444
11688
  const parts = value.split("/");
11445
11689
  return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
11446
11690
  }, "expected a portable relative path without '.', '..', or backslashes");
@@ -11616,7 +11860,7 @@ function message3(error) {
11616
11860
  return error instanceof Error ? error.message : String(error);
11617
11861
  }
11618
11862
  async function readRecipeFile(recipePath, bundled) {
11619
- const absolute = import_node_path14.default.resolve(recipePath);
11863
+ const absolute = import_node_path15.default.resolve(recipePath);
11620
11864
  let raw;
11621
11865
  try {
11622
11866
  raw = JSON.parse(await (0, import_promises7.readFile)(absolute, "utf8"));
@@ -11630,13 +11874,13 @@ async function readRecipeFile(recipePath, bundled) {
11630
11874
  ` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
11631
11875
  );
11632
11876
  }
11633
- return { recipe: parsed.data, authored: raw, path: absolute, dir: import_node_path14.default.dirname(absolute), bundled };
11877
+ return { recipe: parsed.data, authored: raw, path: absolute, dir: import_node_path15.default.dirname(absolute), bundled };
11634
11878
  }
11635
11879
  async function walkRecipeFiles(root) {
11636
- if (!(0, import_node_fs10.existsSync)(root)) return [];
11880
+ if (!(0, import_node_fs11.existsSync)(root)) return [];
11637
11881
  const out = [];
11638
11882
  for (const entry of await (0, import_promises7.readdir)(root, { withFileTypes: true })) {
11639
- const resolved = import_node_path14.default.join(root, entry.name);
11883
+ const resolved = import_node_path15.default.join(root, entry.name);
11640
11884
  if (entry.isDirectory()) out.push(...await walkRecipeFiles(resolved));
11641
11885
  else if (entry.isFile() && entry.name === RECIPE_FILE) out.push(resolved);
11642
11886
  }
@@ -11647,7 +11891,7 @@ function bundledRecipeRoot() {
11647
11891
  }
11648
11892
  async function listBundledRecipes(root = bundledRecipeRoot()) {
11649
11893
  const recipes = await Promise.all(
11650
- (await walkRecipeFiles(import_node_path14.default.resolve(root))).map((file) => readRecipeFile(file, true))
11894
+ (await walkRecipeFiles(import_node_path15.default.resolve(root))).map((file) => readRecipeFile(file, true))
11651
11895
  );
11652
11896
  return recipes.sort(
11653
11897
  (a, b) => a.recipe.id.localeCompare(b.recipe.id) || compareVersions(b.recipe.version, a.recipe.version)
@@ -11670,9 +11914,9 @@ function parseSelector(selector) {
11670
11914
  return { id: match[1], version: match[2] };
11671
11915
  }
11672
11916
  async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
11673
- const local = import_node_path14.default.resolve(target);
11674
- if ((0, import_node_fs10.existsSync)(local)) {
11675
- const recipePath = import_node_path14.default.basename(local) === RECIPE_FILE ? local : import_node_path14.default.join(local, RECIPE_FILE);
11917
+ const local = import_node_path15.default.resolve(target);
11918
+ if ((0, import_node_fs11.existsSync)(local)) {
11919
+ const recipePath = import_node_path15.default.basename(local) === RECIPE_FILE ? local : import_node_path15.default.join(local, RECIPE_FILE);
11676
11920
  return readRecipeFile(recipePath, false);
11677
11921
  }
11678
11922
  if (target.startsWith(".") || target.startsWith("/") || target.includes("\\")) {
@@ -11686,8 +11930,8 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
11686
11930
  return matches[0];
11687
11931
  }
11688
11932
  async function verifyFile(root, file) {
11689
- const absolute = import_node_path14.default.resolve(root, ...file.path.split("/"));
11690
- if (!(0, import_node_fs10.existsSync)(absolute)) {
11933
+ const absolute = import_node_path15.default.resolve(root, ...file.path.split("/"));
11934
+ if (!(0, import_node_fs11.existsSync)(absolute)) {
11691
11935
  return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
11692
11936
  }
11693
11937
  const actualSha256 = await sha256File(absolute);
@@ -11702,7 +11946,7 @@ async function verifyRecipe(target, options = {}) {
11702
11946
  const loaded = await resolveRecipe(target, options.bundledRoot);
11703
11947
  const actualIntegrity = recipeDigest(loaded.authored);
11704
11948
  const files = await Promise.all(loaded.recipe.files.map((file) => verifyFile(loaded.dir, file)));
11705
- const modelRoot = options.modelRoot ? import_node_path14.default.resolve(options.modelRoot) : null;
11949
+ const modelRoot = options.modelRoot ? import_node_path15.default.resolve(options.modelRoot) : null;
11706
11950
  const models = modelRoot ? await Promise.all(loaded.recipe.models.map(async (model) => ({
11707
11951
  ...await verifyFile(modelRoot, model),
11708
11952
  source: model.source,
@@ -11736,7 +11980,7 @@ async function verifyRecipe(target, options = {}) {
11736
11980
  };
11737
11981
  }
11738
11982
  function renderStyle(style, destination, cwd) {
11739
- const portableDir = import_node_path14.default.relative(cwd, destination).split(import_node_path14.default.sep).join("/") || ".";
11983
+ const portableDir = import_node_path15.default.relative(cwd, destination).split(import_node_path15.default.sep).join("/") || ".";
11740
11984
  const replace = (value) => {
11741
11985
  if (typeof value === "string") return value.replaceAll(RECIPE_DIR_TOKEN, portableDir);
11742
11986
  if (Array.isArray(value)) return value.map(replace);
@@ -11748,24 +11992,24 @@ function renderStyle(style, destination, cwd) {
11748
11992
  return replace(style);
11749
11993
  }
11750
11994
  async function installRecipe(target, options = {}) {
11751
- const cwd = import_node_path14.default.resolve(options.cwd ?? process.cwd());
11995
+ const cwd = import_node_path15.default.resolve(options.cwd ?? process.cwd());
11752
11996
  const loaded = await resolveRecipe(target, options.bundledRoot);
11753
11997
  const verification = await verifyRecipe(loaded.path);
11754
11998
  if (!verification.ok) {
11755
11999
  throw new Error(`Recipe ${loaded.recipe.id}@${loaded.recipe.version} failed integrity verification.`);
11756
12000
  }
11757
- const destination = options.out ? import_node_path14.default.resolve(cwd, options.out) : import_node_path14.default.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
12001
+ const destination = options.out ? import_node_path15.default.resolve(cwd, options.out) : import_node_path15.default.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
11758
12002
  const sources = [
11759
12003
  { path: RECIPE_FILE, source: loaded.path },
11760
- ...loaded.recipe.files.map((file) => ({ path: file.path, source: import_node_path14.default.join(loaded.dir, ...file.path.split("/")) }))
12004
+ ...loaded.recipe.files.map((file) => ({ path: file.path, source: import_node_path15.default.join(loaded.dir, ...file.path.split("/")) }))
11761
12005
  ];
11762
12006
  const files = await Promise.all(sources.map(async (file) => ({
11763
- path: import_node_path14.default.join(destination, ...file.path.split("/")),
12007
+ path: import_node_path15.default.join(destination, ...file.path.split("/")),
11764
12008
  data: await (0, import_promises7.readFile)(file.source)
11765
12009
  })));
11766
12010
  if (!options.force) {
11767
12011
  for (const file of files) {
11768
- if (!(0, import_node_fs10.existsSync)(file.path)) continue;
12012
+ if (!(0, import_node_fs11.existsSync)(file.path)) continue;
11769
12013
  const current = await (0, import_promises7.readFile)(file.path);
11770
12014
  if (!current.equals(file.data)) {
11771
12015
  throw new OverwriteRefusedError(
@@ -11791,8 +12035,8 @@ async function installRecipe(target, options = {}) {
11791
12035
 
11792
12036
  // src/pipeline/audit.ts
11793
12037
  var import_promises8 = require("fs/promises");
11794
- var import_node_fs11 = require("fs");
11795
- var import_node_path15 = __toESM(require("path"), 1);
12038
+ var import_node_fs12 = require("fs");
12039
+ var import_node_path16 = __toESM(require("path"), 1);
11796
12040
  function paletteDistance(asset, reference) {
11797
12041
  if (!asset.length || !reference.length) return 0;
11798
12042
  let total = 0;
@@ -11828,7 +12072,7 @@ async function auditStyle(loaded, specs, styleId, lock) {
11828
12072
  const unreadable = [];
11829
12073
  for (const spec of mine) {
11830
12074
  for (const output of resolveSpecOutputs(spec, lock, loaded.root)) {
11831
- if (!(0, import_node_fs11.existsSync)(output.absolutePath)) {
12075
+ if (!(0, import_node_fs12.existsSync)(output.absolutePath)) {
11832
12076
  missing.push(output.id);
11833
12077
  continue;
11834
12078
  }
@@ -11857,8 +12101,8 @@ async function auditStyle(loaded, specs, styleId, lock) {
11857
12101
  let referenceFromStyleImages = false;
11858
12102
  const refPalettes = [];
11859
12103
  for (const img of style.styleImages) {
11860
- const abs = import_node_path15.default.resolve(loaded.root, img.path);
11861
- if (!(0, import_node_fs11.existsSync)(abs)) continue;
12104
+ const abs = import_node_path16.default.resolve(loaded.root, img.path);
12105
+ if (!(0, import_node_fs12.existsSync)(abs)) continue;
11862
12106
  try {
11863
12107
  refPalettes.push(extractPalette(decodePng(await (0, import_promises8.readFile)(abs)), 12));
11864
12108
  } catch {
@@ -11936,19 +12180,19 @@ function hex(c) {
11936
12180
  }
11937
12181
 
11938
12182
  // src/pixellab-utils.ts
11939
- var import_node_fs12 = require("fs");
12183
+ var import_node_fs13 = require("fs");
11940
12184
  var import_promises9 = require("fs/promises");
11941
- var import_node_path16 = __toESM(require("path"), 1);
12185
+ var import_node_path17 = __toESM(require("path"), 1);
11942
12186
  var UNZOOM_MIN_SIDE = 256;
11943
12187
  var UNZOOM_MAX_AREA = 2048 * 2048;
11944
12188
  function defaultUnzoomOut(input) {
11945
- const ext = import_node_path16.default.extname(input);
12189
+ const ext = import_node_path17.default.extname(input);
11946
12190
  return `${ext ? input.slice(0, -ext.length) : input}.unzoomed.png`;
11947
12191
  }
11948
12192
  async function unzoomFile(client2, input, opts = {}) {
11949
- const out = import_node_path16.default.resolve(opts.out ?? defaultUnzoomOut(input));
11950
- if (import_node_path16.default.resolve(input) === out) throw new Error("unzoom will not overwrite its own input; pass a different --out");
11951
- if ((0, import_node_fs12.existsSync)(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
12193
+ const out = import_node_path17.default.resolve(opts.out ?? defaultUnzoomOut(input));
12194
+ if (import_node_path17.default.resolve(input) === out) throw new Error("unzoom will not overwrite its own input; pass a different --out");
12195
+ if ((0, import_node_fs13.existsSync)(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
11952
12196
  const bytes = await (0, import_promises9.readFile)(input);
11953
12197
  const metadata = imageMetadata(bytes);
11954
12198
  if (!metadata) throw new Error(`${input} is not a readable PNG or JPEG`);
@@ -11962,7 +12206,7 @@ async function unzoomFile(client2, input, opts = {}) {
11962
12206
  throw new Error(`${input} is ${width}x${height}; PixelLab's unzoom takes at most 2048x2048 worth of pixels`);
11963
12207
  }
11964
12208
  const res = await client2.unzoom({ image: { base64: bytes.toString("base64"), format }, quantize: opts.quantize });
11965
- await (0, import_promises9.mkdir)(import_node_path16.default.dirname(out), { recursive: true });
12209
+ await (0, import_promises9.mkdir)(import_node_path17.default.dirname(out), { recursive: true });
11966
12210
  await (0, import_promises9.writeFile)(out, res.png);
11967
12211
  return { out, original: res.originalSize, unzoomed: res.unzoomedSize, zoomFactor: res.zoomFactor, usage: res.usage };
11968
12212
  }
@@ -11970,14 +12214,14 @@ var FONT_WEIGHTS = ["Bold", "Regular"];
11970
12214
  var FONT_GLYPH_SIZES = [8, 16, 32, 64];
11971
12215
  var FONT_COST_GENERATIONS = 25;
11972
12216
  function fontOutputPaths(out) {
11973
- const base = import_node_path16.default.resolve(out.replace(/\.(ttf|png)$/i, ""));
12217
+ const base = import_node_path17.default.resolve(out.replace(/\.(ttf|png)$/i, ""));
11974
12218
  return { ttf: `${base}.ttf`, atlas: `${base}.png` };
11975
12219
  }
11976
12220
  async function generateFont(client2, opts) {
11977
12221
  const { ttf, atlas } = fontOutputPaths(opts.out);
11978
12222
  if (!opts.force) {
11979
12223
  for (const file of [ttf, atlas]) {
11980
- if ((0, import_node_fs12.existsSync)(file)) throw new Error(`${file} already exists; pass --force to replace it`);
12224
+ if ((0, import_node_fs13.existsSync)(file)) throw new Error(`${file} already exists; pass --force to replace it`);
11981
12225
  }
11982
12226
  }
11983
12227
  const sleep3 = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
@@ -12004,7 +12248,7 @@ async function generateFont(client2, opts) {
12004
12248
  client2.download(job.download_ttf_url),
12005
12249
  client2.download(job.download_atlas_url)
12006
12250
  ]);
12007
- await (0, import_promises9.mkdir)(import_node_path16.default.dirname(ttf), { recursive: true });
12251
+ await (0, import_promises9.mkdir)(import_node_path17.default.dirname(ttf), { recursive: true });
12008
12252
  await (0, import_promises9.writeFile)(ttf, ttfBytes);
12009
12253
  await (0, import_promises9.writeFile)(atlas, atlasBytes);
12010
12254
  return { jobId, ttf, atlas, glyphPx: job.glyph_px ?? null, usage: job.usage };
@@ -12019,15 +12263,15 @@ async function generateFont(client2, opts) {
12019
12263
  }
12020
12264
 
12021
12265
  // src/pipeline/quality-regression.ts
12022
- var import_node_path17 = __toESM(require("path"), 1);
12023
- var import_node_fs13 = require("fs");
12266
+ var import_node_path18 = __toESM(require("path"), 1);
12267
+ var import_node_fs14 = require("fs");
12024
12268
  var import_promises10 = require("fs/promises");
12025
12269
  var import_zod5 = require("zod");
12026
12270
  var SHA256_RE2 = /^[0-9a-f]{64}$/;
12027
12271
  var HEX_RE = /^#[0-9a-f]{6}$/;
12028
12272
  var MAX_REDMEAN_DISTANCE = 765;
12029
12273
  var QualityPathSchema = import_zod5.z.string().min(1).refine(
12030
- (value) => !import_node_path17.default.posix.isAbsolute(value) && !value.includes("\\"),
12274
+ (value) => !import_node_path18.default.posix.isAbsolute(value) && !value.includes("\\"),
12031
12275
  "expected a portable relative path"
12032
12276
  );
12033
12277
  var DEFAULT_QUALITY_TOLERANCES = {
@@ -12174,7 +12418,7 @@ function measureDecodedImageQuality(png, digest2) {
12174
12418
  };
12175
12419
  }
12176
12420
  async function measureImageQuality(file) {
12177
- const absolute = import_node_path17.default.resolve(file);
12421
+ const absolute = import_node_path18.default.resolve(file);
12178
12422
  let bytes;
12179
12423
  try {
12180
12424
  bytes = await (0, import_promises10.readFile)(absolute);
@@ -12196,11 +12440,11 @@ function resolveQualityInputs(raw, inputsFilePath) {
12196
12440
  }
12197
12441
  const ids = parsed.data.map((entry) => entry.id);
12198
12442
  if (new Set(ids).size !== ids.length) throw new Error("--inputs contains a duplicate quality case id.");
12199
- const root = import_node_path17.default.dirname(import_node_path17.default.resolve(inputsFilePath));
12443
+ const root = import_node_path18.default.dirname(import_node_path18.default.resolve(inputsFilePath));
12200
12444
  return parsed.data.map((entry) => ({
12201
12445
  id: entry.id,
12202
- file: import_node_path17.default.resolve(root, entry.path),
12203
- ...entry.record ? { record: import_node_path17.default.resolve(root, entry.record) } : {},
12446
+ file: import_node_path18.default.resolve(root, entry.path),
12447
+ ...entry.record ? { record: import_node_path18.default.resolve(root, entry.record) } : {},
12204
12448
  tolerances: QualityToleranceSchema.parse({
12205
12449
  ...DEFAULT_QUALITY_TOLERANCES,
12206
12450
  ...entry.tolerances
@@ -12208,7 +12452,7 @@ function resolveQualityInputs(raw, inputsFilePath) {
12208
12452
  }));
12209
12453
  }
12210
12454
  function portableRelative2(from, to) {
12211
- return import_node_path17.default.relative(from, import_node_path17.default.resolve(to)).split(import_node_path17.default.sep).join("/") || ".";
12455
+ return import_node_path18.default.relative(from, import_node_path18.default.resolve(to)).split(import_node_path18.default.sep).join("/") || ".";
12212
12456
  }
12213
12457
  async function verifyRecordForImage(recordPath, imagePath) {
12214
12458
  const verification = await verifyArtifactBundle(recordPath);
@@ -12223,20 +12467,20 @@ async function verifyRecordForImage(recordPath, imagePath) {
12223
12467
  if (record.kind !== "refine" || options === null || typeof options !== "object" || options.schema !== "pixelkiln-quality") {
12224
12468
  throw new Error(`Quality record ${recordPath} is not a PixelKiln refinement record.`);
12225
12469
  }
12226
- const outputs = record.outputs.map((output) => import_node_path17.default.resolve(import_node_path17.default.dirname(recordPath), output.path));
12227
- if (!outputs.includes(import_node_path17.default.resolve(imagePath))) {
12470
+ const outputs = record.outputs.map((output) => import_node_path18.default.resolve(import_node_path18.default.dirname(recordPath), output.path));
12471
+ if (!outputs.includes(import_node_path18.default.resolve(imagePath))) {
12228
12472
  throw new Error(`Quality record ${recordPath} does not own ${imagePath}.`);
12229
12473
  }
12230
12474
  }
12231
12475
  async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
12232
12476
  if (!inputs.length) throw new Error("Cannot snapshot an empty quality baseline.");
12233
- const absolute = import_node_path17.default.resolve(baselinePath);
12477
+ const absolute = import_node_path18.default.resolve(baselinePath);
12234
12478
  for (const input of inputs) {
12235
- if (import_node_path17.default.resolve(input.file) === absolute || input.record && import_node_path17.default.resolve(input.record) === absolute) {
12479
+ if (import_node_path18.default.resolve(input.file) === absolute || input.record && import_node_path18.default.resolve(input.record) === absolute) {
12236
12480
  throw new Error(`Quality baseline output would overwrite an input: ${absolute}.`);
12237
12481
  }
12238
12482
  }
12239
- const root = import_node_path17.default.dirname(absolute);
12483
+ const root = import_node_path18.default.dirname(absolute);
12240
12484
  const cases = [];
12241
12485
  for (const input of [...inputs].sort((a, b) => a.id.localeCompare(b.id))) {
12242
12486
  const expected = await measureImageQuality(input.file);
@@ -12263,7 +12507,7 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
12263
12507
  cases
12264
12508
  });
12265
12509
  const data = Buffer.from(JSON.stringify(baseline, null, 2) + "\n");
12266
- if ((0, import_node_fs13.existsSync)(absolute) && !options.force) {
12510
+ if ((0, import_node_fs14.existsSync)(absolute) && !options.force) {
12267
12511
  const current = await (0, import_promises10.readFile)(absolute);
12268
12512
  if (!current.equals(data)) {
12269
12513
  throw new OverwriteRefusedError(
@@ -12276,7 +12520,7 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
12276
12520
  return { baseline, path: absolute, changed: result.changed.length > 0 };
12277
12521
  }
12278
12522
  async function readQualityBaseline(baselinePath) {
12279
- const absolute = import_node_path17.default.resolve(baselinePath);
12523
+ const absolute = import_node_path18.default.resolve(baselinePath);
12280
12524
  let raw;
12281
12525
  try {
12282
12526
  raw = JSON.parse(await (0, import_promises10.readFile)(absolute, "utf8"));
@@ -12349,12 +12593,12 @@ function regressionViolations(expected, actual, tolerances) {
12349
12593
  return { violations, warnings };
12350
12594
  }
12351
12595
  async function checkQualityBaseline(baselinePath) {
12352
- const absolute = import_node_path17.default.resolve(baselinePath);
12596
+ const absolute = import_node_path18.default.resolve(baselinePath);
12353
12597
  const baseline = await readQualityBaseline(absolute);
12354
- const root = import_node_path17.default.dirname(absolute);
12598
+ const root = import_node_path18.default.dirname(absolute);
12355
12599
  const cases = [];
12356
12600
  for (const entry of baseline.cases) {
12357
- const file = import_node_path17.default.resolve(root, entry.file);
12601
+ const file = import_node_path18.default.resolve(root, entry.file);
12358
12602
  let actual = null;
12359
12603
  const violations = [];
12360
12604
  const warnings = [];
@@ -12367,7 +12611,7 @@ async function checkQualityBaseline(baselinePath) {
12367
12611
  violations.push(message4(error));
12368
12612
  }
12369
12613
  if (entry.record) {
12370
- const recordPath = import_node_path17.default.resolve(root, entry.record.path);
12614
+ const recordPath = import_node_path18.default.resolve(root, entry.record.path);
12371
12615
  try {
12372
12616
  const recordHash = await sha256File(recordPath);
12373
12617
  if (recordHash !== entry.record.sha256) violations.push("quality record hash changed");
@@ -12403,14 +12647,14 @@ async function checkQualityBaseline(baselinePath) {
12403
12647
  }
12404
12648
 
12405
12649
  // src/pipeline/cache-health.ts
12406
- var import_node_fs17 = require("fs");
12650
+ var import_node_fs18 = require("fs");
12407
12651
  var import_promises13 = require("fs/promises");
12408
- var import_node_path21 = __toESM(require("path"), 1);
12652
+ var import_node_path22 = __toESM(require("path"), 1);
12409
12653
 
12410
12654
  // src/cache.ts
12411
12655
  var import_promises11 = require("fs/promises");
12412
- var import_node_fs14 = require("fs");
12413
- var import_node_path18 = __toESM(require("path"), 1);
12656
+ var import_node_fs15 = require("fs");
12657
+ var import_node_path19 = __toESM(require("path"), 1);
12414
12658
  var import_zod6 = require("zod");
12415
12659
  var HashCacheSchema = import_zod6.z.object({
12416
12660
  version: import_zod6.z.literal(1),
@@ -12424,10 +12668,10 @@ function isSha256Hash(value) {
12424
12668
  function parseCache(value) {
12425
12669
  return HashCacheSchema.parse(value);
12426
12670
  }
12427
- async function loadCache(path36) {
12428
- if (!(0, import_node_fs14.existsSync)(path36)) return { version: 1, hashes: {} };
12671
+ async function loadCache(path38) {
12672
+ if (!(0, import_node_fs15.existsSync)(path38)) return { version: 1, hashes: {} };
12429
12673
  try {
12430
- const parsed = HashCacheSchema.safeParse(JSON.parse(await (0, import_promises11.readFile)(path36, "utf8")));
12674
+ const parsed = HashCacheSchema.safeParse(JSON.parse(await (0, import_promises11.readFile)(path38, "utf8")));
12431
12675
  if (!parsed.success) return { version: 1, hashes: {} };
12432
12676
  return {
12433
12677
  version: 1,
@@ -12439,18 +12683,18 @@ async function loadCache(path36) {
12439
12683
  return { version: 1, hashes: {} };
12440
12684
  }
12441
12685
  }
12442
- async function saveCache(path36, cache) {
12686
+ async function saveCache(path38, cache) {
12443
12687
  const sorted = {};
12444
12688
  for (const key of Object.keys(cache.hashes).sort()) {
12445
12689
  const hash = cache.hashes[key];
12446
12690
  if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
12447
12691
  sorted[key] = hash;
12448
12692
  }
12449
- await (0, import_promises11.mkdir)(import_node_path18.default.dirname(import_node_path18.default.resolve(path36)), { recursive: true });
12450
- const tmp = `${path36}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
12693
+ await (0, import_promises11.mkdir)(import_node_path19.default.dirname(import_node_path19.default.resolve(path38)), { recursive: true });
12694
+ const tmp = `${path38}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
12451
12695
  try {
12452
12696
  await (0, import_promises11.writeFile)(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
12453
- await (0, import_promises11.rename)(tmp, path36);
12697
+ await (0, import_promises11.rename)(tmp, path38);
12454
12698
  } finally {
12455
12699
  await (0, import_promises11.rm)(tmp, { force: true });
12456
12700
  }
@@ -12474,20 +12718,20 @@ function cachePathFor(lockPath) {
12474
12718
  }
12475
12719
 
12476
12720
  // src/pipeline/history.ts
12477
- var import_node_fs16 = require("fs");
12478
- var import_node_path20 = __toESM(require("path"), 1);
12721
+ var import_node_fs17 = require("fs");
12722
+ var import_node_path21 = __toESM(require("path"), 1);
12479
12723
 
12480
12724
  // src/pipeline/fetch.ts
12481
- var import_node_fs15 = require("fs");
12725
+ var import_node_fs16 = require("fs");
12482
12726
  var import_promises12 = require("fs/promises");
12483
- var import_node_path19 = __toESM(require("path"), 1);
12727
+ var import_node_path20 = __toESM(require("path"), 1);
12484
12728
  async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12485
12729
  const log = opts.onProgress ?? (() => {
12486
12730
  });
12487
12731
  const result = { downloaded: 0, skipped: 0, failed: 0, ...opts.refresh ? { unchanged: 0 } : {} };
12488
12732
  const specByKey = new Map(specs.map((s) => [lockKey(s.styleId, s.assetId), s]));
12489
12733
  normalizeLockOutputPaths(lock, specs);
12490
- const cacheDir = opts.cacheDir === false ? null : import_node_path19.default.resolve(opts.cacheDir ?? import_node_path19.default.join(import_node_path19.default.dirname(lockPath), ".pixelkiln", "cache"));
12734
+ const cacheDir = opts.cacheDir === false ? null : import_node_path20.default.resolve(opts.cacheDir ?? import_node_path20.default.join(import_node_path20.default.dirname(lockPath), ".pixelkiln", "cache"));
12491
12735
  const pending = Object.entries(lock.entries).filter(([key, e]) => {
12492
12736
  if (e.provider !== provider.id || !specByKey.has(key)) return false;
12493
12737
  if (specByKey.get(key)?.mirror) return false;
@@ -12498,15 +12742,15 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12498
12742
  if (spec && e.specHash === spec.specHash && e.outputs.length && !postprocessCurrent(e, spec)) {
12499
12743
  return Boolean(opts.force) || e.outputs.every((output) => {
12500
12744
  const file = resolveOutputPath(output.path, spec.root);
12501
- return (0, import_node_fs15.existsSync)(file) && sha256((0, import_node_fs15.readFileSync)(file)) === output.sha256;
12745
+ return (0, import_node_fs16.existsSync)(file) && sha256((0, import_node_fs16.readFileSync)(file)) === output.sha256;
12502
12746
  });
12503
12747
  }
12504
12748
  if (!opts.repair) return false;
12505
12749
  if (e.outputs.length === 0 || !spec) return true;
12506
12750
  return e.outputs.some((output) => {
12507
12751
  const file = resolveOutputPath(output.path, spec.root);
12508
- if (!(0, import_node_fs15.existsSync)(file)) return true;
12509
- const current = sha256((0, import_node_fs15.readFileSync)(file));
12752
+ if (!(0, import_node_fs16.existsSync)(file)) return true;
12753
+ const current = sha256((0, import_node_fs16.readFileSync)(file));
12510
12754
  if (current === output.sha256) return false;
12511
12755
  return Boolean(opts.force) || (e.supersededOutputs ?? []).some((old) => old.sha256 === current);
12512
12756
  });
@@ -12589,14 +12833,14 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12589
12833
  if (!source.url) throw new Error(`no durable source URL to refresh ${source.role ?? "asset"} from`);
12590
12834
  fresh = await provider.download(source.url);
12591
12835
  if (recorded && sha256(fresh) === recordedRaw && !reapply) {
12592
- log(` current ${import_node_path19.default.relative(process.cwd(), target)}`);
12836
+ log(` current ${import_node_path20.default.relative(process.cwd(), target)}`);
12593
12837
  outputs.push({ ...recorded, path: portableOutputPath(target, spec.root) });
12594
12838
  continue;
12595
12839
  }
12596
12840
  }
12597
12841
  let raw = null;
12598
12842
  let final = null;
12599
- if ((0, import_node_fs15.existsSync)(target)) {
12843
+ if ((0, import_node_fs16.existsSync)(target)) {
12600
12844
  const currentHash = await sha256File(target);
12601
12845
  if (recorded && currentHash === recorded.sha256) {
12602
12846
  if (!fresh && !reapply) {
@@ -12613,9 +12857,9 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12613
12857
  }
12614
12858
  if (reapply && !fresh) {
12615
12859
  if (!recorded.raw) raw = await (0, import_promises12.readFile)(target);
12616
- log(` reapply ${import_node_path19.default.relative(process.cwd(), target)}`);
12860
+ log(` reapply ${import_node_path20.default.relative(process.cwd(), target)}`);
12617
12861
  } else {
12618
- log(` changed ${import_node_path19.default.relative(process.cwd(), target)} (upstream)`);
12862
+ log(` changed ${import_node_path20.default.relative(process.cwd(), target)} (upstream)`);
12619
12863
  }
12620
12864
  } else {
12621
12865
  const superseded = (entry.supersededOutputs ?? []).find(
@@ -12643,7 +12887,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12643
12887
  }
12644
12888
  }
12645
12889
  log(
12646
- ` replace ${import_node_path19.default.relative(process.cwd(), target)}` + (opts.force ? " (--force)" : " (previous tracked generation)")
12890
+ ` replace ${import_node_path20.default.relative(process.cwd(), target)}` + (opts.force ? " (--force)" : " (previous tracked generation)")
12647
12891
  );
12648
12892
  }
12649
12893
  }
@@ -12652,7 +12896,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12652
12896
  } else if (!raw && recorded && cacheDir) {
12653
12897
  if (!reapply) final = await readCachedMedia(cacheDir, recorded.sha256, expectedMediaType);
12654
12898
  if (!final && recordedRaw) raw = await readCachedMedia(cacheDir, recordedRaw, expectedMediaType);
12655
- if (final || raw) log(` cached ${import_node_path19.default.relative(process.cwd(), target)}`);
12899
+ if (final || raw) log(` cached ${import_node_path20.default.relative(process.cwd(), target)}`);
12656
12900
  }
12657
12901
  if (!final && !raw) {
12658
12902
  if (!source.url) {
@@ -12679,11 +12923,11 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12679
12923
  buf = processed.bytes;
12680
12924
  if (processed.changed) {
12681
12925
  rawHash = sha256(raw);
12682
- log(` snapped ${import_node_path19.default.relative(process.cwd(), target)} to ${wanted.palette.colors.length} colours`);
12926
+ log(` snapped ${import_node_path20.default.relative(process.cwd(), target)} to ${wanted.palette.colors.length} colours`);
12683
12927
  }
12684
12928
  }
12685
12929
  if (cacheDir) await cacheMedia(cacheDir, buf, mediaType);
12686
- await (0, import_promises12.mkdir)(import_node_path19.default.dirname(target), { recursive: true });
12930
+ await (0, import_promises12.mkdir)(import_node_path20.default.dirname(target), { recursive: true });
12687
12931
  const tmp = `${target}.pixelkiln.tmp`;
12688
12932
  await (0, import_promises12.writeFile)(tmp, buf);
12689
12933
  outputs.push({
@@ -12697,7 +12941,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12697
12941
  await saveLock(lockPath, lock);
12698
12942
  await (0, import_promises12.rename)(tmp, target);
12699
12943
  wrote = true;
12700
- log(` wrote ${import_node_path19.default.relative(process.cwd(), target)}`);
12944
+ log(` wrote ${import_node_path20.default.relative(process.cwd(), target)}`);
12701
12945
  }
12702
12946
  if (opts.refresh && !wrote) {
12703
12947
  if (rotated) {
@@ -12741,8 +12985,8 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12741
12985
  return result;
12742
12986
  }
12743
12987
  async function readCachedMedia(cacheDir, hash, mediaType) {
12744
- const file = import_node_path19.default.join(cacheDir, cacheFileName(hash, mediaType));
12745
- if (!(0, import_node_fs15.existsSync)(file)) return null;
12988
+ const file = import_node_path20.default.join(cacheDir, cacheFileName(hash, mediaType));
12989
+ if (!(0, import_node_fs16.existsSync)(file)) return null;
12746
12990
  try {
12747
12991
  const buf = await (0, import_promises12.readFile)(file);
12748
12992
  if (sha256(buf) !== hash) return null;
@@ -12755,7 +12999,7 @@ async function readCachedMedia(cacheDir, hash, mediaType) {
12755
12999
  async function cacheMedia(cacheDir, buf, mediaType, knownHash) {
12756
13000
  const hash = knownHash ?? sha256(buf);
12757
13001
  validateMedia(buf, mediaType);
12758
- const file = import_node_path19.default.join(cacheDir, cacheFileName(hash, mediaType));
13002
+ const file = import_node_path20.default.join(cacheDir, cacheFileName(hash, mediaType));
12759
13003
  if (await readCachedMedia(cacheDir, hash, mediaType)) return;
12760
13004
  await (0, import_promises12.mkdir)(cacheDir, { recursive: true });
12761
13005
  const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
@@ -12763,7 +13007,7 @@ async function cacheMedia(cacheDir, buf, mediaType, knownHash) {
12763
13007
  try {
12764
13008
  await (0, import_promises12.rename)(tmp, file);
12765
13009
  } catch (err) {
12766
- if (!(0, import_node_fs15.existsSync)(file)) throw err;
13010
+ if (!(0, import_node_fs16.existsSync)(file)) throw err;
12767
13011
  } finally {
12768
13012
  await (0, import_promises12.rm)(tmp, { force: true });
12769
13013
  }
@@ -12882,10 +13126,10 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
12882
13126
  }
12883
13127
  for (let i = 0; i < entry.outputs.length; i++) {
12884
13128
  const file = currentEntryOutputPath(entry, spec, i);
12885
- if (!(0, import_node_fs16.existsSync)(file)) continue;
13129
+ if (!(0, import_node_fs17.existsSync)(file)) continue;
12886
13130
  if (await sha256File(file) !== entry.outputs[i].sha256 && !opts.force) {
12887
13131
  throw new OverwriteRefusedError(
12888
- `${import_node_path20.default.relative(process.cwd(), file)} was changed after download; keep it with pixelkiln edit, or pass --force to replace it`,
13132
+ `${import_node_path21.default.relative(process.cwd(), file)} was changed after download; keep it with pixelkiln edit, or pass --force to replace it`,
12889
13133
  [file]
12890
13134
  );
12891
13135
  }
@@ -12937,11 +13181,11 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
12937
13181
 
12938
13182
  // src/pipeline/cache-health.ts
12939
13183
  async function inspectCaches(lock, lockPath, options = {}) {
12940
- if (options.prune && !(0, import_node_fs17.existsSync)(lockPath)) {
12941
- throw new Error(`Refusing to prune without an existing lockfile at ${import_node_path21.default.resolve(lockPath)}`);
13184
+ if (options.prune && !(0, import_node_fs18.existsSync)(lockPath)) {
13185
+ throw new Error(`Refusing to prune without an existing lockfile at ${import_node_path22.default.resolve(lockPath)}`);
12942
13186
  }
12943
- const contentDir = import_node_path21.default.resolve(import_node_path21.default.dirname(lockPath), ".pixelkiln", "cache");
12944
- const remotePath = import_node_path21.default.resolve(cachePathFor(lockPath));
13187
+ const contentDir = import_node_path22.default.resolve(import_node_path22.default.dirname(lockPath), ".pixelkiln", "cache");
13188
+ const remotePath = import_node_path22.default.resolve(cachePathFor(lockPath));
12945
13189
  const referenced = referencedHashes(lock);
12946
13190
  let content = await inspectContentCache(contentDir, referenced);
12947
13191
  let remoteHashes = await inspectRemoteHashCache(remotePath);
@@ -12953,7 +13197,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
12953
13197
  ]);
12954
13198
  for (const name of names) {
12955
13199
  try {
12956
- await (0, import_promises13.rm)(import_node_path21.default.join(contentDir, name), { force: true });
13200
+ await (0, import_promises13.rm)(import_node_path22.default.join(contentDir, name), { force: true });
12957
13201
  removed.contentFiles++;
12958
13202
  } catch {
12959
13203
  }
@@ -12980,7 +13224,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
12980
13224
  async function inspectContentCache(contentDir, referenced) {
12981
13225
  const report = {
12982
13226
  path: contentDir,
12983
- exists: (0, import_node_fs17.existsSync)(contentDir),
13227
+ exists: (0, import_node_fs18.existsSync)(contentDir),
12984
13228
  files: 0,
12985
13229
  bytes: 0,
12986
13230
  valid: 0,
@@ -13001,7 +13245,7 @@ async function inspectContentCache(contentDir, referenced) {
13001
13245
  continue;
13002
13246
  }
13003
13247
  report.files++;
13004
- const file = import_node_path21.default.join(contentDir, entry.name);
13248
+ const file = import_node_path22.default.join(contentDir, entry.name);
13005
13249
  const mediaType = mediaTypeFromExtension(entry.name);
13006
13250
  const expected = mediaType ? entry.name.slice(0, -4) : "";
13007
13251
  let bytes;
@@ -13044,7 +13288,7 @@ async function inspectContentCache(contentDir, referenced) {
13044
13288
  async function inspectRemoteHashCache(remotePath) {
13045
13289
  const report = {
13046
13290
  path: remotePath,
13047
- exists: (0, import_node_fs17.existsSync)(remotePath),
13291
+ exists: (0, import_node_fs18.existsSync)(remotePath),
13048
13292
  entries: 0,
13049
13293
  valid: 0,
13050
13294
  invalidIds: [],
@@ -13068,20 +13312,20 @@ async function inspectRemoteHashCache(remotePath) {
13068
13312
  }
13069
13313
 
13070
13314
  // src/pipeline/pack.ts
13071
- var import_node_fs18 = require("fs");
13072
- var import_node_path22 = __toESM(require("path"), 1);
13315
+ var import_node_fs19 = require("fs");
13316
+ var import_node_path23 = __toESM(require("path"), 1);
13073
13317
  var DEFAULT_FRAME_SET_FPS = 12;
13074
13318
  function resolvePackInputs(raw, inputsFilePath) {
13075
13319
  if (!Array.isArray(raw) || !raw.length) {
13076
13320
  throw new Error("--inputs must be a non-empty JSON array of { id, path }");
13077
13321
  }
13078
- const baseDir = import_node_path22.default.dirname(import_node_path22.default.resolve(inputsFilePath));
13322
+ const baseDir = import_node_path23.default.dirname(import_node_path23.default.resolve(inputsFilePath));
13079
13323
  return raw.map((entry, i) => {
13080
13324
  const e = entry;
13081
13325
  if (typeof e.id !== "string" || typeof e.path !== "string") {
13082
13326
  throw new Error(`--inputs[${i}] needs string "id" and "path"`);
13083
13327
  }
13084
- return { id: e.id, path: import_node_path22.default.resolve(baseDir, e.path) };
13328
+ return { id: e.id, path: import_node_path23.default.resolve(baseDir, e.path) };
13085
13329
  });
13086
13330
  }
13087
13331
  function packSprites(inputs, options = {}) {
@@ -13102,7 +13346,7 @@ function packSprites(inputs, options = {}) {
13102
13346
  for (const input of ordered) {
13103
13347
  let sourceHash = null;
13104
13348
  try {
13105
- const bytes = (0, import_node_fs18.readFileSync)(input.path);
13349
+ const bytes = (0, import_node_fs19.readFileSync)(input.path);
13106
13350
  sourceHash = sha256(bytes);
13107
13351
  const png = decodePng(bytes);
13108
13352
  sprites.push({ id: input.id, width: png.width, height: png.height, pixels: png.pixels });
@@ -13165,7 +13409,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
13165
13409
  }
13166
13410
  const inputs2 = Object.entries(options.sourceOverrides).map(([id, source]) => ({
13167
13411
  id,
13168
- path: import_node_path22.default.resolve(source)
13412
+ path: import_node_path23.default.resolve(source)
13169
13413
  }));
13170
13414
  if (!inputs2.length) throw new Error(`No approved quality outputs for style "${styleId}".`);
13171
13415
  const packed2 = packSprites(inputs2, options);
@@ -13189,7 +13433,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
13189
13433
  let outputs = resolveEntryOutputs(entry, id, manifestDir);
13190
13434
  const source = sources[id];
13191
13435
  if (source && outputs.length <= 1) {
13192
- inputs.push({ id, path: import_node_path22.default.resolve(manifestDir, source) });
13436
+ inputs.push({ id, path: import_node_path23.default.resolve(manifestDir, source) });
13193
13437
  continue;
13194
13438
  }
13195
13439
  if (source && sourceIsStem(source, entry, manifestDir)) {
@@ -13223,7 +13467,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
13223
13467
  }
13224
13468
  }
13225
13469
  for (const [id, source] of Object.entries(sources)) {
13226
- if (!locked.has(id)) inputs.push({ id, path: import_node_path22.default.resolve(manifestDir, source) });
13470
+ if (!locked.has(id)) inputs.push({ id, path: import_node_path23.default.resolve(manifestDir, source) });
13227
13471
  }
13228
13472
  if (!inputs.length) {
13229
13473
  throw new Error(`No readable sprites for style "${styleId}"; ${noOutput.length} skipped.`);
@@ -13260,7 +13504,7 @@ function mountSprites(placements, options) {
13260
13504
  for (const p of placements) {
13261
13505
  let sourceHash = null;
13262
13506
  try {
13263
- const bytes = (0, import_node_fs18.readFileSync)(p.path);
13507
+ const bytes = (0, import_node_fs19.readFileSync)(p.path);
13264
13508
  sourceHash = sha256(bytes);
13265
13509
  const png = decodePng(bytes);
13266
13510
  if (png.width > cellWidth || png.height > cellHeight) {
@@ -13331,7 +13575,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
13331
13575
  const source = sources[id];
13332
13576
  const entry = lock.entries[`${styleId}/${id}`];
13333
13577
  if (source && !(entry && sourceIsStem(source, entry, manifestDir))) {
13334
- placements.push({ id, path: import_node_path22.default.resolve(manifestDir, source), cell });
13578
+ placements.push({ id, path: import_node_path23.default.resolve(manifestDir, source), cell });
13335
13579
  continue;
13336
13580
  }
13337
13581
  if (source && entry) {
@@ -13364,7 +13608,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
13364
13608
  `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}`
13365
13609
  );
13366
13610
  }
13367
- const basePng = mount.base ? (0, import_node_fs18.readFileSync)(import_node_path22.default.resolve(manifestDir, mount.base)) : void 0;
13611
+ const basePng = mount.base ? (0, import_node_fs19.readFileSync)(import_node_path23.default.resolve(manifestDir, mount.base)) : void 0;
13368
13612
  const mounted = mountSprites(placements, {
13369
13613
  cellWidth: mount.cellWidth,
13370
13614
  cellHeight: mount.cellHeight,
@@ -13374,7 +13618,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
13374
13618
  ...mounted,
13375
13619
  sources: mount.base ? [{
13376
13620
  id: "$base",
13377
- path: import_node_path22.default.resolve(manifestDir, mount.base),
13621
+ path: import_node_path23.default.resolve(manifestDir, mount.base),
13378
13622
  sha256: sha256(basePng),
13379
13623
  included: true
13380
13624
  }, ...mounted.sources] : mounted.sources,
@@ -13966,6 +14210,10 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
13966
14210
  ...spec.revision.keypointsSha256 ? { keypointsSha256: spec.revision.keypointsSha256 } : {},
13967
14211
  ...spec.revision.skeletonTemplate ? { skeletonTemplate: spec.revision.skeletonTemplate } : {},
13968
14212
  ...spec.revision.description ? { description: spec.revision.description } : {},
14213
+ ...spec.revision.removalTask ? { removalTask: spec.revision.removalTask } : {},
14214
+ ...spec.revision.driftThreshold == null ? {} : { driftThreshold: spec.revision.driftThreshold },
14215
+ ...spec.revision.subjectDescription ? { subjectDescription: spec.revision.subjectDescription } : {},
14216
+ ...spec.revision.initialPose ? { initialPose: spec.revision.initialPose } : {},
13969
14217
  ...spec.revision.engine ? { engine: spec.revision.engine } : {}
13970
14218
  } : null,
13971
14219
  outfit: spec.character?.kind === "outfit" && outfitParentEntry ? {
@@ -14180,9 +14428,9 @@ async function poll(provider, lock, lockPath, opts = {}) {
14180
14428
  }
14181
14429
 
14182
14430
  // src/pipeline/doctor.ts
14183
- var import_node_fs19 = require("fs");
14431
+ var import_node_fs20 = require("fs");
14184
14432
  var import_promises14 = require("fs/promises");
14185
- var import_node_path23 = __toESM(require("path"), 1);
14433
+ var import_node_path24 = __toESM(require("path"), 1);
14186
14434
  async function doctor(loaded, specs, lock, lockPath, opts = {}) {
14187
14435
  const checks = [];
14188
14436
  const add = (id, level, message6) => checks.push({ id, level, message: message6 });
@@ -14193,14 +14441,14 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
14193
14441
  );
14194
14442
  add("lock", "ok", `${Object.keys(lock.entries).length} valid v2 lock entr(ies)`);
14195
14443
  const dirs = [...new Set(specs.flatMap((spec) => [
14196
- import_node_path23.default.dirname(spec.outFile),
14197
- ...spec.quality ? [import_node_path23.default.dirname(spec.quality.outFile)] : []
14444
+ import_node_path24.default.dirname(spec.outFile),
14445
+ ...spec.quality ? [import_node_path24.default.dirname(spec.quality.outFile)] : []
14198
14446
  ]))];
14199
14447
  const unwritable = [];
14200
14448
  for (const dir of dirs) {
14201
14449
  const ancestor = await nearestExistingDirectory(dir);
14202
14450
  try {
14203
- await (0, import_promises14.access)(ancestor, import_node_fs19.constants.W_OK);
14451
+ await (0, import_promises14.access)(ancestor, import_node_fs20.constants.W_OK);
14204
14452
  } catch {
14205
14453
  unwritable.push(dir);
14206
14454
  }
@@ -14217,7 +14465,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
14217
14465
  for (let index = 0; index < entry.outputs.length; index++) {
14218
14466
  const output = entry.outputs[index];
14219
14467
  const spec = specByKey.get(key);
14220
- const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, import_node_path23.default.dirname(lockPath));
14468
+ const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, import_node_path24.default.dirname(lockPath));
14221
14469
  const owner = outputOwners.get(absolute);
14222
14470
  if (owner && owner !== key) duplicateOutputs.push(`${output.path} (${owner}, ${key})`);
14223
14471
  outputOwners.set(absolute, key);
@@ -14232,7 +14480,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
14232
14480
  } else {
14233
14481
  add("lock-outputs", "ok", `${outputOwners.size} recorded output path(s) are uniquely owned`);
14234
14482
  }
14235
- const cacheDir = import_node_path23.default.join(import_node_path23.default.dirname(lockPath), ".pixelkiln", "cache");
14483
+ const cacheDir = import_node_path24.default.join(import_node_path24.default.dirname(lockPath), ".pixelkiln", "cache");
14236
14484
  const stranded = [];
14237
14485
  const invalidState = [];
14238
14486
  const staleMapUrls = [];
@@ -14248,11 +14496,11 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
14248
14496
  const hasSource = Boolean(entry.sourceUrl || entry.sourceUrls?.length);
14249
14497
  let hasCache = false;
14250
14498
  for (const output of entry.outputs) {
14251
- const cached = import_node_path23.default.join(
14499
+ const cached = import_node_path24.default.join(
14252
14500
  cacheDir,
14253
14501
  cacheFileName(output.sha256, output.mediaType ?? MediaType.PNG)
14254
14502
  );
14255
- if ((0, import_node_fs19.existsSync)(cached) && await sha256File(cached) === output.sha256) {
14503
+ if ((0, import_node_fs20.existsSync)(cached) && await sha256File(cached) === output.sha256) {
14256
14504
  hasCache = true;
14257
14505
  break;
14258
14506
  }
@@ -14348,16 +14596,16 @@ async function nearestExistingDirectory(start) {
14348
14596
  if ((await (0, import_promises14.stat)(current)).isDirectory()) return current;
14349
14597
  } catch {
14350
14598
  }
14351
- const parent = import_node_path23.default.dirname(current);
14599
+ const parent = import_node_path24.default.dirname(current);
14352
14600
  if (parent === current) return current;
14353
14601
  current = parent;
14354
14602
  }
14355
14603
  }
14356
14604
 
14357
14605
  // src/pipeline/adopt.ts
14358
- var import_node_fs20 = require("fs");
14606
+ var import_node_fs21 = require("fs");
14359
14607
  var import_promises15 = require("fs/promises");
14360
- var import_node_path24 = __toESM(require("path"), 1);
14608
+ var import_node_path25 = __toESM(require("path"), 1);
14361
14609
  async function adopt(provider, specs, lock, lockPath, opts = {}) {
14362
14610
  const log = opts.onProgress ?? (() => {
14363
14611
  });
@@ -14368,7 +14616,7 @@ async function adopt(provider, specs, lock, lockPath, opts = {}) {
14368
14616
  const localByHash = /* @__PURE__ */ new Map();
14369
14617
  const unmatchedLocal = [];
14370
14618
  for (const spec of specs) {
14371
- if (!(0, import_node_fs20.existsSync)(spec.outFile)) {
14619
+ if (!(0, import_node_fs21.existsSync)(spec.outFile)) {
14372
14620
  unmatchedLocal.push(`${spec.styleId}/${spec.assetId} (no file at ${spec.outFile})`);
14373
14621
  continue;
14374
14622
  }
@@ -14490,13 +14738,13 @@ async function tagAdopted(provider, specs, lock, opts = {}) {
14490
14738
  }
14491
14739
 
14492
14740
  // src/pick/server.ts
14493
- var import_node_fs22 = require("fs");
14741
+ var import_node_fs23 = require("fs");
14494
14742
  var import_promises18 = require("fs/promises");
14495
14743
 
14496
14744
  // src/pipeline/init.ts
14497
14745
  var import_promises16 = require("fs/promises");
14498
- var import_node_fs21 = require("fs");
14499
- var import_node_path25 = __toESM(require("path"), 1);
14746
+ var import_node_fs22 = require("fs");
14747
+ var import_node_path26 = __toESM(require("path"), 1);
14500
14748
  function pngSize(buf) {
14501
14749
  if (buf.length < 24) return null;
14502
14750
  const isPng = buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834;
@@ -14510,7 +14758,7 @@ function slugify(value) {
14510
14758
  async function walk(dir, exclude) {
14511
14759
  const out = [];
14512
14760
  for (const entry of await (0, import_promises16.readdir)(dir, { withFileTypes: true })) {
14513
- const full = import_node_path25.default.join(dir, entry.name);
14761
+ const full = import_node_path26.default.join(dir, entry.name);
14514
14762
  if (exclude.some((x) => full.includes(x))) continue;
14515
14763
  if (entry.isDirectory()) out.push(...await walk(full, exclude));
14516
14764
  else if (entry.name.toLowerCase().endsWith(".png")) out.push(full);
@@ -14529,9 +14777,9 @@ async function scanAssets(root, opts = {}) {
14529
14777
  skipped.push(`${file} (not a readable PNG)`);
14530
14778
  continue;
14531
14779
  }
14532
- const rel = import_node_path25.default.relative(root, file);
14533
- const category = import_node_path25.default.dirname(rel) === "." ? "" : import_node_path25.default.dirname(rel);
14534
- let id = slugify(import_node_path25.default.basename(rel));
14780
+ const rel = import_node_path26.default.relative(root, file);
14781
+ const category = import_node_path26.default.dirname(rel) === "." ? "" : import_node_path26.default.dirname(rel);
14782
+ let id = slugify(import_node_path26.default.basename(rel));
14535
14783
  if (seen.has(id)) {
14536
14784
  const n = seen.get(id) + 1;
14537
14785
  seen.set(id, n);
@@ -15080,7 +15328,7 @@ async function currentArt(entry, spec) {
15080
15328
  const previous = primaryOutput({ ...entry, outputs: entry.supersededOutputs });
15081
15329
  if (!previous) return null;
15082
15330
  const file = resolveOutputPath(previous.path, spec.root);
15083
- if (!(0, import_node_fs22.existsSync)(file)) return null;
15331
+ if (!(0, import_node_fs23.existsSync)(file)) return null;
15084
15332
  const contentType = previous.mediaType ?? mediaTypeFromExtension(file);
15085
15333
  if (!contentType) return null;
15086
15334
  let width = entry.width;
@@ -15254,15 +15502,15 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
15254
15502
  }
15255
15503
 
15256
15504
  // src/gallery/snapshot.ts
15257
- var import_node_crypto6 = require("crypto");
15258
- var import_node_fs27 = require("fs");
15259
- var import_promises25 = require("fs/promises");
15260
- var import_node_path32 = __toESM(require("path"), 1);
15505
+ var import_node_crypto7 = require("crypto");
15506
+ var import_node_fs29 = require("fs");
15507
+ var import_promises26 = require("fs/promises");
15508
+ var import_node_path34 = __toESM(require("path"), 1);
15261
15509
 
15262
15510
  // src/workspace.ts
15263
15511
  var import_promises19 = require("fs/promises");
15264
- var import_node_fs23 = require("fs");
15265
- var import_node_path26 = __toESM(require("path"), 1);
15512
+ var import_node_fs24 = require("fs");
15513
+ var import_node_path27 = __toESM(require("path"), 1);
15266
15514
  var import_zod7 = require("zod");
15267
15515
  var WorkspaceProjectSchema = import_zod7.z.object({
15268
15516
  id: import_zod7.z.string().min(1),
@@ -15287,7 +15535,7 @@ ${parsed.error.issues.slice(0, 5).map((i) => ` ${i.path.join(".")}: ${i.message
15287
15535
  );
15288
15536
  }
15289
15537
  async function loadWorkspace(workspacePath) {
15290
- if (!(0, import_node_fs23.existsSync)(workspacePath)) return { version: 1, projects: [] };
15538
+ if (!(0, import_node_fs24.existsSync)(workspacePath)) return { version: 1, projects: [] };
15291
15539
  let raw;
15292
15540
  try {
15293
15541
  raw = JSON.parse(await (0, import_promises19.readFile)(workspacePath, "utf8"));
@@ -15304,7 +15552,7 @@ async function saveWorkspace(workspacePath, ws) {
15304
15552
  version: 1,
15305
15553
  projects: [...ws.projects].sort((a, b) => a.id.localeCompare(b.id))
15306
15554
  };
15307
- await (0, import_promises19.mkdir)(import_node_path26.default.dirname(import_node_path26.default.resolve(workspacePath)), { recursive: true });
15555
+ await (0, import_promises19.mkdir)(import_node_path27.default.dirname(import_node_path27.default.resolve(workspacePath)), { recursive: true });
15308
15556
  const tmp = `${workspacePath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
15309
15557
  try {
15310
15558
  await (0, import_promises19.writeFile)(tmp, JSON.stringify(sorted, null, 2) + "\n");
@@ -15314,12 +15562,12 @@ async function saveWorkspace(workspacePath, ws) {
15314
15562
  }
15315
15563
  }
15316
15564
  function toPortablePath(dir, absolute) {
15317
- return import_node_path26.default.relative(dir, absolute).split(import_node_path26.default.sep).join("/");
15565
+ return import_node_path27.default.relative(dir, absolute).split(import_node_path27.default.sep).join("/");
15318
15566
  }
15319
15567
  function resolveProject(dir, project) {
15320
15568
  return {
15321
- manifestPath: import_node_path26.default.resolve(dir, project.manifest.split("/").join(import_node_path26.default.sep)),
15322
- lockPath: import_node_path26.default.resolve(dir, project.lock.split("/").join(import_node_path26.default.sep))
15569
+ manifestPath: import_node_path27.default.resolve(dir, project.manifest.split("/").join(import_node_path27.default.sep)),
15570
+ lockPath: import_node_path27.default.resolve(dir, project.lock.split("/").join(import_node_path27.default.sep))
15323
15571
  };
15324
15572
  }
15325
15573
  function validateWorkspace(ws, dir) {
@@ -15332,21 +15580,21 @@ function validateWorkspace(ws, dir) {
15332
15580
  const { manifestPath, lockPath } = resolveProject(dir, project);
15333
15581
  lockOwners.set(lockPath, [...lockOwners.get(lockPath) ?? [], project.id]);
15334
15582
  manifestOwners.set(manifestPath, [...manifestOwners.get(manifestPath) ?? [], project.id]);
15335
- if (import_node_path26.default.isAbsolute(project.manifest) || import_node_path26.default.isAbsolute(project.lock)) {
15583
+ if (import_node_path27.default.isAbsolute(project.manifest) || import_node_path27.default.isAbsolute(project.lock)) {
15336
15584
  diagnostics.push({
15337
15585
  id: "absolute-path",
15338
15586
  level: "warning",
15339
15587
  message: `project "${project.id}" stores an absolute path; the catalog will not resolve correctly if this tree is cloned or moved elsewhere`
15340
15588
  });
15341
15589
  }
15342
- if (!(0, import_node_fs23.existsSync)(manifestPath)) {
15590
+ if (!(0, import_node_fs24.existsSync)(manifestPath)) {
15343
15591
  diagnostics.push({
15344
15592
  id: "missing-manifest",
15345
15593
  level: "error",
15346
15594
  message: `project "${project.id}" manifest not found: ${manifestPath}`
15347
15595
  });
15348
15596
  }
15349
- if (!(0, import_node_fs23.existsSync)(lockPath)) {
15597
+ if (!(0, import_node_fs24.existsSync)(lockPath)) {
15350
15598
  diagnostics.push({
15351
15599
  id: "missing-lock",
15352
15600
  level: "error",
@@ -15393,15 +15641,16 @@ function validateWorkspace(ws, dir) {
15393
15641
  }
15394
15642
 
15395
15643
  // src/gallery/edit.ts
15396
- var import_promises24 = require("fs/promises");
15397
- var import_node_path31 = __toESM(require("path"), 1);
15398
- var import_zod12 = require("zod");
15644
+ var import_promises25 = require("fs/promises");
15645
+ var import_node_path33 = __toESM(require("path"), 1);
15646
+ var import_zod13 = require("zod");
15399
15647
 
15400
15648
  // src/manifest-edit.ts
15401
15649
  var import_promises20 = require("fs/promises");
15402
- var import_node_path27 = __toESM(require("path"), 1);
15650
+ var import_node_path28 = __toESM(require("path"), 1);
15403
15651
  var import_zod8 = require("zod");
15404
15652
  var HexSha = import_zod8.z.string().regex(/^[0-9a-f]{64}$/);
15653
+ var ProjectFileSchema = import_zod8.z.string().min(1).refine((file) => !import_node_path28.default.isAbsolute(file) && !file.split(/[\\/]/).includes(".."), "must be a path inside the project");
15405
15654
  var AssetPatchSchema = import_zod8.z.object({
15406
15655
  prompt: import_zod8.z.string().optional(),
15407
15656
  /** Per-style override for the style being viewed; null removes it. */
@@ -15450,10 +15699,11 @@ var NewAssetSchema = import_zod8.z.object({
15450
15699
  styles: import_zod8.z.array(import_zod8.z.string().min(1)).optional(),
15451
15700
  /**
15452
15701
  * A controlled regeneration of another asset in the same manifest,
15453
- * instead of a fresh generation. `inpaint` needs a mask upload and
15454
- * `interpolate` an ending-keyframe image the gallery does not offer, so
15455
- * those two are hand-edited; every other mode can be created here
15456
- * (`animate-skeleton` after the gallery writes its keypoints file).
15702
+ * instead of a fresh generation. Every mode but `outpaint` can be
15703
+ * created here: `inpaint` names a mask file the gallery's mask editor
15704
+ * wrote first, `interpolate` names an existing image as its ending
15705
+ * keyframe, and `animate-skeleton` names the keypoints file the gallery
15706
+ * wrote first.
15457
15707
  */
15458
15708
  revision: import_zod8.z.discriminatedUnion("mode", [
15459
15709
  import_zod8.z.object({
@@ -15470,15 +15720,38 @@ var NewAssetSchema = import_zod8.z.object({
15470
15720
  dithering: RevisionDitheringSchema.optional(),
15471
15721
  ditheringStrength: import_zod8.z.number().min(0).max(10).optional()
15472
15722
  }).strict(),
15723
+ import_zod8.z.object({
15724
+ mode: import_zod8.z.literal("inpaint"),
15725
+ from: import_zod8.z.string().min(1),
15726
+ /** Black-and-white PNG the same size as the parent; white is repainted. */
15727
+ mask: ProjectFileSchema
15728
+ }).strict(),
15729
+ import_zod8.z.object({
15730
+ mode: import_zod8.z.literal("interpolate"),
15731
+ from: import_zod8.z.string().min(1),
15732
+ /** The ending keyframe: an image already in the project, the parent's size. */
15733
+ lastFrame: ProjectFileSchema,
15734
+ fps: import_zod8.z.number().int().min(1).max(60).optional()
15735
+ }).strict(),
15473
15736
  import_zod8.z.object({ mode: import_zod8.z.literal("correct-pixelart"), from: import_zod8.z.string().min(1), strength: import_zod8.z.number().min(0).max(1).optional() }).strict(),
15737
+ import_zod8.z.object({
15738
+ mode: import_zod8.z.literal("remove-background"),
15739
+ from: import_zod8.z.string().min(1),
15740
+ removalTask: import_zod8.z.enum(["simple", "complex"]).optional(),
15741
+ /** A short description of the foreground, sent as PixelLab's text hint. */
15742
+ description: import_zod8.z.string().min(1).optional()
15743
+ }).strict(),
15474
15744
  import_zod8.z.object({
15475
15745
  mode: import_zod8.z.enum(["animate", "animate-pixminimax"]),
15476
15746
  from: import_zod8.z.string().min(1),
15477
15747
  frames: import_zod8.z.number().int().min(4).max(40).refine((n) => n % 2 === 0, "frames must be even").optional(),
15478
15748
  fps: import_zod8.z.number().int().min(1).max(60).optional(),
15479
15749
  enhancePrompt: import_zod8.z.boolean().optional(),
15480
- /** `animate-pixminimax` only; the loader refuses it on `animate`. */
15481
- direction: CharacterDirectionSchema.optional()
15750
+ driftThreshold: import_zod8.z.number().min(0).optional(),
15751
+ /** `animate-pixminimax` only; the loader refuses these on `animate`. */
15752
+ direction: CharacterDirectionSchema.optional(),
15753
+ subjectDescription: import_zod8.z.string().min(1).max(300).optional(),
15754
+ initialPose: import_zod8.z.string().min(1).max(300).optional()
15482
15755
  }).strict(),
15483
15756
  import_zod8.z.object({
15484
15757
  mode: import_zod8.z.literal("edit-animation"),
@@ -15796,7 +16069,7 @@ function applyEdit(raw, edit) {
15796
16069
  }
15797
16070
  }
15798
16071
  async function withCandidate(manifestPath, edit, use, unchanged) {
15799
- const absolute = import_node_path27.default.resolve(manifestPath);
16072
+ const absolute = import_node_path28.default.resolve(manifestPath);
15800
16073
  const original = await (0, import_promises20.readFile)(absolute, "utf8");
15801
16074
  if (sha256(original) !== edit.expectedSha256) throw new ManifestDriftError(absolute);
15802
16075
  let raw;
@@ -15810,9 +16083,9 @@ async function withCandidate(manifestPath, edit, use, unchanged) {
15810
16083
  applyEdit(raw, edit);
15811
16084
  const next = serializeLike(original, raw);
15812
16085
  if (next === original) return unchanged(absolute, original);
15813
- const tmp = import_node_path27.default.join(
15814
- import_node_path27.default.dirname(absolute),
15815
- `.${import_node_path27.default.basename(absolute)}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`
16086
+ const tmp = import_node_path28.default.join(
16087
+ import_node_path28.default.dirname(absolute),
16088
+ `.${import_node_path28.default.basename(absolute)}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`
15816
16089
  );
15817
16090
  try {
15818
16091
  await (0, import_promises20.writeFile)(tmp, next);
@@ -15841,18 +16114,18 @@ async function applyManifestEdit(manifestPath, edit) {
15841
16114
  }
15842
16115
 
15843
16116
  // src/pipeline/hand-edit.ts
15844
- var import_node_fs24 = require("fs");
16117
+ var import_node_fs25 = require("fs");
15845
16118
  var import_promises21 = require("fs/promises");
15846
- var import_node_path28 = __toESM(require("path"), 1);
16119
+ var import_node_path29 = __toESM(require("path"), 1);
15847
16120
  var import_zod9 = require("zod");
15848
16121
  var HAND_EDIT_DIR = "edits";
15849
16122
  function handEditPath(loaded, spec) {
15850
16123
  const style = loaded.manifest.styles[spec.styleId];
15851
- const outDir = import_node_path28.default.resolve(loaded.root, style?.outDir ?? ".");
15852
- const relative = import_node_path28.default.relative(outDir, spec.outFile);
15853
- const inside = relative && !relative.startsWith("..") && !import_node_path28.default.isAbsolute(relative);
15854
- const file = inside ? relative : import_node_path28.default.basename(spec.outFile);
15855
- return import_node_path28.default.join(outDir, HAND_EDIT_DIR, file.replace(/\.gif$/i, ".png"));
16124
+ const outDir = import_node_path29.default.resolve(loaded.root, style?.outDir ?? ".");
16125
+ const relative = import_node_path29.default.relative(outDir, spec.outFile);
16126
+ const inside = relative && !relative.startsWith("..") && !import_node_path29.default.isAbsolute(relative);
16127
+ const file = inside ? relative : import_node_path29.default.basename(spec.outFile);
16128
+ return import_node_path29.default.join(outDir, HAND_EDIT_DIR, file.replace(/\.gif$/i, ".png"));
15856
16129
  }
15857
16130
  function handEditBases(spec, lock) {
15858
16131
  const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
@@ -15865,10 +16138,10 @@ function handEditBases(spec, lock) {
15865
16138
  }));
15866
16139
  if (members.some((member) => member.mediaType && member.mediaType !== "image/png")) return null;
15867
16140
  if (entry.outputs.length === 1 && !primaryOutput(entry)) return null;
15868
- if (!members.every((member) => (0, import_node_fs24.existsSync)(member.path))) return null;
16141
+ if (!members.every((member) => (0, import_node_fs25.existsSync)(member.path))) return null;
15869
16142
  return members.map(({ role, path: file }) => ({ role, path: file }));
15870
16143
  }
15871
- return (0, import_node_fs24.existsSync)(spec.outFile) ? [{ role: null, path: spec.outFile }] : null;
16144
+ return (0, import_node_fs25.existsSync)(spec.outFile) ? [{ role: null, path: spec.outFile }] : null;
15872
16145
  }
15873
16146
  function handEditBase(spec, lock) {
15874
16147
  const bases = handEditBases(spec, lock);
@@ -15898,18 +16171,18 @@ async function startHandEdit(loaded, lock, spec, opts = {}) {
15898
16171
  `${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`
15899
16172
  );
15900
16173
  }
15901
- const existing = spec.source ? import_node_path28.default.resolve(loaded.root, spec.source) : null;
16174
+ const existing = spec.source ? import_node_path29.default.resolve(loaded.root, spec.source) : null;
15902
16175
  const editPath = existing ?? handEditPath(loaded, spec);
15903
16176
  const members = handEditMembers(loaded, spec, lock, editPath);
15904
16177
  let created = false;
15905
16178
  for (const member of members) {
15906
- if ((0, import_node_fs24.existsSync)(member.path)) continue;
16179
+ if ((0, import_node_fs25.existsSync)(member.path)) continue;
15907
16180
  if (!member.base) {
15908
16181
  throw new ManifestEditError(
15909
16182
  `${spec.styleId}/${spec.assetId} has no generated PNG on disk to start from; generate or restore it first`
15910
16183
  );
15911
16184
  }
15912
- await (0, import_promises21.mkdir)(import_node_path28.default.dirname(member.path), { recursive: true });
16185
+ await (0, import_promises21.mkdir)(import_node_path29.default.dirname(member.path), { recursive: true });
15913
16186
  await (0, import_promises21.copyFile)(member.base, member.path);
15914
16187
  created = true;
15915
16188
  }
@@ -16047,7 +16320,7 @@ async function saveHandEdit(loaded, lock, spec, input) {
16047
16320
  editor: input.editor,
16048
16321
  protocol: input.protocol,
16049
16322
  savedAt: (input.now ?? /* @__PURE__ */ new Date()).toISOString(),
16050
- project: projectPath ? import_node_path28.default.basename(projectPath) : null,
16323
+ project: projectPath ? import_node_path29.default.basename(projectPath) : null,
16051
16324
  outputs: incoming.map((frame, index) => ({
16052
16325
  role: frame.role,
16053
16326
  basedOn: basedOnHash(entry, index),
@@ -16089,9 +16362,9 @@ function openInEditor(file, editor = process.env.PIXELKILN_EDITOR) {
16089
16362
  }
16090
16363
 
16091
16364
  // src/gallery/upload.ts
16092
- var import_node_fs25 = require("fs");
16365
+ var import_node_fs26 = require("fs");
16093
16366
  var import_promises22 = require("fs/promises");
16094
- var import_node_path29 = __toESM(require("path"), 1);
16367
+ var import_node_path30 = __toESM(require("path"), 1);
16095
16368
  var import_zod10 = require("zod");
16096
16369
  var MAX_UPLOAD_BYTES = 8 * 1024 * 1024;
16097
16370
  var UploadImageSchema = import_zod10.z.object({
@@ -16104,12 +16377,12 @@ var UploadImageSchema = import_zod10.z.object({
16104
16377
  overwrite: import_zod10.z.boolean().optional()
16105
16378
  }).strict();
16106
16379
  async function saveProjectImage(root, request) {
16107
- const file = import_node_path29.default.resolve(root, request.path);
16108
- const relative = import_node_path29.default.relative(root, file);
16109
- if (relative.startsWith("..") || import_node_path29.default.isAbsolute(relative)) {
16380
+ const file = import_node_path30.default.resolve(root, request.path);
16381
+ const relative = import_node_path30.default.relative(root, file);
16382
+ if (relative.startsWith("..") || import_node_path30.default.isAbsolute(relative)) {
16110
16383
  throw new ManifestEditError("an upload must stay inside the project");
16111
16384
  }
16112
- const extension = import_node_path29.default.extname(file).toLowerCase();
16385
+ const extension = import_node_path30.default.extname(file).toLowerCase();
16113
16386
  const expected = extension === ".png" ? "png" : extension === ".jpg" || extension === ".jpeg" ? "jpeg" : null;
16114
16387
  if (!expected) throw new ManifestEditError("an upload must be a .png, .jpg, or .jpeg path");
16115
16388
  const bytes = Buffer.from(request.base64, "base64");
@@ -16121,70 +16394,126 @@ async function saveProjectImage(root, request) {
16121
16394
  if (metadata.format !== expected) {
16122
16395
  throw new ManifestEditError(`the upload is a ${metadata.format.toUpperCase()}; name it ${metadata.format === "png" ? ".png" : ".jpg"}`);
16123
16396
  }
16124
- if ((0, import_node_fs25.existsSync)(file) && !request.overwrite) {
16125
- throw new ManifestEditError(`${relative.split(import_node_path29.default.sep).join("/")} already exists; choose another name, or replace it`);
16397
+ if ((0, import_node_fs26.existsSync)(file) && !request.overwrite) {
16398
+ throw new ManifestEditError(`${relative.split(import_node_path30.default.sep).join("/")} already exists; choose another name, or replace it`);
16126
16399
  }
16127
- await (0, import_promises22.mkdir)(import_node_path29.default.dirname(file), { recursive: true });
16400
+ await (0, import_promises22.mkdir)(import_node_path30.default.dirname(file), { recursive: true });
16128
16401
  await (0, import_promises22.writeFile)(file, bytes);
16129
- return relative.split(import_node_path29.default.sep).join("/");
16402
+ return relative.split(import_node_path30.default.sep).join("/");
16130
16403
  }
16131
16404
 
16132
- // src/gallery/skeleton.ts
16133
- var import_node_fs26 = require("fs");
16405
+ // src/gallery/image-revision.ts
16406
+ var import_node_fs27 = require("fs");
16134
16407
  var import_promises23 = require("fs/promises");
16135
- var import_node_path30 = __toESM(require("path"), 1);
16408
+ var import_node_path31 = __toESM(require("path"), 1);
16136
16409
  var import_zod11 = require("zod");
16137
- var SkeletonEstimateRequestSchema = import_zod11.z.object({
16138
- project: import_zod11.z.string().min(1).optional(),
16139
- styleId: import_zod11.z.string().min(1),
16140
- assetId: import_zod11.z.string().min(1),
16141
- frames: import_zod11.z.number().int().min(3).max(15).optional()
16142
- }).strict();
16143
- var SkeletonAnimationRequestSchema = import_zod11.z.object({
16144
- action: import_zod11.z.literal("create-skeleton-animation"),
16410
+ var InpaintRevisionRequestSchema = import_zod11.z.object({
16411
+ action: import_zod11.z.literal("create-inpaint-revision"),
16145
16412
  project: import_zod11.z.string().min(1).optional(),
16146
16413
  expectedSha256: import_zod11.z.string().regex(/^[0-9a-f]{64}$/),
16147
16414
  styleId: import_zod11.z.string().min(1),
16148
- /** The sprite the animation poses. */
16415
+ /** The sprite being repainted. */
16149
16416
  from: import_zod11.z.string().min(1),
16150
16417
  assetId: import_zod11.z.string().min(1).regex(/^[^/\\]+$/, "asset ids cannot contain slashes"),
16151
- /** The motion, sent as PixelLab's `action`. */
16418
+ /** What to paint into the masked area. */
16152
16419
  prompt: import_zod11.z.string().min(1),
16420
+ /** Manifest-relative `.png` destination for the mask. */
16421
+ maskPath: import_zod11.z.string().min(1),
16422
+ maskBase64: import_zod11.z.string().regex(/^[A-Za-z0-9+/]*={0,2}$/),
16423
+ /** Replace a file already at `maskPath`. */
16424
+ overwrite: import_zod11.z.boolean().optional()
16425
+ }).strict();
16426
+ async function createInpaintRevision(manifestPath, request) {
16427
+ const root = import_node_path31.default.dirname(import_node_path31.default.resolve(manifestPath));
16428
+ if (import_node_path31.default.extname(request.maskPath).toLowerCase() !== ".png") {
16429
+ throw new ManifestEditError("the mask must be a .png path inside the project");
16430
+ }
16431
+ const bytes = Buffer.from(request.maskBase64, "base64");
16432
+ const metadata = imageMetadata(bytes);
16433
+ if (!metadata || metadata.format !== "png") throw new ManifestEditError("the mask is not a readable PNG");
16434
+ const file = import_node_path31.default.resolve(root, request.maskPath);
16435
+ const previous = (0, import_node_fs27.existsSync)(file) ? await (0, import_promises23.readFile)(file) : null;
16436
+ const mask = await saveProjectImage(root, {
16437
+ action: "upload-image",
16438
+ path: request.maskPath,
16439
+ base64: request.maskBase64,
16440
+ ...request.overwrite ? { overwrite: true } : {}
16441
+ });
16442
+ try {
16443
+ await applyManifestEdit(manifestPath, {
16444
+ action: "add-asset",
16445
+ assetId: request.assetId,
16446
+ expectedSha256: request.expectedSha256,
16447
+ asset: {
16448
+ prompt: request.prompt,
16449
+ styles: [request.styleId],
16450
+ revision: { mode: "inpaint", from: request.from, mask }
16451
+ }
16452
+ });
16453
+ } catch (error) {
16454
+ if (previous) await (0, import_promises23.writeFile)(file, previous);
16455
+ else await (0, import_promises23.rm)(file, { force: true });
16456
+ throw error;
16457
+ }
16458
+ return mask;
16459
+ }
16460
+
16461
+ // src/gallery/skeleton.ts
16462
+ var import_node_fs28 = require("fs");
16463
+ var import_promises24 = require("fs/promises");
16464
+ var import_node_path32 = __toESM(require("path"), 1);
16465
+ var import_zod12 = require("zod");
16466
+ var SkeletonEstimateRequestSchema = import_zod12.z.object({
16467
+ project: import_zod12.z.string().min(1).optional(),
16468
+ styleId: import_zod12.z.string().min(1),
16469
+ assetId: import_zod12.z.string().min(1),
16470
+ frames: import_zod12.z.number().int().min(3).max(15).optional()
16471
+ }).strict();
16472
+ var SkeletonAnimationRequestSchema = import_zod12.z.object({
16473
+ action: import_zod12.z.literal("create-skeleton-animation"),
16474
+ project: import_zod12.z.string().min(1).optional(),
16475
+ expectedSha256: import_zod12.z.string().regex(/^[0-9a-f]{64}$/),
16476
+ styleId: import_zod12.z.string().min(1),
16477
+ /** The sprite the animation poses. */
16478
+ from: import_zod12.z.string().min(1),
16479
+ assetId: import_zod12.z.string().min(1).regex(/^[^/\\]+$/, "asset ids cannot contain slashes"),
16480
+ /** The motion, sent as PixelLab's `action`. */
16481
+ prompt: import_zod12.z.string().min(1),
16153
16482
  direction: CharacterDirectionSchema,
16154
- description: import_zod11.z.string().min(1).optional(),
16155
- skeletonTemplate: import_zod11.z.string().min(1).optional(),
16483
+ description: import_zod12.z.string().min(1).optional(),
16484
+ skeletonTemplate: import_zod12.z.string().min(1).optional(),
16156
16485
  /** Manifest-relative JSON path inside the project. */
16157
- keypointsFile: import_zod11.z.string().min(1),
16486
+ keypointsFile: import_zod12.z.string().min(1),
16158
16487
  /** Poses to write to `keypointsFile`; omitted to use a file that already exists. */
16159
16488
  set: SkeletonSetSchema.optional(),
16160
16489
  /** Replace an existing `keypointsFile` with `set`. */
16161
- overwrite: import_zod11.z.boolean().optional()
16490
+ overwrite: import_zod12.z.boolean().optional()
16162
16491
  }).strict();
16163
- var SkeletonKeypointsUpdateSchema = import_zod11.z.object({
16164
- action: import_zod11.z.literal("update-skeleton-keypoints"),
16165
- project: import_zod11.z.string().min(1).optional(),
16166
- styleId: import_zod11.z.string().min(1),
16167
- assetId: import_zod11.z.string().min(1),
16492
+ var SkeletonKeypointsUpdateSchema = import_zod12.z.object({
16493
+ action: import_zod12.z.literal("update-skeleton-keypoints"),
16494
+ project: import_zod12.z.string().min(1).optional(),
16495
+ styleId: import_zod12.z.string().min(1),
16496
+ assetId: import_zod12.z.string().min(1),
16168
16497
  /** The file's SHA-256 when the page loaded it; anything else on disk is a concurrent edit. */
16169
- expectedSha256: import_zod11.z.string().regex(/^[0-9a-f]{64}$/),
16498
+ expectedSha256: import_zod12.z.string().regex(/^[0-9a-f]{64}$/),
16170
16499
  set: SkeletonSetSchema
16171
16500
  }).strict();
16172
16501
  async function createSkeletonAnimation(manifestPath, request) {
16173
- const root = import_node_path30.default.dirname(import_node_path30.default.resolve(manifestPath));
16174
- const file = import_node_path30.default.resolve(root, request.keypointsFile);
16175
- const relative = import_node_path30.default.relative(root, file);
16176
- if (!request.keypointsFile.endsWith(".json") || relative.startsWith("..") || import_node_path30.default.isAbsolute(relative)) {
16502
+ const root = import_node_path32.default.dirname(import_node_path32.default.resolve(manifestPath));
16503
+ const file = import_node_path32.default.resolve(root, request.keypointsFile);
16504
+ const relative = import_node_path32.default.relative(root, file);
16505
+ if (!request.keypointsFile.endsWith(".json") || relative.startsWith("..") || import_node_path32.default.isAbsolute(relative)) {
16177
16506
  throw new ManifestEditError("the keypoints file must be a .json path inside the project");
16178
16507
  }
16179
- const existed = (0, import_node_fs26.existsSync)(file);
16508
+ const existed = (0, import_node_fs28.existsSync)(file);
16180
16509
  let previous = null;
16181
16510
  if (request.set) {
16182
16511
  if (existed && !request.overwrite) {
16183
16512
  throw new ManifestEditError(`${request.keypointsFile} already exists; choose another path, or replace it`);
16184
16513
  }
16185
- if (existed) previous = await (0, import_promises23.readFile)(file);
16186
- await (0, import_promises23.mkdir)(import_node_path30.default.dirname(file), { recursive: true });
16187
- await (0, import_promises23.writeFile)(file, JSON.stringify(request.set, null, 2) + "\n");
16514
+ if (existed) previous = await (0, import_promises24.readFile)(file);
16515
+ await (0, import_promises24.mkdir)(import_node_path32.default.dirname(file), { recursive: true });
16516
+ await (0, import_promises24.writeFile)(file, JSON.stringify(request.set, null, 2) + "\n");
16188
16517
  } else if (!existed) {
16189
16518
  throw new ManifestEditError(`${request.keypointsFile} does not exist; paste poses or estimate them from the sprite`);
16190
16519
  }
@@ -16199,7 +16528,7 @@ async function createSkeletonAnimation(manifestPath, request) {
16199
16528
  revision: {
16200
16529
  mode: "animate-skeleton",
16201
16530
  from: request.from,
16202
- keypointsFile: relative.split(import_node_path30.default.sep).join("/"),
16531
+ keypointsFile: relative.split(import_node_path32.default.sep).join("/"),
16203
16532
  direction: request.direction,
16204
16533
  ...request.description ? { description: request.description } : {},
16205
16534
  ...request.skeletonTemplate ? { skeletonTemplate: request.skeletonTemplate } : {}
@@ -16208,8 +16537,8 @@ async function createSkeletonAnimation(manifestPath, request) {
16208
16537
  });
16209
16538
  } catch (error) {
16210
16539
  if (request.set) {
16211
- if (previous) await (0, import_promises23.writeFile)(file, previous);
16212
- else await (0, import_promises23.rm)(file, { force: true });
16540
+ if (previous) await (0, import_promises24.writeFile)(file, previous);
16541
+ else await (0, import_promises24.rm)(file, { force: true });
16213
16542
  }
16214
16543
  throw error;
16215
16544
  }
@@ -16220,64 +16549,64 @@ async function updateSkeletonKeypoints(ctx, request) {
16220
16549
  if (!spec) throw new ManifestEditError(`"${key}" is not declared by the manifest`);
16221
16550
  const file = spec.revision?.mode === "animate-skeleton" ? spec.revision.keypointsFile : void 0;
16222
16551
  if (!file) throw new ManifestEditError(`${key} is not an animate-skeleton revision`);
16223
- const relative = import_node_path30.default.relative(ctx.loaded.root, file);
16224
- if (relative.startsWith("..") || import_node_path30.default.isAbsolute(relative)) {
16552
+ const relative = import_node_path32.default.relative(ctx.loaded.root, file);
16553
+ if (relative.startsWith("..") || import_node_path32.default.isAbsolute(relative)) {
16225
16554
  throw new ManifestEditError(`${key}'s keypoints file is outside the project; edit it by hand`);
16226
16555
  }
16227
- const current = (0, import_node_fs26.existsSync)(file) ? sha256(await (0, import_promises23.readFile)(file)) : null;
16556
+ const current = (0, import_node_fs28.existsSync)(file) ? sha256(await (0, import_promises24.readFile)(file)) : null;
16228
16557
  if (current !== request.expectedSha256) {
16229
16558
  throw Object.assign(
16230
- new Error(`${relative.split(import_node_path30.default.sep).join("/")} changed on disk since this page loaded it; press Refresh and edit again`),
16559
+ new Error(`${relative.split(import_node_path32.default.sep).join("/")} changed on disk since this page loaded it; press Refresh and edit again`),
16231
16560
  { status: 409 }
16232
16561
  );
16233
16562
  }
16234
- await (0, import_promises23.writeFile)(file, JSON.stringify(request.set, null, 2) + "\n");
16235
- return relative.split(import_node_path30.default.sep).join("/");
16563
+ await (0, import_promises24.writeFile)(file, JSON.stringify(request.set, null, 2) + "\n");
16564
+ return relative.split(import_node_path32.default.sep).join("/");
16236
16565
  }
16237
16566
 
16238
16567
  // src/gallery/edit.ts
16239
- var Base64 = import_zod12.z.string().regex(/^[A-Za-z0-9+/]*={0,2}$/).max(Math.ceil(MAX_HAND_EDIT_BYTES / 3) * 4);
16240
- var HandEditRequestSchema = import_zod12.z.discriminatedUnion("action", [
16241
- import_zod12.z.object({
16242
- action: import_zod12.z.literal("start-edit"),
16243
- project: import_zod12.z.string().min(1).optional(),
16244
- assetId: import_zod12.z.string().min(1),
16245
- styleId: import_zod12.z.string().min(1),
16246
- expectedSha256: import_zod12.z.string().regex(/^[0-9a-f]{64}$/),
16568
+ var Base64 = import_zod13.z.string().regex(/^[A-Za-z0-9+/]*={0,2}$/).max(Math.ceil(MAX_HAND_EDIT_BYTES / 3) * 4);
16569
+ var HandEditRequestSchema = import_zod13.z.discriminatedUnion("action", [
16570
+ import_zod13.z.object({
16571
+ action: import_zod13.z.literal("start-edit"),
16572
+ project: import_zod13.z.string().min(1).optional(),
16573
+ assetId: import_zod13.z.string().min(1),
16574
+ styleId: import_zod13.z.string().min(1),
16575
+ expectedSha256: import_zod13.z.string().regex(/^[0-9a-f]{64}$/),
16247
16576
  /** Also launch the author's editor on the file. */
16248
- open: import_zod12.z.boolean().optional()
16577
+ open: import_zod13.z.boolean().optional()
16249
16578
  }).strict(),
16250
- import_zod12.z.object({
16251
- action: import_zod12.z.literal("detach-edit"),
16252
- project: import_zod12.z.string().min(1).optional(),
16253
- assetId: import_zod12.z.string().min(1),
16254
- styleId: import_zod12.z.string().min(1),
16255
- expectedSha256: import_zod12.z.string().regex(/^[0-9a-f]{64}$/)
16579
+ import_zod13.z.object({
16580
+ action: import_zod13.z.literal("detach-edit"),
16581
+ project: import_zod13.z.string().min(1).optional(),
16582
+ assetId: import_zod13.z.string().min(1),
16583
+ styleId: import_zod13.z.string().min(1),
16584
+ expectedSha256: import_zod13.z.string().regex(/^[0-9a-f]{64}$/)
16256
16585
  }).strict(),
16257
16586
  /** From the in-browser editor: the page holds the bytes, the server validates and writes. */
16258
- import_zod12.z.object({
16259
- action: import_zod12.z.literal("save-edit"),
16260
- project: import_zod12.z.string().min(1).optional(),
16261
- assetId: import_zod12.z.string().min(1),
16262
- styleId: import_zod12.z.string().min(1),
16263
- expectedSha256: import_zod12.z.string().regex(/^[0-9a-f]{64}$/),
16587
+ import_zod13.z.object({
16588
+ action: import_zod13.z.literal("save-edit"),
16589
+ project: import_zod13.z.string().min(1).optional(),
16590
+ assetId: import_zod13.z.string().min(1),
16591
+ styleId: import_zod13.z.string().min(1),
16592
+ expectedSha256: import_zod13.z.string().regex(/^[0-9a-f]{64}$/),
16264
16593
  /** The flattened image; for a frame set, `frames` instead. */
16265
16594
  png: Base64.min(1).optional(),
16266
16595
  /** Every frame of a set, tagged with the role it was opened under (null for one added in the editor). */
16267
- frames: import_zod12.z.array(import_zod12.z.object({ role: import_zod12.z.string().min(1).nullable(), png: Base64.min(1) }).strict()).min(1).max(256).optional(),
16596
+ frames: import_zod13.z.array(import_zod13.z.object({ role: import_zod13.z.string().min(1).nullable(), png: Base64.min(1) }).strict()).min(1).max(256).optional(),
16268
16597
  /** The editor's layered project file, kept beside the edit. */
16269
16598
  pxo: Base64.optional(),
16270
16599
  /** Editor identity from the bridge's ready message. */
16271
- editor: import_zod12.z.string().min(1).max(200),
16272
- protocol: import_zod12.z.number().int().positive()
16600
+ editor: import_zod13.z.string().min(1).max(200),
16601
+ protocol: import_zod13.z.number().int().positive()
16273
16602
  }).strict()
16274
16603
  ]);
16275
16604
  var UNDO_DEPTH = 20;
16276
- var UndoRequestSchema = import_zod12.z.object({
16277
- action: import_zod12.z.literal("undo-edit"),
16278
- project: import_zod12.z.string().min(1).optional(),
16605
+ var UndoRequestSchema = import_zod13.z.object({
16606
+ action: import_zod13.z.literal("undo-edit"),
16607
+ project: import_zod13.z.string().min(1).optional(),
16279
16608
  /** The manifest as the page last saw it; undo refuses a manifest someone else has changed since. */
16280
- expectedSha256: import_zod12.z.string().regex(/^[0-9a-f]{64}$/)
16609
+ expectedSha256: import_zod13.z.string().regex(/^[0-9a-f]{64}$/)
16281
16610
  }).strict();
16282
16611
  function createGalleryEditHandler(opts) {
16283
16612
  const log = opts.onProgress ?? (() => {
@@ -16286,7 +16615,7 @@ function createGalleryEditHandler(opts) {
16286
16615
  const history = /* @__PURE__ */ new Map();
16287
16616
  const manifestOf = async (project) => {
16288
16617
  try {
16289
- return import_node_path31.default.resolve(await opts.manifestFor(typeof project === "string" ? project : void 0));
16618
+ return import_node_path33.default.resolve(await opts.manifestFor(typeof project === "string" ? project : void 0));
16290
16619
  } catch {
16291
16620
  return null;
16292
16621
  }
@@ -16296,7 +16625,7 @@ function createGalleryEditHandler(opts) {
16296
16625
  if (undo.success) {
16297
16626
  const manifestPath2 = await manifestOf(undo.data.project);
16298
16627
  if (!manifestPath2) throw new ManifestEditError("no manifest to undo an edit in");
16299
- const current = await (0, import_promises24.readFile)(manifestPath2, "utf8");
16628
+ const current = await (0, import_promises25.readFile)(manifestPath2, "utf8");
16300
16629
  if (sha256(current) !== undo.data.expectedSha256) throw new ManifestDriftError(manifestPath2);
16301
16630
  const stack = history.get(manifestPath2) ?? [];
16302
16631
  const last = stack[stack.length - 1];
@@ -16306,17 +16635,17 @@ function createGalleryEditHandler(opts) {
16306
16635
  throw Object.assign(new Error("the manifest changed outside the gallery since its last edit; undo would discard that change"), { status: 409 });
16307
16636
  }
16308
16637
  const tmp = manifestPath2 + ".pixelkiln-undo";
16309
- await (0, import_promises24.writeFile)(tmp, last.before);
16310
- await (0, import_promises24.rename)(tmp, manifestPath2);
16638
+ await (0, import_promises25.writeFile)(tmp, last.before);
16639
+ await (0, import_promises25.rename)(tmp, manifestPath2);
16311
16640
  stack.pop();
16312
- log(` manifest edit undone in ${import_node_path31.default.relative(process.cwd(), manifestPath2) || manifestPath2}`);
16641
+ log(` manifest edit undone in ${import_node_path33.default.relative(process.cwd(), manifestPath2) || manifestPath2}`);
16313
16642
  return opts.reload();
16314
16643
  }
16315
16644
  const manifestPath = await manifestOf(body?.project);
16316
- const before = manifestPath ? await (0, import_promises24.readFile)(manifestPath, "utf8").catch(() => null) : null;
16645
+ const before = manifestPath ? await (0, import_promises25.readFile)(manifestPath, "utf8").catch(() => null) : null;
16317
16646
  const build = await apply(body);
16318
16647
  if (manifestPath && before !== null) {
16319
- const after = await (0, import_promises24.readFile)(manifestPath, "utf8").catch(() => null);
16648
+ const after = await (0, import_promises25.readFile)(manifestPath, "utf8").catch(() => null);
16320
16649
  if (after !== null && after !== before) {
16321
16650
  const stack = history.get(manifestPath) ?? [];
16322
16651
  stack.push({ before, afterSha: sha256(after) });
@@ -16371,7 +16700,7 @@ function createEditApplier(opts, log) {
16371
16700
  } catch (error) {
16372
16701
  throw new ManifestEditError(error instanceof Error ? error.message : String(error));
16373
16702
  }
16374
- const written = await saveProjectImage(import_node_path31.default.dirname(import_node_path31.default.resolve(manifestPath2)), upload.data);
16703
+ const written = await saveProjectImage(import_node_path33.default.dirname(import_node_path33.default.resolve(manifestPath2)), upload.data);
16375
16704
  log(` image added: ${written}`);
16376
16705
  return opts.reload();
16377
16706
  }
@@ -16407,6 +16736,23 @@ function createEditApplier(opts, log) {
16407
16736
  "invalid skeleton animation: " + skeleton.error.issues.slice(0, 3).map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")
16408
16737
  );
16409
16738
  }
16739
+ const inpaint = InpaintRevisionRequestSchema.safeParse(body);
16740
+ if (inpaint.success) {
16741
+ let manifestPath2;
16742
+ try {
16743
+ manifestPath2 = await opts.manifestFor(inpaint.data.project);
16744
+ } catch (error) {
16745
+ throw new ManifestEditError(error instanceof Error ? error.message : String(error));
16746
+ }
16747
+ const mask = await createInpaintRevision(manifestPath2, inpaint.data);
16748
+ log(` manifest edited: added inpaint revision ${inpaint.data.assetId} (mask ${mask})`);
16749
+ return opts.reload();
16750
+ }
16751
+ if (body?.action === "create-inpaint-revision") {
16752
+ throw new ManifestEditError(
16753
+ "invalid inpaint revision: " + inpaint.error.issues.slice(0, 3).map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")
16754
+ );
16755
+ }
16410
16756
  const parsed = ManifestEditSchema.safeParse(body);
16411
16757
  if (!parsed.success) {
16412
16758
  throw new ManifestEditError(
@@ -16421,7 +16767,7 @@ function createEditApplier(opts, log) {
16421
16767
  }
16422
16768
  const result = await applyManifestEdit(manifestPath, parsed.data);
16423
16769
  if (result.changed) {
16424
- const relative = import_node_path31.default.relative(process.cwd(), result.manifestPath);
16770
+ const relative = import_node_path33.default.relative(process.cwd(), result.manifestPath);
16425
16771
  const shown = !relative || relative.startsWith("..") ? result.manifestPath : relative;
16426
16772
  log(` manifest edited: ${describeEdit(parsed.data)} in ${shown}`);
16427
16773
  }
@@ -16449,7 +16795,7 @@ function describeEdit(edit) {
16449
16795
  // src/gallery/snapshot.ts
16450
16796
  var PROJECT_FILE_TYPE = "application/octet-stream";
16451
16797
  function galleryMediaId(absolutePath) {
16452
- return (0, import_node_crypto6.createHash)("sha256").update(import_node_path32.default.resolve(absolutePath)).digest("hex").slice(0, 24);
16798
+ return (0, import_node_crypto7.createHash)("sha256").update(import_node_path34.default.resolve(absolutePath)).digest("hex").slice(0, 24);
16453
16799
  }
16454
16800
  function galleryMediaRoute(id) {
16455
16801
  return `/media/${id}`;
@@ -16468,9 +16814,9 @@ function describeHistory(media, entry, cacheDir) {
16468
16814
  return (entry.history ?? []).map((generation, i) => {
16469
16815
  const outputs = generation.outputs.map((output) => {
16470
16816
  const mediaType = output.mediaType ?? MediaType.PNG;
16471
- const file = import_node_path32.default.join(cacheDir, cacheFileName(output.sha256, mediaType));
16817
+ const file = import_node_path34.default.join(cacheDir, cacheFileName(output.sha256, mediaType));
16472
16818
  let url = null;
16473
- if ((0, import_node_fs27.existsSync)(file)) {
16819
+ if ((0, import_node_fs29.existsSync)(file)) {
16474
16820
  const id = galleryMediaId(file);
16475
16821
  media.set(id, { path: file, contentType: mediaType });
16476
16822
  url = `${galleryMediaRoute(id)}?v=${output.sha256.slice(0, 24)}`;
@@ -16502,7 +16848,7 @@ function describeHistory(media, entry, cacheDir) {
16502
16848
  var MAX_COMPARED_BYTES = 8 * 1024 * 1024;
16503
16849
  async function samePixels(a, b) {
16504
16850
  try {
16505
- const [left, right] = await Promise.all([(0, import_promises25.readFile)(a), (0, import_promises25.readFile)(b)]);
16851
+ const [left, right] = await Promise.all([(0, import_promises26.readFile)(a), (0, import_promises26.readFile)(b)]);
16506
16852
  if (left.length > MAX_COMPARED_BYTES || right.length > MAX_COMPARED_BYTES) return false;
16507
16853
  const x = decodePng(left);
16508
16854
  const y = decodePng(right);
@@ -16568,7 +16914,7 @@ function describeCharacter(spec, entry) {
16568
16914
  }
16569
16915
  async function fileInfo(absolutePath) {
16570
16916
  try {
16571
- const info = await (0, import_promises25.stat)(absolutePath);
16917
+ const info = await (0, import_promises26.stat)(absolutePath);
16572
16918
  return info.isFile() ? { bytes: info.size, modifiedAt: info.mtime.toISOString() } : null;
16573
16919
  } catch {
16574
16920
  return null;
@@ -16600,10 +16946,10 @@ async function describeOutput(media, root, absolutePath, recorded = {}) {
16600
16946
  async function describeQuality(media, root, inspection) {
16601
16947
  const outputPaths = inspection.outputs ?? [inspection.output];
16602
16948
  const outputs = await Promise.all(
16603
- outputPaths.map((file) => describeOutput(media, root, import_node_path32.default.resolve(root, file)))
16949
+ outputPaths.map((file) => describeOutput(media, root, import_node_path34.default.resolve(root, file)))
16604
16950
  );
16605
- const recordPath = import_node_path32.default.resolve(root, inspection.record);
16606
- const recordExists = (0, import_node_fs27.existsSync)(recordPath);
16951
+ const recordPath = import_node_path34.default.resolve(root, inspection.record);
16952
+ const recordExists = (0, import_node_fs29.existsSync)(recordPath);
16607
16953
  let options = null;
16608
16954
  let check = null;
16609
16955
  if (recordExists) {
@@ -16622,7 +16968,7 @@ async function describeQuality(media, root, inspection) {
16622
16968
  return {
16623
16969
  state: inspection.state,
16624
16970
  reason: inspection.reason,
16625
- output: portableOutputPath(import_node_path32.default.resolve(root, inspection.output), root),
16971
+ output: portableOutputPath(import_node_path34.default.resolve(root, inspection.output), root),
16626
16972
  record: portableOutputPath(recordPath, root),
16627
16973
  recordExists,
16628
16974
  outputs,
@@ -16660,9 +17006,9 @@ async function buildGallerySnapshot(opts) {
16660
17006
  const media = /* @__PURE__ */ new Map();
16661
17007
  const plan = await buildPlan(specs, lock);
16662
17008
  const items = [];
16663
- const manifestText = await (0, import_promises25.readFile)(loaded.path, "utf8");
17009
+ const manifestText = await (0, import_promises26.readFile)(loaded.path, "utf8");
16664
17010
  const manifestSha256 = sha256(manifestText);
16665
- const cacheDir = import_node_path32.default.resolve(import_node_path32.default.dirname(opts.lockPath), ".pixelkiln", "cache");
17011
+ const cacheDir = import_node_path34.default.resolve(import_node_path34.default.dirname(opts.lockPath), ".pixelkiln", "cache");
16666
17012
  let rawStyles = {};
16667
17013
  try {
16668
17014
  const raw = JSON.parse(manifestText);
@@ -16686,12 +17032,12 @@ async function buildGallerySnapshot(opts) {
16686
17032
  )
16687
17033
  );
16688
17034
  if (spec.source) {
16689
- const editPath = import_node_path32.default.resolve(root, spec.source);
17035
+ const editPath = import_node_path34.default.resolve(root, spec.source);
16690
17036
  const memberCount = sourceIsStem(spec.source, entry, root) ? entry.outputs.length : 1;
16691
17037
  const editShas = await Promise.all(
16692
17038
  Array.from({ length: memberCount }, async (_, index) => {
16693
17039
  const file = sourceOutputPath(spec.source, entry, index, root);
16694
- return { file, sha256: (0, import_node_fs27.existsSync)(file) ? await sha256File(file) : null };
17040
+ return { file, sha256: (0, import_node_fs29.existsSync)(file) ? await sha256File(file) : null };
16695
17041
  })
16696
17042
  );
16697
17043
  edits = await Promise.all(editShas.map(({ file, sha256: editSha }) => describeOutput(media, root, file, { sha256: editSha })));
@@ -16699,7 +17045,7 @@ async function buildGallerySnapshot(opts) {
16699
17045
  const companion = await readHandEditCompanion(editPath);
16700
17046
  if (companion) {
16701
17047
  const projectPath = handEditProjectPath(editPath);
16702
- const project = companion.project && (0, import_node_fs27.existsSync)(projectPath) ? await fileInfo(projectPath) : null;
17048
+ const project = companion.project && (0, import_node_fs29.existsSync)(projectPath) ? await fileInfo(projectPath) : null;
16703
17049
  let projectUrl = null;
16704
17050
  if (project) {
16705
17051
  const id = galleryMediaId(projectPath);
@@ -16735,8 +17081,8 @@ async function buildGallerySnapshot(opts) {
16735
17081
  editStatus = edits.some((member) => !member.exists) ? "missing" : editChanged.every((changed) => !changed) ? "same" : regenerated ? "regenerated-since" : "edited";
16736
17082
  }
16737
17083
  } else if (spec.source) {
16738
- outputs = [await describeOutput(media, root, import_node_path32.default.resolve(root, spec.source))];
16739
- } else if (!entry && (0, import_node_fs27.existsSync)(spec.outFile)) {
17084
+ outputs = [await describeOutput(media, root, import_node_path34.default.resolve(root, spec.source))];
17085
+ } else if (!entry && (0, import_node_fs29.existsSync)(spec.outFile)) {
16740
17086
  outputs = [await describeOutput(media, root, spec.outFile)];
16741
17087
  } else {
16742
17088
  outputs = [];
@@ -16939,7 +17285,7 @@ async function buildGallerySnapshot(opts) {
16939
17285
  id: loaded.manifest.name,
16940
17286
  name: loaded.manifest.name,
16941
17287
  manifest: loaded.path,
16942
- lock: import_node_path32.default.resolve(opts.lockPath),
17288
+ lock: import_node_path34.default.resolve(opts.lockPath),
16943
17289
  root,
16944
17290
  provider: loaded.manifest.provider,
16945
17291
  account: null,
@@ -16965,7 +17311,7 @@ async function buildGallerySnapshot(opts) {
16965
17311
  return { snapshot, media };
16966
17312
  }
16967
17313
  async function buildWorkspaceGallerySnapshot(opts) {
16968
- const dir = import_node_path32.default.dirname(import_node_path32.default.resolve(opts.workspacePath));
17314
+ const dir = import_node_path34.default.dirname(import_node_path34.default.resolve(opts.workspacePath));
16969
17315
  const media = /* @__PURE__ */ new Map();
16970
17316
  const projects = [];
16971
17317
  const items = [];
@@ -17008,11 +17354,11 @@ async function buildWorkspaceGallerySnapshot(opts) {
17008
17354
  id: project.id,
17009
17355
  name: loaded.manifest.name,
17010
17356
  manifest: manifestPath,
17011
- lock: import_node_path32.default.resolve(lockPath),
17357
+ lock: import_node_path34.default.resolve(lockPath),
17012
17358
  root: loaded.root,
17013
17359
  provider: loaded.manifest.provider,
17014
17360
  account: project.account ?? null,
17015
- manifestSha256: manifestSha256 ?? sha256(await (0, import_promises25.readFile)(manifestPath, "utf8")),
17361
+ manifestSha256: manifestSha256 ?? sha256(await (0, import_promises26.readFile)(manifestPath, "utf8")),
17016
17362
  historyLimit: historyLimit(loaded.manifest),
17017
17363
  entries: Object.keys(lock.entries).length,
17018
17364
  items: projectItems.length,
@@ -17024,8 +17370,8 @@ async function buildWorkspaceGallerySnapshot(opts) {
17024
17370
  id: project.id,
17025
17371
  name: project.id,
17026
17372
  manifest: manifestPath,
17027
- lock: import_node_path32.default.resolve(lockPath),
17028
- root: import_node_path32.default.dirname(manifestPath),
17373
+ lock: import_node_path34.default.resolve(lockPath),
17374
+ root: import_node_path34.default.dirname(manifestPath),
17029
17375
  provider: project.provider,
17030
17376
  account: project.account ?? null,
17031
17377
  manifestSha256: null,
@@ -17052,7 +17398,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
17052
17398
  version: 1,
17053
17399
  generatedAt: (opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
17054
17400
  project: null,
17055
- workspace: { path: import_node_path32.default.resolve(opts.workspacePath), dir, projects },
17401
+ workspace: { path: import_node_path34.default.resolve(opts.workspacePath), dir, projects },
17056
17402
  filter,
17057
17403
  totals: {
17058
17404
  entries: projects.reduce((sum, project) => sum + project.entries, 0),
@@ -17069,7 +17415,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
17069
17415
  }
17070
17416
 
17071
17417
  // src/gallery/page.ts
17072
- var import_node_fs28 = require("fs");
17418
+ var import_node_fs30 = require("fs");
17073
17419
 
17074
17420
  // src/gallery/client-bundle.ts
17075
17421
  var import_node_module = require("module");
@@ -17109,8 +17455,8 @@ var client;
17109
17455
  function clientAssets() {
17110
17456
  const prebuilt = new URL("gallery.js", CLIENT_DIR);
17111
17457
  client ??= {
17112
- css: (0, import_node_fs28.readFileSync)(new URL("gallery.css", CLIENT_DIR), "utf8"),
17113
- js: (0, import_node_fs28.existsSync)(prebuilt) ? (0, import_node_fs28.readFileSync)(prebuilt, "utf8") : bundleGalleryClient()
17458
+ css: (0, import_node_fs30.readFileSync)(new URL("gallery.css", CLIENT_DIR), "utf8"),
17459
+ js: (0, import_node_fs30.existsSync)(prebuilt) ? (0, import_node_fs30.readFileSync)(prebuilt, "utf8") : bundleGalleryClient()
17114
17460
  };
17115
17461
  return client;
17116
17462
  }
@@ -17252,11 +17598,11 @@ var EDITOR_PIN = pin_default;
17252
17598
  var EDITOR_RELEASE_BASE = "https://github.com/gfargo/pixelkiln/releases/download";
17253
17599
 
17254
17600
  // src/editor/install.ts
17255
- var import_node_crypto7 = require("crypto");
17256
- var import_node_fs29 = require("fs");
17257
- var import_promises26 = require("fs/promises");
17258
- var import_node_os3 = __toESM(require("os"), 1);
17259
- var import_node_path33 = __toESM(require("path"), 1);
17601
+ var import_node_crypto8 = require("crypto");
17602
+ var import_node_fs31 = require("fs");
17603
+ var import_promises27 = require("fs/promises");
17604
+ var import_node_os4 = __toESM(require("os"), 1);
17605
+ var import_node_path35 = __toESM(require("path"), 1);
17260
17606
  var EditorInstallError = class extends Error {
17261
17607
  constructor(message6, file) {
17262
17608
  super(message6);
@@ -17267,15 +17613,15 @@ var EditorInstallError = class extends Error {
17267
17613
  };
17268
17614
  function editorToolsRoot() {
17269
17615
  const override = process.env.PIXELKILN_TOOLS_DIR?.trim();
17270
- if (override) return import_node_path33.default.resolve(override);
17271
- if (process.platform === "darwin") return import_node_path33.default.join(import_node_os3.default.homedir(), "Library", "Caches", "pixelkiln", "tools");
17616
+ if (override) return import_node_path35.default.resolve(override);
17617
+ if (process.platform === "darwin") return import_node_path35.default.join(import_node_os4.default.homedir(), "Library", "Caches", "pixelkiln", "tools");
17272
17618
  if (process.platform === "win32") {
17273
- return import_node_path33.default.join(process.env.LOCALAPPDATA ?? import_node_path33.default.join(import_node_os3.default.homedir(), "AppData", "Local"), "pixelkiln", "tools");
17619
+ return import_node_path35.default.join(process.env.LOCALAPPDATA ?? import_node_path35.default.join(import_node_os4.default.homedir(), "AppData", "Local"), "pixelkiln", "tools");
17274
17620
  }
17275
- return import_node_path33.default.join(process.env.XDG_CACHE_HOME ?? import_node_path33.default.join(import_node_os3.default.homedir(), ".cache"), "pixelkiln", "tools");
17621
+ return import_node_path35.default.join(process.env.XDG_CACHE_HOME ?? import_node_path35.default.join(import_node_os4.default.homedir(), ".cache"), "pixelkiln", "tools");
17276
17622
  }
17277
17623
  function editorDir(pin = EDITOR_PIN, root = editorToolsRoot()) {
17278
- return import_node_path33.default.join(root, "pixelorama", pin.release ?? "unpublished");
17624
+ return import_node_path35.default.join(root, "pixelorama", pin.release ?? "unpublished");
17279
17625
  }
17280
17626
  function editorBaseUrl(pin = EDITOR_PIN) {
17281
17627
  const override = process.env.PIXELKILN_EDITOR_URL?.trim();
@@ -17285,7 +17631,7 @@ function editorBaseUrl(pin = EDITOR_PIN) {
17285
17631
  }
17286
17632
  async function sha256Of(file) {
17287
17633
  try {
17288
- return (0, import_node_crypto7.createHash)("sha256").update(await (0, import_promises26.readFile)(file)).digest("hex");
17634
+ return (0, import_node_crypto8.createHash)("sha256").update(await (0, import_promises27.readFile)(file)).digest("hex");
17289
17635
  } catch {
17290
17636
  return null;
17291
17637
  }
@@ -17298,8 +17644,8 @@ async function editorStatus(opts = {}) {
17298
17644
  let totalBytes = 0;
17299
17645
  for (const [name, expected] of Object.entries(pin.files)) {
17300
17646
  totalBytes += expected.bytes;
17301
- const file = import_node_path33.default.join(dir, name);
17302
- const info = (0, import_node_fs29.existsSync)(file) ? await (0, import_promises26.stat)(file).catch(() => null) : null;
17647
+ const file = import_node_path35.default.join(dir, name);
17648
+ const info = (0, import_node_fs31.existsSync)(file) ? await (0, import_promises27.stat)(file).catch(() => null) : null;
17303
17649
  if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
17304
17650
  installedBytes += expected.bytes;
17305
17651
  } else {
@@ -17328,7 +17674,7 @@ async function installEditor(opts = {}) {
17328
17674
  );
17329
17675
  }
17330
17676
  if (!baseUrl) throw new EditorInstallError("no download location for the editor build");
17331
- await (0, import_promises26.mkdir)(dir, { recursive: true });
17677
+ await (0, import_promises27.mkdir)(dir, { recursive: true });
17332
17678
  const downloaded = [];
17333
17679
  const skipped = Object.keys(pin.files).filter((name) => !before.missing.includes(name));
17334
17680
  let fetchedBytes = before.installedBytes;
@@ -17352,17 +17698,17 @@ async function installEditor(opts = {}) {
17352
17698
  if (bytes.length !== expected.bytes) {
17353
17699
  throw new EditorInstallError(`${name}: expected ${expected.bytes} bytes, got ${bytes.length}`, name);
17354
17700
  }
17355
- const digest2 = (0, import_node_crypto7.createHash)("sha256").update(bytes).digest("hex");
17701
+ const digest2 = (0, import_node_crypto8.createHash)("sha256").update(bytes).digest("hex");
17356
17702
  if (digest2 !== expected.sha256) {
17357
17703
  throw new EditorInstallError(`${name}: sha256 ${digest2.slice(0, 12)}\u2026 does not match the pinned ${expected.sha256.slice(0, 12)}\u2026`, name);
17358
17704
  }
17359
- const final = import_node_path33.default.join(dir, name);
17705
+ const final = import_node_path35.default.join(dir, name);
17360
17706
  const tmp = `${final}.${process.pid}.part`;
17361
17707
  try {
17362
- await (0, import_promises26.writeFile)(tmp, bytes);
17363
- await (0, import_promises26.rename)(tmp, final);
17708
+ await (0, import_promises27.writeFile)(tmp, bytes);
17709
+ await (0, import_promises27.rename)(tmp, final);
17364
17710
  } finally {
17365
- await (0, import_promises26.rm)(tmp, { force: true });
17711
+ await (0, import_promises27.rm)(tmp, { force: true });
17366
17712
  }
17367
17713
  fetchedBytes += bytes.length;
17368
17714
  downloaded.push(name);
@@ -17402,12 +17748,12 @@ var EDITOR_CONTENT_TYPES = {
17402
17748
  ".json": "application/json; charset=utf-8"
17403
17749
  };
17404
17750
  function editorContentType(name) {
17405
- return EDITOR_CONTENT_TYPES[import_node_path33.default.extname(name).toLowerCase()] ?? null;
17751
+ return EDITOR_CONTENT_TYPES[import_node_path35.default.extname(name).toLowerCase()] ?? null;
17406
17752
  }
17407
17753
 
17408
17754
  // src/gallery/editor.ts
17409
- var import_promises27 = require("fs/promises");
17410
- var import_node_path34 = __toESM(require("path"), 1);
17755
+ var import_promises28 = require("fs/promises");
17756
+ var import_node_path36 = __toESM(require("path"), 1);
17411
17757
  function editorRoute(release, name = "index.html") {
17412
17758
  return `/editor/${encodeURIComponent(release)}/${encodeURIComponent(name)}`;
17413
17759
  }
@@ -17480,7 +17826,7 @@ function createGalleryEditorHandlers(opts = {}) {
17480
17826
  if (!status.installed) return null;
17481
17827
  let bytes;
17482
17828
  try {
17483
- bytes = await (0, import_promises27.readFile)(import_node_path34.default.join(dir, name));
17829
+ bytes = await (0, import_promises28.readFile)(import_node_path36.default.join(dir, name));
17484
17830
  } catch {
17485
17831
  verified = null;
17486
17832
  return null;
@@ -17495,9 +17841,9 @@ function createGalleryEditorHandlers(opts = {}) {
17495
17841
  }
17496
17842
 
17497
17843
  // src/gallery/server.ts
17498
- var import_node_crypto8 = require("crypto");
17844
+ var import_node_crypto9 = require("crypto");
17499
17845
  var import_node_http2 = require("http");
17500
- var import_promises28 = require("fs/promises");
17846
+ var import_promises29 = require("fs/promises");
17501
17847
  var MAX_EDIT_BYTES = 48 * 1024 * 1024;
17502
17848
  var EDITOR_CSP = [
17503
17849
  "default-src 'self'",
@@ -17525,7 +17871,7 @@ async function readJsonBody(req) {
17525
17871
  async function serveGallery(opts) {
17526
17872
  const log = opts.onProgress ?? (() => {
17527
17873
  });
17528
- const session = opts.edit || opts.generate || opts.editor || opts.skeleton || opts.price ? (0, import_node_crypto8.randomBytes)(16).toString("hex") : null;
17874
+ const session = opts.edit || opts.generate || opts.editor || opts.skeleton || opts.price ? (0, import_node_crypto9.randomBytes)(16).toString("hex") : null;
17529
17875
  let media = /* @__PURE__ */ new Map();
17530
17876
  let loading = null;
17531
17877
  const reviewAssets = /* @__PURE__ */ new Map();
@@ -17678,7 +18024,7 @@ async function serveGallery(opts) {
17678
18024
  const asset = reviewAssets.get(jobId)?.get(url2.pathname);
17679
18025
  if (!asset) return fail(404, "no such review asset");
17680
18026
  try {
17681
- const bytes = await (0, import_promises28.readFile)(asset.path);
18027
+ const bytes = await (0, import_promises29.readFile)(asset.path);
17682
18028
  res.writeHead(200, {
17683
18029
  "Content-Type": asset.contentType,
17684
18030
  "Cache-Control": "no-store",
@@ -17729,7 +18075,7 @@ async function serveGallery(opts) {
17729
18075
  try {
17730
18076
  if (url2.pathname === "/") {
17731
18077
  const { snapshot } = await load();
17732
- const nonce = (0, import_node_crypto8.randomBytes)(16).toString("base64");
18078
+ const nonce = (0, import_node_crypto9.randomBytes)(16).toString("base64");
17733
18079
  res.writeHead(200, {
17734
18080
  "Content-Type": "text/html; charset=utf-8",
17735
18081
  "Cache-Control": "no-store",
@@ -17761,7 +18107,7 @@ async function serveGallery(opts) {
17761
18107
  if (!asset) return fail(404, "no such gallery media");
17762
18108
  let bytes;
17763
18109
  try {
17764
- bytes = await (0, import_promises28.readFile)(asset.path);
18110
+ bytes = await (0, import_promises29.readFile)(asset.path);
17765
18111
  } catch {
17766
18112
  return fail(404, "gallery media is no longer on disk; refresh the page");
17767
18113
  }
@@ -17804,28 +18150,28 @@ async function serveGallery(opts) {
17804
18150
  }
17805
18151
 
17806
18152
  // src/gallery/generate.ts
17807
- var import_node_crypto9 = require("crypto");
17808
- var import_zod13 = require("zod");
17809
- var GenerateRequestSchema = import_zod13.z.object({
17810
- project: import_zod13.z.string().min(1).optional(),
17811
- keys: import_zod13.z.array(import_zod13.z.string().min(1)).min(1).max(500),
18153
+ var import_node_crypto10 = require("crypto");
18154
+ var import_zod14 = require("zod");
18155
+ var GenerateRequestSchema = import_zod14.z.object({
18156
+ project: import_zod14.z.string().min(1).optional(),
18157
+ keys: import_zod14.z.array(import_zod14.z.string().min(1)).min(1).max(500),
17812
18158
  /**
17813
18159
  * Regenerate work that is up to date, as `gen --force` does, and let the
17814
18160
  * fetch replace a locally modified or untracked file. With `restore`, put
17815
18161
  * the recorded bytes back over a modified file.
17816
18162
  */
17817
- force: import_zod13.z.boolean().optional(),
18163
+ force: import_zod14.z.boolean().optional(),
17818
18164
  /** Advance in-flight, review, or selected work without submitting anything. */
17819
- resume: import_zod13.z.boolean().optional(),
18165
+ resume: import_zod14.z.boolean().optional(),
17820
18166
  /** Re-download downloaded outputs and replace files whose object changed upstream. */
17821
- refresh: import_zod13.z.boolean().optional(),
18167
+ refresh: import_zod14.z.boolean().optional(),
17822
18168
  /** Put recorded outputs back on disk from the cache or the provider, without generating. */
17823
- restore: import_zod13.z.boolean().optional(),
18169
+ restore: import_zod14.z.boolean().optional(),
17824
18170
  /**
17825
18171
  * Bring a replaced generation back: 1-based position in the record's
17826
18172
  * history (newest first) or an output hash prefix. One key at a time.
17827
18173
  */
17828
- revert: import_zod13.z.string().min(1).max(64).optional()
18174
+ revert: import_zod14.z.string().min(1).max(64).optional()
17829
18175
  }).strict().refine((request) => [request.resume, request.refresh, request.restore, request.revert !== void 0].filter(Boolean).length <= 1, {
17830
18176
  message: "resume, refresh, restore, and revert are exclusive"
17831
18177
  }).refine((request) => request.revert === void 0 || request.keys.length === 1, { message: "revert takes one key" });
@@ -18104,7 +18450,7 @@ function createGenerateHandlers(opts) {
18104
18450
  }
18105
18451
  }
18106
18452
  const job = {
18107
- id: (0, import_node_crypto9.randomBytes)(8).toString("hex"),
18453
+ id: (0, import_node_crypto10.randomBytes)(8).toString("hex"),
18108
18454
  project: request.project ?? null,
18109
18455
  keys: request.keys,
18110
18456
  mode: request.refresh ? "refresh" : request.resume ? "resume" : request.restore ? "restore" : request.revert !== void 0 ? "revert" : "generate",
@@ -18171,19 +18517,19 @@ function createGenerateHandlers(opts) {
18171
18517
  }
18172
18518
 
18173
18519
  // src/pipeline/salvage.ts
18174
- var import_promises29 = require("fs/promises");
18175
- var import_node_fs30 = require("fs");
18176
- var import_node_path35 = __toESM(require("path"), 1);
18520
+ var import_promises30 = require("fs/promises");
18521
+ var import_node_fs32 = require("fs");
18522
+ var import_node_path37 = __toESM(require("path"), 1);
18177
18523
  function providerClaimId(provider, objectId) {
18178
18524
  return `${encodeURIComponent(provider)}:${encodeURIComponent(objectId)}`;
18179
18525
  }
18180
18526
  async function loadClaims(lockPaths, opts = {}) {
18181
18527
  const claimed = /* @__PURE__ */ new Set();
18182
18528
  for (const p of lockPaths) {
18183
- if (!(0, import_node_fs30.existsSync)(p)) throw new Error(`Claim lockfile not found: ${p}`);
18529
+ if (!(0, import_node_fs32.existsSync)(p)) throw new Error(`Claim lockfile not found: ${p}`);
18184
18530
  let parsed;
18185
18531
  try {
18186
- parsed = parseLock(JSON.parse(await (0, import_promises29.readFile)(p, "utf8")));
18532
+ parsed = parseLock(JSON.parse(await (0, import_promises30.readFile)(p, "utf8")));
18187
18533
  } catch {
18188
18534
  throw new Error(`Claim lockfile is malformed: ${p}`);
18189
18535
  }
@@ -18287,20 +18633,20 @@ function matchOrphanStyle(prompt, manifest) {
18287
18633
  return matchStyleByPattern(prompt, manifest);
18288
18634
  }
18289
18635
  async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, claimPaths) {
18290
- const own = import_node_path35.default.resolve(ownManifestPath);
18636
+ const own = import_node_path37.default.resolve(ownManifestPath);
18291
18637
  const siblingManifestPaths = [
18292
18638
  .../* @__PURE__ */ new Set([
18293
18639
  ...workspaceManifestPaths,
18294
- ...claimPaths.map((c) => import_node_path35.default.join(import_node_path35.default.dirname(import_node_path35.default.resolve(c)), "pixelkiln.manifest.json"))
18640
+ ...claimPaths.map((c) => import_node_path37.default.join(import_node_path37.default.dirname(import_node_path37.default.resolve(c)), "pixelkiln.manifest.json"))
18295
18641
  ])
18296
18642
  ];
18297
18643
  const siblings = [];
18298
18644
  for (const siblingManifestPath of siblingManifestPaths) {
18299
- if (import_node_path35.default.resolve(siblingManifestPath) === own) continue;
18300
- if (!(0, import_node_fs30.existsSync)(siblingManifestPath)) continue;
18645
+ if (import_node_path37.default.resolve(siblingManifestPath) === own) continue;
18646
+ if (!(0, import_node_fs32.existsSync)(siblingManifestPath)) continue;
18301
18647
  try {
18302
18648
  const { manifest } = await loadManifest(siblingManifestPath);
18303
- siblings.push({ label: import_node_path35.default.basename(import_node_path35.default.dirname(siblingManifestPath)), manifest });
18649
+ siblings.push({ label: import_node_path37.default.basename(import_node_path37.default.dirname(siblingManifestPath)), manifest });
18304
18650
  } catch {
18305
18651
  }
18306
18652
  }
@@ -18420,8 +18766,8 @@ async function applyTags(provider, decisions, existing, opts = {}) {
18420
18766
  }
18421
18767
 
18422
18768
  // src/pick/salvage-server.ts
18423
- var import_promises30 = require("fs/promises");
18424
- var import_node_path36 = __toESM(require("path"), 1);
18769
+ var import_promises31 = require("fs/promises");
18770
+ var import_node_path38 = __toESM(require("path"), 1);
18425
18771
 
18426
18772
  // src/pick/salvage-sheet.ts
18427
18773
  var escapeHtml3 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
@@ -18587,7 +18933,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
18587
18933
  });
18588
18934
  const html = renderSalvageSheet(orphans, {
18589
18935
  styleId: ctx.styleId,
18590
- importDir: import_node_path36.default.relative(process.cwd(), ctx.importDir) || "."
18936
+ importDir: import_node_path38.default.relative(process.cwd(), ctx.importDir) || "."
18591
18937
  });
18592
18938
  const byId = new Map(orphans.map((o) => [o.id, o]));
18593
18939
  const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
@@ -18643,10 +18989,10 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
18643
18989
  if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
18644
18990
  decodePng(buf);
18645
18991
  const assetId = idFromPrompt(orphan.prompt, taken);
18646
- const rel = import_node_path36.default.join("_salvaged", `${assetId}.png`);
18647
- const outFile = import_node_path36.default.resolve(ctx.importDir, rel);
18648
- await (0, import_promises30.mkdir)(import_node_path36.default.dirname(outFile), { recursive: true });
18649
- await (0, import_promises30.writeFile)(outFile, buf);
18992
+ const rel = import_node_path38.default.join("_salvaged", `${assetId}.png`);
18993
+ const outFile = import_node_path38.default.resolve(ctx.importDir, rel);
18994
+ await (0, import_promises31.mkdir)(import_node_path38.default.dirname(outFile), { recursive: true });
18995
+ await (0, import_promises31.writeFile)(outFile, buf);
18650
18996
  ctx.manifest.assets[assetId] = {
18651
18997
  prompt: orphan.prompt,
18652
18998
  promptByStyle: {},
@@ -18678,7 +19024,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
18678
19024
  sourceUrl: durableSource,
18679
19025
  sourceUrls: durableSource ? [{ url: durableSource }] : [],
18680
19026
  outputs: [{
18681
- path: portableOutputPath(outFile, import_node_path36.default.dirname(ctx.manifestPath)),
19027
+ path: portableOutputPath(outFile, import_node_path38.default.dirname(ctx.manifestPath)),
18682
19028
  sha256: sha256(buf)
18683
19029
  }],
18684
19030
  submittedAt: orphan.createdAt,
@@ -18702,9 +19048,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
18702
19048
  }
18703
19049
  }
18704
19050
  await applyTags(provider, decisions, existingTags, { onProgress: log, characters: characterMembers });
18705
- const raw = JSON.parse(await (0, import_promises30.readFile)(ctx.manifestPath, "utf8"));
19051
+ const raw = JSON.parse(await (0, import_promises31.readFile)(ctx.manifestPath, "utf8"));
18706
19052
  for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
18707
- await (0, import_promises30.writeFile)(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
19053
+ await (0, import_promises31.writeFile)(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
18708
19054
  await saveLock(ctx.lockPath, ctx.lock);
18709
19055
  if (characterAssetIds.length) result.characterAssetIds = characterAssetIds;
18710
19056
  return result;