pixelkiln 0.87.0 → 0.88.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
@@ -32,6 +32,7 @@ var index_exports = {};
32
32
  __export(index_exports, {
33
33
  AssetBatchSchema: () => AssetBatchSchema,
34
34
  AssetSchema: () => AssetSchema,
35
+ AssetVfxSchema: () => AssetVfxSchema,
35
36
  BudgetError: () => BudgetError,
36
37
  CHARACTER_DIRECTIONS_4: () => CHARACTER_DIRECTIONS_4,
37
38
  CHARACTER_DIRECTIONS_8: () => CHARACTER_DIRECTIONS_8,
@@ -88,12 +89,14 @@ __export(index_exports, {
88
89
  OverwriteRefusedError: () => OverwriteRefusedError,
89
90
  PIXELATE_COST_GENERATIONS: () => PIXELATE_COST_GENERATIONS,
90
91
  PIXELATE_MAX_SIDE: () => PIXELATE_MAX_SIDE,
92
+ PIXELLAB_MCP_URL: () => PIXELLAB_MCP_URL,
91
93
  PIXEL_ART_FIXER_REVISION: () => PIXEL_ART_FIXER_REVISION,
92
94
  PIXEL_ART_FIXER_URL: () => PIXEL_ART_FIXER_URL,
93
95
  PRO_TIER_PRICES: () => PRO_TIER_PRICES,
94
96
  PixelKilnError: () => PixelKilnError,
95
97
  PixelLabClient: () => PixelLabClient,
96
98
  PixelLabError: () => PixelLabError,
99
+ PixelLabMcpClient: () => PixelLabMcpClient,
97
100
  PixelLabProvider: () => PixelLabProvider,
98
101
  PostprocessSchema: () => PostprocessSchema,
99
102
  ProjectError: () => ProjectError,
@@ -116,12 +119,15 @@ __export(index_exports, {
116
119
  UiPieceSchema: () => UiPieceSchema,
117
120
  UnsupportedCapabilityError: () => UnsupportedCapabilityError,
118
121
  UsageError: () => UsageError,
122
+ VFX_TOOL: () => VFX_TOOL,
123
+ VfxPartSchema: () => VfxPartSchema,
119
124
  WorkspaceProjectSchema: () => WorkspaceProjectSchema,
120
125
  WorkspaceSchema: () => WorkspaceSchema,
121
126
  adopt: () => adopt,
122
127
  applyManifestEdit: () => applyManifestEdit,
123
128
  applyTags: () => applyTags,
124
129
  approveQualityRecord: () => approveQualityRecord,
130
+ assetInStyle: () => assetInStyle,
125
131
  auditStyle: () => auditStyle,
126
132
  availableProviders: () => availableProviders,
127
133
  backoffMs: () => backoffMs,
@@ -148,6 +154,7 @@ __export(index_exports, {
148
154
  defaultOpenCommand: () => defaultOpenCommand,
149
155
  defaultPixelateOut: () => defaultPixelateOut,
150
156
  defaultUnzoomOut: () => defaultUnzoomOut,
157
+ describeVfx: () => describeVfx,
151
158
  detachHandEdit: () => detachHandEdit,
152
159
  detectMediaType: () => detectMediaType,
153
160
  doctor: () => doctor,
@@ -168,6 +175,7 @@ __export(index_exports, {
168
175
  fontOutputPaths: () => fontOutputPaths,
169
176
  formatCost: () => formatCost,
170
177
  frameSetFps: () => frameSetFps,
178
+ frameSetLoop: () => frameSetLoop,
171
179
  galleryContentSecurityPolicy: () => galleryContentSecurityPolicy,
172
180
  galleryMediaId: () => galleryMediaId,
173
181
  galleryMediaRoute: () => galleryMediaRoute,
@@ -190,9 +198,11 @@ __export(index_exports, {
190
198
  inspectRevisionReadiness: () => inspectRevisionReadiness,
191
199
  installEditor: () => installEditor,
192
200
  installRecipe: () => installRecipe,
201
+ isBusyReply: () => isBusyReply,
193
202
  isFrameSetEntry: () => isFrameSetEntry,
194
203
  isMemberSetEntry: () => isMemberSetEntry,
195
204
  isSensitiveSourceUrl: () => isSensitiveSourceUrl,
205
+ isVfxReference: () => isVfxReference,
196
206
  isometricTileCost: () => isometricTileCost,
197
207
  listBundledRecipes: () => listBundledRecipes,
198
208
  loadClaims: () => loadClaims,
@@ -201,7 +211,9 @@ __export(index_exports, {
201
211
  loadSiblingManifests: () => loadSiblingManifests,
202
212
  loadWorkspace: () => loadWorkspace,
203
213
  lockKey: () => lockKey,
214
+ makeVfx: () => makeVfx,
204
215
  matchOrphanStyle: () => matchOrphanStyle,
216
+ mcpClientFromEnv: () => mcpClientFromEnv,
205
217
  measureBalanceChange: () => measureBalanceChange,
206
218
  measureDecodedImageQuality: () => measureDecodedImageQuality,
207
219
  measureImageQuality: () => measureImageQuality,
@@ -225,8 +237,12 @@ __export(index_exports, {
225
237
  paletteDistance: () => paletteDistance,
226
238
  parentAssetId: () => parentAssetId,
227
239
  parseLock: () => parseLock,
240
+ parseMcpBody: () => parseMcpBody,
228
241
  parseProFlashQuote: () => parseProFlashQuote,
229
242
  parseTerrainDescriptions: () => parseTerrainDescriptions,
243
+ parseVfxExport: () => parseVfxExport,
244
+ parseVfxRecipe: () => parseVfxRecipe,
245
+ parseVfxStep: () => parseVfxStep,
230
246
  parseWorkspace: () => parseWorkspace,
231
247
  pickHistory: () => pickHistory,
232
248
  pixelateFile: () => pixelateFile,
@@ -246,6 +262,8 @@ __export(index_exports, {
246
262
  readArtifactBundleManifest: () => readArtifactBundleManifest,
247
263
  readHandEditCompanion: () => readHandEditCompanion,
248
264
  readQualityBaseline: () => readQualityBaseline,
265
+ readSavedVfxRecipe: () => readSavedVfxRecipe,
266
+ readVfxRecipe: () => readVfxRecipe,
249
267
  recipeDigest: () => recipeDigest,
250
268
  referencedHashes: () => referencedHashes,
251
269
  refineAsset: () => refineAsset,
@@ -276,6 +294,7 @@ __export(index_exports, {
276
294
  resolveSpecs: () => resolveSpecs,
277
295
  resolveStyleImages: () => resolveStyleImages,
278
296
  resolveStyleOutputs: () => resolveStyleOutputs,
297
+ resolveVfxSprite: () => resolveVfxSprite,
279
298
  resumeActions: () => resumeActions,
280
299
  resumeCommandForStatus: () => resumeCommandForStatus,
281
300
  retireGeneration: () => retireGeneration,
@@ -283,6 +302,7 @@ __export(index_exports, {
283
302
  revertGeneration: () => revertGeneration,
284
303
  runPicker: () => runPicker,
285
304
  runSalvage: () => runSalvage,
305
+ runVfxWorkbench: () => runVfxWorkbench,
286
306
  saveHandEdit: () => saveHandEdit,
287
307
  saveLock: () => saveLock,
288
308
  saveWorkspace: () => saveWorkspace,
@@ -293,6 +313,7 @@ __export(index_exports, {
293
313
  sha256File: () => sha256File,
294
314
  shouldPersistSourceUrl: () => shouldPersistSourceUrl,
295
315
  shouldRetry: () => shouldRetry,
316
+ sliceVfxSheet: () => sliceVfxSheet,
296
317
  slugify: () => slugify,
297
318
  snapshotQualityBaseline: () => snapshotQualityBaseline,
298
319
  sourceIsStem: () => sourceIsStem,
@@ -317,6 +338,8 @@ __export(index_exports, {
317
338
  validateWorkspace: () => validateWorkspace,
318
339
  verifyArtifactBundle: () => verifyArtifactBundle,
319
340
  verifyRecipe: () => verifyRecipe,
341
+ vfxOutputPaths: () => vfxOutputPaths,
342
+ vfxRecipeSha256: () => vfxRecipeSha256,
320
343
  withArtifactManifest: () => withArtifactManifest,
321
344
  workspaceClaims: () => workspaceClaims,
322
345
  workspaceStatus: () => workspaceStatus,
@@ -332,7 +355,7 @@ var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
332
355
  // src/types.ts
333
356
  var import_zod = require("zod");
334
357
  var MediaTypeSchema = import_zod.z.enum(["image/png", "image/gif"]);
335
- var GeneratorSchema = import_zod.z.enum(["1dir", "map", "pixflux", "tiles", "animation", "frames", "character", "terrain", "imagePro", "isometricTile", "objectPro", "uiAsset", "uiElement", "imageProFlash"]);
358
+ var GeneratorSchema = import_zod.z.enum(["1dir", "map", "pixflux", "tiles", "animation", "frames", "character", "terrain", "imagePro", "isometricTile", "objectPro", "uiAsset", "uiElement", "imageProFlash", "vfx"]);
336
359
  var GridConfidenceSchema = import_zod.z.enum(["low", "medium", "high"]);
337
360
  var RevisionModeSchema = import_zod.z.enum([
338
361
  "image-to-image",
@@ -365,6 +388,7 @@ function candidateCount(size) {
365
388
  return 1;
366
389
  }
367
390
  function generationCost(width, height, generator = "map") {
391
+ if (generator === "vfx") return 0;
368
392
  if (generator === "map" || generator === "pixflux") return 1;
369
393
  if (generator === "imagePro") return proTierCost(width, height);
370
394
  const px = width * height;
@@ -549,6 +573,29 @@ var MapSceneSchema = import_zod.z.object({
549
573
  image: import_zod.z.string().min(1),
550
574
  placement: MapScenePlacementSchema.default({ oval: 0.3 })
551
575
  }).strict();
576
+ var VfxPartSchema = import_zod.z.object({ name: import_zod.z.string().min(1) }).catchall(import_zod.z.unknown());
577
+ var AssetVfxSchema = import_zod.z.object({
578
+ preset: import_zod.z.string().min(1).optional(),
579
+ recipe: import_zod.z.string().min(1).optional(),
580
+ parts: import_zod.z.array(VfxPartSchema).optional(),
581
+ sprite: import_zod.z.string().min(1).optional(),
582
+ fps: import_zod.z.number().int().min(1).max(60).optional(),
583
+ name: import_zod.z.string().min(1).optional()
584
+ }).strict().superRefine((vfx, context) => {
585
+ if (vfx.preset && vfx.recipe) {
586
+ context.addIssue({ code: import_zod.z.ZodIssueCode.custom, message: "start a vfx from a preset or a recipe, not both", path: ["recipe"] });
587
+ }
588
+ if (!vfx.preset && !vfx.recipe && !vfx.parts?.length) {
589
+ context.addIssue({ code: import_zod.z.ZodIssueCode.custom, message: "a vfx needs a preset, a recipe, or at least one part", path: ["preset"] });
590
+ }
591
+ if (vfx.sprite && /^(https?|data):/i.test(vfx.sprite)) {
592
+ context.addIssue({
593
+ code: import_zod.z.ZodIssueCode.custom,
594
+ message: "a vfx sprite is a manifest-relative image or a character:/object:/gallery: reference; a URL has no content to track",
595
+ path: ["sprite"]
596
+ });
597
+ }
598
+ });
552
599
  var CharacterOutfitSchema = import_zod.z.object({
553
600
  /** The loop (a character animation asset) to re-clothe, in the same style. */
554
601
  of: import_zod.z.string().min(1),
@@ -1182,7 +1229,7 @@ var StyleSchema = StyleObjectSchema.refine((s) => !(s.tileFeature && s.styleImag
1182
1229
  message: "tileFeature and styleImages cannot be combined; a connectable set derives its own tile geometry, so remove one or the other",
1183
1230
  path: ["tileFeature"]
1184
1231
  }).superRefine((style, ctx) => {
1185
- if (style.quality && (style.generator === "tiles" || style.generator === "terrain" || style.generator === "animation")) {
1232
+ if (style.quality && (style.generator === "tiles" || style.generator === "terrain" || style.generator === "animation" || style.generator === "vfx")) {
1186
1233
  ctx.addIssue({
1187
1234
  code: import_zod.z.ZodIssueCode.custom,
1188
1235
  message: "quality profiles currently support single-image generators only",
@@ -1312,6 +1359,8 @@ var AssetSchema = import_zod.z.object({
1312
1359
  outfit: CharacterOutfitSchema.optional(),
1313
1360
  /** `map` styles only: draw this object in the style of a scene image. See `MapSceneSchema`. */
1314
1361
  scene: MapSceneSchema.optional(),
1362
+ /** `vfx` styles only: the effect to render. See `AssetVfxSchema`. */
1363
+ vfx: AssetVfxSchema.optional(),
1315
1364
  /** `character` styles, `standard` humanoid bases: this character's proportions, over the style's. */
1316
1365
  proportions: CharacterProportionsSchema.optional(),
1317
1366
  /**
@@ -1388,7 +1437,7 @@ var AssetSchema = import_zod.z.object({
1388
1437
  path: ["revision"]
1389
1438
  });
1390
1439
  }
1391
- const shapes = [asset.revision && "revision", asset.state && "state", asset.animation && "animation", asset.mirror && "mirror", asset.batch && "batch", asset.portrait && "portrait", asset.outfit && "outfit", asset.scene && "scene"].filter(Boolean);
1440
+ const shapes = [asset.revision && "revision", asset.state && "state", asset.animation && "animation", asset.mirror && "mirror", asset.batch && "batch", asset.portrait && "portrait", asset.outfit && "outfit", asset.scene && "scene", asset.vfx && "vfx"].filter(Boolean);
1392
1441
  if (shapes.length > 1) {
1393
1442
  context.addIssue({
1394
1443
  code: import_zod.z.ZodIssueCode.custom,
@@ -1403,7 +1452,7 @@ var AssetSchema = import_zod.z.object({
1403
1452
  path: ["mirror"]
1404
1453
  });
1405
1454
  }
1406
- if (asset.prompt === void 0 && !asset.mirror && !asset.portrait && !asset.outfit && !asset.animation?.template) {
1455
+ if (asset.prompt === void 0 && !asset.mirror && !asset.portrait && !asset.outfit && !asset.animation?.template && !asset.vfx) {
1407
1456
  context.addIssue({
1408
1457
  code: import_zod.z.ZodIssueCode.custom,
1409
1458
  message: "Required",
@@ -2163,9 +2212,9 @@ var PixelLabClient = class {
2163
2212
  apiKey;
2164
2213
  http;
2165
2214
  /** Retries transport failures, 429, and 5xx; see http.ts for the policy. */
2166
- async request(path38, init) {
2215
+ async request(path39, init) {
2167
2216
  const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
2168
- const res = await this.http(`${BASE}${path38}`, {
2217
+ const res = await this.http(`${BASE}${path39}`, {
2169
2218
  ...init,
2170
2219
  headers: {
2171
2220
  Authorization: auth,
@@ -2175,13 +2224,13 @@ var PixelLabClient = class {
2175
2224
  });
2176
2225
  const text = await res.text();
2177
2226
  if (!res.ok) {
2178
- throw new PixelLabError(`${init?.method ?? "GET"} ${path38} \u2192 ${res.status}`, res.status, text);
2227
+ throw new PixelLabError(`${init?.method ?? "GET"} ${path39} \u2192 ${res.status}`, res.status, text);
2179
2228
  }
2180
2229
  if (!text) return {};
2181
2230
  try {
2182
2231
  return JSON.parse(text);
2183
2232
  } catch {
2184
- throw new Error(`${init?.method ?? "GET"} ${path38} returned invalid JSON`);
2233
+ throw new Error(`${init?.method ?? "GET"} ${path39} returned invalid JSON`);
2185
2234
  }
2186
2235
  }
2187
2236
  async balance() {
@@ -2586,10 +2635,10 @@ var PixelLabClient = class {
2586
2635
  const palette = args.paletteSwatchBase64 ? { color_image: { type: "base64", base64: args.paletteSwatchBase64, format: "png" }, force_colors: true } : {};
2587
2636
  const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
2588
2637
  const south = args.reference?.south;
2589
- let path38;
2638
+ let path39;
2590
2639
  let body;
2591
2640
  if (args.mode === "v3") {
2592
- path38 = "/create-character-v3";
2641
+ path39 = "/create-character-v3";
2593
2642
  body = {
2594
2643
  description: args.description,
2595
2644
  image_size: imageSize,
@@ -2603,7 +2652,7 @@ var PixelLabClient = class {
2603
2652
  ...args.enhancePrompt ? { enhance_prompt: true } : {}
2604
2653
  };
2605
2654
  } else if (args.mode === "pro-flash") {
2606
- path38 = "/create-character-pro-flash";
2655
+ path39 = "/create-character-pro-flash";
2607
2656
  const traits = args.styleTraits;
2608
2657
  body = {
2609
2658
  description: args.description,
@@ -2627,7 +2676,7 @@ var PixelLabClient = class {
2627
2676
  } : {}
2628
2677
  };
2629
2678
  } else if (args.mode === "pro") {
2630
- path38 = "/create-character-pro";
2679
+ path39 = "/create-character-pro";
2631
2680
  body = {
2632
2681
  description: args.description,
2633
2682
  image_size: imageSize,
@@ -2641,7 +2690,7 @@ var PixelLabClient = class {
2641
2690
  ...!south && args.styleCharacterId ? { style_character_id: args.styleCharacterId } : {}
2642
2691
  };
2643
2692
  } else {
2644
- path38 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
2693
+ path39 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
2645
2694
  body = {
2646
2695
  description: args.description,
2647
2696
  image_size: imageSize,
@@ -2658,8 +2707,8 @@ var PixelLabClient = class {
2658
2707
  ...palette
2659
2708
  };
2660
2709
  }
2661
- const raw = await this.request(path38, { method: "POST", body: JSON.stringify(body) });
2662
- return validateResponse(CharacterSubmitSchema, raw, path38.slice(1));
2710
+ const raw = await this.request(path39, { method: "POST", body: JSON.stringify(body) });
2711
+ return validateResponse(CharacterSubmitSchema, raw, path39.slice(1));
2663
2712
  }
2664
2713
  /** A text edit of an existing character, applied to every direction and stored as a sibling. */
2665
2714
  async createCharacterState(args) {
@@ -2959,8 +3008,8 @@ var PixelLabClient = class {
2959
3008
  /**
2960
3009
  * `/image-to-pixelart-pro-flash`: a photo, painting, or render in, pixel
2961
3010
  * art out, with no size to pass: PixelLab detects the native pixel scale
2962
- * and picks the output size (a 256x256 render came back 108x109, on a
2963
- * transparent background). A flat 6 generations, billed exactly that live
3011
+ * and picks the output size (a 256x256 render came back 108x109). The
3012
+ * result is opaque, on a near-white background. A flat 6 generations, billed exactly that live
2964
3013
  * in October 2026. Sources over 2048px on the longer side are scaled down
2965
3014
  * upstream. The finished job carries the image at `last_response.images[0]`.
2966
3015
  */
@@ -3884,10 +3933,10 @@ function imageMetadata(buf) {
3884
3933
  }
3885
3934
 
3886
3935
  // src/providers/pixellab.ts
3887
- var import_node_fs2 = require("fs");
3936
+ var import_node_fs3 = require("fs");
3888
3937
  var import_node_crypto2 = require("crypto");
3889
3938
  var import_node_os = __toESM(require("os"), 1);
3890
- var import_node_path = __toESM(require("path"), 1);
3939
+ var import_node_path2 = __toESM(require("path"), 1);
3891
3940
 
3892
3941
  // src/hash.ts
3893
3942
  var import_node_crypto = require("crypto");
@@ -3895,16 +3944,17 @@ var import_node_fs = require("fs");
3895
3944
  function sha256(data) {
3896
3945
  return (0, import_node_crypto.createHash)("sha256").update(data).digest("hex");
3897
3946
  }
3898
- async function sha256File(path38) {
3947
+ async function sha256File(path39) {
3899
3948
  return new Promise((resolve, reject) => {
3900
3949
  const hash = (0, import_node_crypto.createHash)("sha256");
3901
- const stream = (0, import_node_fs.createReadStream)(path38);
3950
+ const stream = (0, import_node_fs.createReadStream)(path39);
3902
3951
  stream.on("data", (chunk2) => hash.update(chunk2));
3903
3952
  stream.on("error", reject);
3904
3953
  stream.on("end", () => resolve(hash.digest("hex")));
3905
3954
  });
3906
3955
  }
3907
3956
  function specHash(spec, styleImageHashes, providerOptionIdentity = spec.providerOptions, providerInputIdentity = spec.providerInputs ?? {}) {
3957
+ const effect = Boolean(spec.vfx);
3908
3958
  return sha256(
3909
3959
  JSON.stringify({
3910
3960
  // Preserve every existing PixelLab hash while making a provider switch
@@ -3913,15 +3963,15 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
3913
3963
  providerOptions: providerOptionIdentity && (typeof providerOptionIdentity !== "object" || Object.keys(providerOptionIdentity).length > 0) ? providerOptionIdentity : void 0,
3914
3964
  providerInputs: providerInputIdentity && (typeof providerInputIdentity !== "object" || Object.keys(providerInputIdentity).length > 0) ? providerInputIdentity : void 0,
3915
3965
  generator: spec.generator,
3916
- prompt: spec.prompt,
3917
- width: spec.width,
3918
- height: spec.height,
3919
- view: spec.view,
3920
- outline: spec.outline ?? null,
3921
- shading: spec.shading ?? null,
3922
- detail: spec.detail ?? null,
3923
- seed: spec.seed ?? null,
3924
- palette: spec.palette,
3966
+ prompt: effect ? void 0 : spec.prompt,
3967
+ width: effect ? void 0 : spec.width,
3968
+ height: effect ? void 0 : spec.height,
3969
+ view: effect ? void 0 : spec.view,
3970
+ outline: effect ? void 0 : spec.outline ?? null,
3971
+ shading: effect ? void 0 : spec.shading ?? null,
3972
+ detail: effect ? void 0 : spec.detail ?? null,
3973
+ seed: effect ? void 0 : spec.seed ?? null,
3974
+ palette: effect ? void 0 : spec.palette,
3925
3975
  // `noBackground` only reaches the wire for pixflux and uiAsset; the
3926
3976
  // tile fields are undefined for every other generator. `tileSize` is
3927
3977
  // intentionally absent; width/height are derived from it, so it is
@@ -3996,6 +4046,15 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
3996
4046
  styleTraits: spec.objectPro.styleTraits,
3997
4047
  reference: spec.objectPro.reference?.sha256
3998
4048
  } : void 0,
4049
+ // A recipe and a local sprite enter as their contents' hashes, never
4050
+ // their paths; the My effects name is not identity.
4051
+ vfx: spec.vfx ? {
4052
+ preset: spec.vfx.preset,
4053
+ recipe: spec.vfx.recipe?.sha256,
4054
+ parts: spec.vfx.parts,
4055
+ fps: spec.vfx.fps,
4056
+ sprite: spec.vfx.sprite ? "reference" in spec.vfx.sprite ? spec.vfx.sprite.reference : spec.vfx.sprite.sha256 : void 0
4057
+ } : void 0,
3999
4058
  // A mirror's bytes come from its source's recorded outputs; the plan
4000
4059
  // compares those directly, so only the choice of source is identity.
4001
4060
  mirror: spec.mirror ? { of: spec.mirror.sourceAssetId } : void 0,
@@ -4033,6 +4092,337 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
4033
4092
  );
4034
4093
  }
4035
4094
 
4095
+ // src/pixellab-mcp.ts
4096
+ var PIXELLAB_MCP_URL = "https://api.pixellab.ai/mcp";
4097
+ var PROTOCOL_VERSION = "2025-06-18";
4098
+ function parseMcpBody(body) {
4099
+ const trimmed = body.trim();
4100
+ if (trimmed.startsWith("{")) return JSON.parse(trimmed);
4101
+ let last;
4102
+ for (const line of body.split(/\r?\n/)) {
4103
+ if (!line.startsWith("data:")) continue;
4104
+ const payload = line.slice(5).trim();
4105
+ if (!payload) continue;
4106
+ const message6 = JSON.parse(payload);
4107
+ if ("id" in message6) last = message6;
4108
+ }
4109
+ if (!last) throw new Error(`PixelLab MCP reply had no JSON-RPC response: ${body.slice(0, 200)}`);
4110
+ return last;
4111
+ }
4112
+ function isBusyReply(text) {
4113
+ return /\bbusy\b.*\bretry\b/i.test(text);
4114
+ }
4115
+ var PixelLabMcpClient = class {
4116
+ constructor(apiKey, opts = {}) {
4117
+ this.apiKey = apiKey;
4118
+ if (!apiKey) throw new Error("PIXELLAB_API_KEY is required");
4119
+ this.url = opts.url ?? PIXELLAB_MCP_URL;
4120
+ this.http = fetchWithRetry(opts.fetch, { timeoutMs: opts.timeoutMs ?? 18e4 });
4121
+ this.busyRetries = opts.busyRetries ?? 4;
4122
+ this.sleep = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
4123
+ }
4124
+ apiKey;
4125
+ http;
4126
+ url;
4127
+ busyRetries;
4128
+ sleep;
4129
+ nextId = 1;
4130
+ /**
4131
+ * Call one tool and return its text and images. A tool-level error
4132
+ * (`isError`) throws with the tool's own message, after a few spaced
4133
+ * retries when the tool only said it was busy.
4134
+ */
4135
+ async callTool(name, args) {
4136
+ for (let attempt = 0; ; attempt++) {
4137
+ const reply = await this.callOnce(name, args);
4138
+ if (!reply.isError) return { text: reply.text, images: reply.images };
4139
+ if (attempt < this.busyRetries && isBusyReply(reply.text)) {
4140
+ await this.sleep(2e3 * 2 ** attempt);
4141
+ continue;
4142
+ }
4143
+ throw new Error(`PixelLab ${name}: ${reply.text || "the tool reported an error"}`);
4144
+ }
4145
+ }
4146
+ async callOnce(name, args) {
4147
+ const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
4148
+ const id = this.nextId++;
4149
+ const res = await this.http(this.url, {
4150
+ method: "POST",
4151
+ headers: {
4152
+ Authorization: auth,
4153
+ "Content-Type": "application/json",
4154
+ Accept: "application/json, text/event-stream",
4155
+ "MCP-Protocol-Version": PROTOCOL_VERSION
4156
+ },
4157
+ body: JSON.stringify({ jsonrpc: "2.0", id, method: "tools/call", params: { name, arguments: args } })
4158
+ });
4159
+ const body = await res.text();
4160
+ if (!res.ok) throw new PixelLabError(`MCP ${name} \u2192 ${res.status}`, res.status, body);
4161
+ const message6 = parseMcpBody(body);
4162
+ const error = message6.error;
4163
+ if (error) throw new Error(`PixelLab MCP ${name} failed: ${error.message ?? JSON.stringify(error)}`);
4164
+ const result = message6.result ?? {};
4165
+ const text = (result.content ?? []).filter((block) => block.type === "text" && typeof block.text === "string").map((block) => block.text).join("\n");
4166
+ const images = (result.content ?? []).filter((block) => block.type === "image" && typeof block.data === "string").map((block) => ({ mimeType: String(block.mimeType ?? "image/png"), data: block.data }));
4167
+ return { text, images, isError: Boolean(result.isError) };
4168
+ }
4169
+ };
4170
+ function mcpClientFromEnv() {
4171
+ const key = process.env.PIXELLAB_API_KEY;
4172
+ if (!key) {
4173
+ throw new Error(
4174
+ "PIXELLAB_API_KEY is not set.\nLooked in the environment, and in .env.local / .env beside the manifest and in the current directory."
4175
+ );
4176
+ }
4177
+ return new PixelLabMcpClient(key);
4178
+ }
4179
+
4180
+ // src/pixellab-vfx.ts
4181
+ var import_node_fs2 = require("fs");
4182
+ var import_promises = require("fs/promises");
4183
+ var import_node_path = __toESM(require("path"), 1);
4184
+ var VFX_TOOL = "vfx_workbench";
4185
+ var UUID = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
4186
+ function parseVfxStep(text) {
4187
+ const match = new RegExp(`effect step: (${UUID})`).exec(text);
4188
+ if (!match) throw new Error(`PixelLab vfx_workbench returned no step id:
4189
+ ${text.slice(0, 600)}`);
4190
+ return match[1];
4191
+ }
4192
+ function parseVfxRecipe(text) {
4193
+ const lines = text.split(/\r?\n/);
4194
+ const at = lines.findIndex((line) => line.trim() === "working recipe:");
4195
+ const json = at >= 0 ? lines[at + 1]?.trim() : void 0;
4196
+ if (!json?.startsWith("{")) return null;
4197
+ return JSON.parse(json);
4198
+ }
4199
+ function parseVfxExport(text) {
4200
+ const effectId = new RegExp(`effect id (${UUID})`).exec(text)?.[1];
4201
+ const download = /download (https:\/\/\S+\/): (.+)$/m.exec(text);
4202
+ const size = /(\d+)x(\d+), (\d+) frames @ ([\d.]+) fps, ([^;\n]+)/.exec(text);
4203
+ if (!effectId || !download || !size) {
4204
+ throw new Error(`PixelLab vfx_workbench export reply was not understood:
4205
+ ${text.slice(0, 600)}`);
4206
+ }
4207
+ const anchor = /anchor[^\n]*? at (-?\d+),\s*(-?\d+)/.exec(text);
4208
+ return {
4209
+ effectId,
4210
+ editUrl: /(https:\/\/pixellab\.ai\/create-vfx\?effect=[0-9a-f-]+)/.exec(text)?.[1] ?? null,
4211
+ width: Number(size[1]),
4212
+ height: Number(size[2]),
4213
+ frames: Number(size[3]),
4214
+ fps: Number(size[4]),
4215
+ loop: !/plays once/.test(size[5]),
4216
+ anchor: anchor ? { x: Number(anchor[1]), y: Number(anchor[2]) } : null,
4217
+ downloadBase: download[1],
4218
+ files: download[2].split(",").map((file) => file.trim()).filter(Boolean),
4219
+ imageId: new RegExp(`image id: (${UUID})`).exec(text)?.[1] ?? null
4220
+ };
4221
+ }
4222
+ async function describeVfx(client2, topic) {
4223
+ return (await client2.callTool(VFX_TOOL, { argv: ["describe", topic] })).text;
4224
+ }
4225
+ function vfxOutputPaths(out) {
4226
+ const base = import_node_path.default.resolve(out.replace(/\.(png|gif|json|zip)$/i, "").replace(/\.vfx$/i, ""));
4227
+ return {
4228
+ sheet: `${base}.png`,
4229
+ gif: `${base}.gif`,
4230
+ atlas: `${base}.json`,
4231
+ provenance: `${base}.vfx.json`,
4232
+ gamePack: `${base}-game.zip`
4233
+ };
4234
+ }
4235
+ function isVfxReference(sprite) {
4236
+ return /^(character|object|gallery):/.test(sprite) || /^(https?|data):/.test(sprite);
4237
+ }
4238
+ async function resolveVfxSprite(sprite) {
4239
+ if (isVfxReference(sprite)) return sprite;
4240
+ const bytes = await (0, import_promises.readFile)(sprite);
4241
+ const metadata = imageMetadata(bytes);
4242
+ if (!metadata) throw new Error(`${sprite} is not a readable PNG or JPEG`);
4243
+ return `data:image/${metadata.format};base64,${bytes.toString("base64")}`;
4244
+ }
4245
+ async function readVfxRecipe(file) {
4246
+ const parsed = JSON.parse(await (0, import_promises.readFile)(file, "utf8"));
4247
+ const record = parsed.tool === VFX_TOOL && parsed.recipe && typeof parsed.recipe === "object";
4248
+ const recipe = record ? parsed.recipe : parsed;
4249
+ if (!Array.isArray(recipe.layers)) throw new Error(`${file} is not a VFX recipe: it has no "layers" list`);
4250
+ const recorded = record ? parsed.source?.sprite : void 0;
4251
+ if (typeof recorded !== "string") return { recipe };
4252
+ const sprite = isVfxReference(recorded) ? recorded : import_node_path.default.resolve(import_node_path.default.dirname(import_node_path.default.resolve(file)), recorded);
4253
+ return { recipe, sprite };
4254
+ }
4255
+ async function defaultDownload(url) {
4256
+ const http = fetchWithRetry();
4257
+ const res = await http(url);
4258
+ if (!res.ok) throw new Error(`download ${url} \u2192 ${res.status}`);
4259
+ const buf = Buffer.from(await res.arrayBuffer());
4260
+ if (buf.length > MAX_DOWNLOAD_BYTES) throw new Error(`download ${url} exceeds the ${MAX_DOWNLOAD_BYTES}-byte safety limit`);
4261
+ return buf;
4262
+ }
4263
+ function localPathFor(file, paths) {
4264
+ if (/-game\.zip$/i.test(file)) return paths.gamePack;
4265
+ if (/\.gif$/i.test(file)) return paths.gif;
4266
+ if (/\.json$/i.test(file)) return paths.atlas;
4267
+ if (/\.png$/i.test(file)) return paths.sheet;
4268
+ return null;
4269
+ }
4270
+ async function runVfxWorkbench(client2, opts) {
4271
+ const starts = [opts.preset, opts.recipe, opts.effect].filter((value) => value !== void 0).length;
4272
+ if (starts > 1) throw new Error("Start a VFX from one of a preset, a recipe, or a saved effect, not several");
4273
+ if (starts === 0 && !opts.parts?.length) {
4274
+ throw new Error("A VFX needs something to start from: a preset, a recipe, a saved effect, or at least one part");
4275
+ }
4276
+ const progress = opts.onProgress ?? (() => {
4277
+ });
4278
+ const call = async (argv) => (await client2.callTool(VFX_TOOL, { argv, ...opts.notes ? { notes: opts.notes } : {} })).text;
4279
+ const startArgv = ["start"];
4280
+ if (opts.sprite) startArgv.push("--sprite", opts.sprite);
4281
+ if (opts.preset) startArgv.push("--preset", opts.preset);
4282
+ if (opts.effect) startArgv.push("--effect", opts.effect);
4283
+ progress(opts.preset ? `starting from preset ${opts.preset}` : opts.effect ? `starting from saved effect ${opts.effect}` : "starting");
4284
+ const started = await call(startArgv);
4285
+ let step = parseVfxStep(started);
4286
+ const savedName = opts.effect ? parseVfxRecipe(started)?.name : opts.recipe?.name;
4287
+ const name = opts.name ?? (typeof savedName === "string" ? savedName : void 0) ?? opts.defaultName;
4288
+ if (opts.recipe) {
4289
+ progress("rendering the recipe");
4290
+ step = parseVfxStep(await call([step, "render", JSON.stringify({ recipe: opts.recipe })]));
4291
+ }
4292
+ if (opts.parts?.length) {
4293
+ progress(`adding ${opts.parts.map((part) => part.name).join(", ")}`);
4294
+ step = parseVfxStep(await call([step, "add_part", JSON.stringify({ parts: opts.parts })]));
4295
+ }
4296
+ if (opts.fps !== void 0) {
4297
+ progress(`setting ${opts.fps} fps`);
4298
+ step = parseVfxStep(await call([step, "render", JSON.stringify({ patch: [{ op: "set", path: "fps", value: opts.fps }] })]));
4299
+ }
4300
+ const exportArgv = [step, "export", "--name", name];
4301
+ if (opts.replace) exportArgv.push("--replace", opts.replace);
4302
+ if (opts.gamePack) exportArgv.push("--game-pack");
4303
+ progress(opts.gamePack ? "exporting with a game pack" : "exporting");
4304
+ const exported = parseVfxExport(await call(exportArgv));
4305
+ await opts.onExported?.(exported, name);
4306
+ progress("reading the saved recipe");
4307
+ const recipe = parseVfxRecipe(await call(["start", "--effect", exported.effectId]));
4308
+ return { exported, recipe, name };
4309
+ }
4310
+ async function readSavedVfxRecipe(client2, effectId) {
4311
+ try {
4312
+ return parseVfxRecipe((await client2.callTool(VFX_TOOL, { argv: ["start", "--effect", effectId] })).text);
4313
+ } catch (error) {
4314
+ if (error instanceof Error && /no saved effect/i.test(error.message)) return null;
4315
+ throw error;
4316
+ }
4317
+ }
4318
+ function vfxRecipeSha256(recipe) {
4319
+ const { name: _name, notes: _notes, ...rest } = recipe;
4320
+ return sha256(JSON.stringify(rest));
4321
+ }
4322
+ function sliceVfxSheet(sheet, facts, atlas) {
4323
+ const image = decodePng(sheet);
4324
+ const listed = atlas?.frames;
4325
+ const rects = Array.isArray(listed) && listed.length === facts.frames ? listed.map((entry) => entry.frame) : Array.from({ length: facts.frames }, (_, index) => {
4326
+ const columns = Math.max(1, Math.floor(image.width / facts.width));
4327
+ return { x: index % columns * facts.width, y: Math.floor(index / columns) * facts.height, w: facts.width, h: facts.height };
4328
+ });
4329
+ return rects.map((rect, index) => {
4330
+ if (rect.x < 0 || rect.y < 0 || rect.x + rect.w > image.width || rect.y + rect.h > image.height) {
4331
+ throw new Error(
4332
+ `VFX frame ${index + 1} (${rect.w}x${rect.h} at ${rect.x},${rect.y}) falls outside the ${image.width}x${image.height} sheet`
4333
+ );
4334
+ }
4335
+ const out = Buffer.alloc(rect.w * rect.h * 4);
4336
+ for (let y = 0; y < rect.h; y++) {
4337
+ const from = ((rect.y + y) * image.width + rect.x) * 4;
4338
+ image.pixels.copy(out, y * rect.w * 4, from, from + rect.w * 4);
4339
+ }
4340
+ return encodeRgbaPng(rect.w, rect.h, out);
4341
+ });
4342
+ }
4343
+ async function makeVfx(client2, opts) {
4344
+ const starts = [opts.preset, opts.recipe, opts.effect].filter((value) => value !== void 0).length;
4345
+ if (starts > 1) throw new Error("Start a VFX from one of a preset, a recipe, or a saved effect, not several");
4346
+ if (starts === 0 && !opts.parts?.length) {
4347
+ throw new Error("A VFX needs something to start from: a preset, a recipe, a saved effect, or at least one part");
4348
+ }
4349
+ const paths = vfxOutputPaths(opts.out);
4350
+ const wanted = [paths.sheet, paths.gif, paths.atlas, paths.provenance, ...opts.gamePack ? [paths.gamePack] : []];
4351
+ if (!opts.force) {
4352
+ for (const file of wanted) {
4353
+ if ((0, import_node_fs2.existsSync)(file)) throw new Error(`${file} already exists; pass --force to replace it`);
4354
+ }
4355
+ }
4356
+ const progress = opts.onProgress ?? (() => {
4357
+ });
4358
+ const { exported, recipe, name } = await runVfxWorkbench(client2, {
4359
+ preset: opts.preset,
4360
+ recipe: opts.recipe,
4361
+ effect: opts.effect,
4362
+ parts: opts.parts,
4363
+ sprite: opts.sprite ? await resolveVfxSprite(opts.sprite) : void 0,
4364
+ fps: opts.fps,
4365
+ name: opts.name,
4366
+ defaultName: opts.preset ?? import_node_path.default.basename(paths.sheet, ".png"),
4367
+ replace: opts.replace ?? opts.effect,
4368
+ gamePack: opts.gamePack,
4369
+ notes: "pixelkiln vfx",
4370
+ onProgress: progress
4371
+ });
4372
+ const download = opts.download ?? defaultDownload;
4373
+ const written = { sheet: paths.sheet, gif: paths.gif, atlas: paths.atlas, provenance: paths.provenance };
4374
+ await (0, import_promises.mkdir)(import_node_path.default.dirname(paths.sheet), { recursive: true });
4375
+ for (const file of exported.files) {
4376
+ const target = localPathFor(file, paths);
4377
+ if (!target) continue;
4378
+ let bytes = await download(`${exported.downloadBase}${encodeURIComponent(file)}`);
4379
+ if (target === paths.atlas) {
4380
+ const atlas = JSON.parse(bytes.toString("utf8"));
4381
+ if (atlas.meta) atlas.meta.image = import_node_path.default.basename(paths.sheet);
4382
+ bytes = Buffer.from(`${JSON.stringify(atlas, null, 2)}
4383
+ `);
4384
+ }
4385
+ await (0, import_promises.writeFile)(target, bytes);
4386
+ if (target === paths.gamePack) written.gamePack = target;
4387
+ }
4388
+ for (const file of [paths.sheet, paths.gif, paths.atlas]) {
4389
+ if (!(0, import_node_fs2.existsSync)(file)) throw new Error(`PixelLab's export listed no file for ${import_node_path.default.basename(file)}: ${exported.files.join(", ")}`);
4390
+ }
4391
+ const spriteRecord = !opts.sprite || /^data:/.test(opts.sprite) ? void 0 : isVfxReference(opts.sprite) ? opts.sprite : import_node_path.default.relative(import_node_path.default.dirname(paths.provenance), import_node_path.default.resolve(opts.sprite)).split(import_node_path.default.sep).join("/");
4392
+ const provenance = {
4393
+ version: 1,
4394
+ provider: "pixellab",
4395
+ tool: VFX_TOOL,
4396
+ name,
4397
+ effectId: exported.effectId,
4398
+ editUrl: exported.editUrl,
4399
+ source: {
4400
+ ...opts.preset ? { preset: opts.preset } : {},
4401
+ ...opts.effect ? { effect: opts.effect } : {},
4402
+ ...opts.recipe ? { recipeFile: true } : {},
4403
+ ...opts.parts?.length ? { parts: opts.parts } : {},
4404
+ ...spriteRecord ? { sprite: spriteRecord } : {}
4405
+ },
4406
+ width: exported.width,
4407
+ height: exported.height,
4408
+ frames: exported.frames,
4409
+ fps: exported.fps,
4410
+ loop: exported.loop,
4411
+ anchor: exported.anchor,
4412
+ files: {
4413
+ sheet: import_node_path.default.basename(paths.sheet),
4414
+ gif: import_node_path.default.basename(paths.gif),
4415
+ atlas: import_node_path.default.basename(paths.atlas),
4416
+ ...written.gamePack ? { gamePack: import_node_path.default.basename(written.gamePack) } : {}
4417
+ },
4418
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
4419
+ recipe
4420
+ };
4421
+ await (0, import_promises.writeFile)(paths.provenance, `${JSON.stringify(provenance, null, 2)}
4422
+ `);
4423
+ return { ...exported, paths: written, recipe };
4424
+ }
4425
+
4036
4426
  // src/providers/pixellab.ts
4037
4427
  function pixelLabObjectUrl(generator, objectId) {
4038
4428
  if (!objectId) return null;
@@ -4047,6 +4437,7 @@ function pixelLabObjectUrl(generator, objectId) {
4047
4437
  const characterId = objectId.split("#")[0];
4048
4438
  return characterId ? `https://www.pixellab.ai/create-character/${encodeURIComponent(characterId)}` : null;
4049
4439
  }
4440
+ if (generator === "vfx") return `https://pixellab.ai/create-vfx?effect=${encodeURIComponent(objectId)}`;
4050
4441
  return null;
4051
4442
  }
4052
4443
  function billedFromUsage(usage) {
@@ -4163,13 +4554,20 @@ function decodeObjectAnimationJob(jobId) {
4163
4554
  return null;
4164
4555
  }
4165
4556
  var PixelLabProvider = class _PixelLabProvider {
4166
- constructor(client2) {
4557
+ /**
4558
+ * `mcp` reaches the tools PixelLab offers only over MCP (the VFX
4559
+ * workbench). Offline and download-only providers have none, and a `vfx`
4560
+ * submission through one says so.
4561
+ */
4562
+ constructor(client2, mcp) {
4167
4563
  this.client = client2;
4564
+ this.mcp = mcp;
4168
4565
  }
4169
4566
  client;
4567
+ mcp;
4170
4568
  id = "pixellab";
4171
4569
  static fromEnv() {
4172
- return new _PixelLabProvider(clientFromEnv());
4570
+ return new _PixelLabProvider(clientFromEnv(), mcpClientFromEnv());
4173
4571
  }
4174
4572
  /** Public storage URLs and the local content cache do not require API auth. */
4175
4573
  static forDownloads() {
@@ -4180,7 +4578,7 @@ var PixelLabProvider = class _PixelLabProvider {
4180
4578
  return new _PixelLabProvider(new PixelLabClient("download-only"));
4181
4579
  }
4182
4580
  supports(generator) {
4183
- return generator === "1dir" || generator === "map" || generator === "pixflux" || generator === "tiles" || generator === "terrain" || generator === "imagePro" || generator === "character" || generator === "isometricTile" || generator === "objectPro" || generator === "uiAsset" || generator === "uiElement" || generator === "imageProFlash";
4581
+ return generator === "1dir" || generator === "map" || generator === "pixflux" || generator === "tiles" || generator === "terrain" || generator === "imagePro" || generator === "character" || generator === "isometricTile" || generator === "objectPro" || generator === "uiAsset" || generator === "uiElement" || generator === "imageProFlash" || generator === "vfx";
4184
4582
  }
4185
4583
  /** `/map-objects` style matching: `background_image` plus `inpainting`. */
4186
4584
  supportsMapScene = true;
@@ -4222,11 +4620,12 @@ var PixelLabProvider = class _PixelLabProvider {
4222
4620
  * is a file read.
4223
4621
  */
4224
4622
  static cacheDir(namespace = "pixflux") {
4225
- const dir = import_node_path.default.join(import_node_os.default.tmpdir(), `pixelkiln-${namespace}`);
4226
- (0, import_node_fs2.mkdirSync)(dir, { recursive: true });
4623
+ const dir = import_node_path2.default.join(import_node_os.default.tmpdir(), `pixelkiln-${namespace}`);
4624
+ (0, import_node_fs3.mkdirSync)(dir, { recursive: true });
4227
4625
  return dir;
4228
4626
  }
4229
4627
  estimate(spec) {
4628
+ if (spec.vfx) return { unit: "generations", amount: 0, candidates: 1 };
4230
4629
  if (spec.batch?.role === "member") {
4231
4630
  return { unit: "generations", amount: 0, candidates: 1 };
4232
4631
  }
@@ -4284,6 +4683,9 @@ var PixelLabProvider = class _PixelLabProvider {
4284
4683
  };
4285
4684
  }
4286
4685
  validate(spec, styleImages) {
4686
+ if (spec.generator === "vfx" && styleImages.length) {
4687
+ throw new Error("PixelLab vfx takes no style images; an effect's look comes from its preset, parts, or recipe");
4688
+ }
4287
4689
  const members = spec.revision?.sourceMembers;
4288
4690
  if (spec.revision && members && !MEMBER_SET_REVISION_MODES.includes(spec.revision.mode)) {
4289
4691
  throw new Error(
@@ -4568,7 +4970,7 @@ var PixelLabProvider = class _PixelLabProvider {
4568
4970
  }
4569
4971
  }
4570
4972
  if (endFrame) {
4571
- const start = startFrame ?? (character.parentFile && (0, import_node_fs2.existsSync)(character.parentFile) ? imageMetadata((0, import_node_fs2.readFileSync)(character.parentFile)) : null);
4973
+ const start = startFrame ?? (character.parentFile && (0, import_node_fs3.existsSync)(character.parentFile) ? imageMetadata((0, import_node_fs3.readFileSync)(character.parentFile)) : null);
4572
4974
  if (start && (start.width !== endFrame.width || start.height !== endFrame.height)) {
4573
4975
  throw new Error(
4574
4976
  `${label}: end frame is ${endFrame.width}x${endFrame.height} but the ${startFrame ? "start frame" : "character's rotation"} is ${start.width}x${start.height}; PixelLab interpolates between frames of one size`
@@ -4726,7 +5128,7 @@ var PixelLabProvider = class _PixelLabProvider {
4726
5128
  }
4727
5129
  }
4728
5130
  if (endFrame) {
4729
- const start = startFrame ?? (object.parentFile && (0, import_node_fs2.existsSync)(object.parentFile) ? imageMetadata((0, import_node_fs2.readFileSync)(object.parentFile)) : null);
5131
+ const start = startFrame ?? (object.parentFile && (0, import_node_fs3.existsSync)(object.parentFile) ? imageMetadata((0, import_node_fs3.readFileSync)(object.parentFile)) : null);
4730
5132
  if (start && (start.width !== endFrame.width || start.height !== endFrame.height)) {
4731
5133
  throw new Error(
4732
5134
  `${label}: end frame is ${endFrame.width}x${endFrame.height} but the ${startFrame ? "start frame" : "object's rotation"} is ${start.width}x${start.height}; PixelLab interpolates between frames of one size`
@@ -4781,13 +5183,13 @@ var PixelLabProvider = class _PixelLabProvider {
4781
5183
  );
4782
5184
  }
4783
5185
  const frames = parentOutputs.slice(0, 16).map((output) => {
4784
- const bytes = (0, import_node_fs2.readFileSync)(output.path);
5186
+ const bytes = (0, import_node_fs3.readFileSync)(output.path);
4785
5187
  const dims = imageMetadata(bytes);
4786
5188
  if (!dims) throw new Error(`${spec.styleId}/${spec.assetId}: outfit source frame is not a readable PNG: ${output.path}`);
4787
5189
  return { base64: bytes.toString("base64"), format: "png", width: dims.width, height: dims.height };
4788
5190
  });
4789
5191
  const outfit = character.outfit;
4790
- const referenceBytes = (0, import_node_fs2.readFileSync)(outfit.reference.path);
5192
+ const referenceBytes = (0, import_node_fs3.readFileSync)(outfit.reference.path);
4791
5193
  if (sha256(referenceBytes) !== outfit.reference.sha256) {
4792
5194
  throw new Error(`${spec.styleId}/${spec.assetId}: outfit reference changed after the manifest was resolved: ${outfit.reference.path}`);
4793
5195
  }
@@ -4830,7 +5232,7 @@ var PixelLabProvider = class _PixelLabProvider {
4830
5232
  }
4831
5233
  if (character.kind === "portrait") {
4832
5234
  const parentFile = character.parentFile;
4833
- const bytes = (0, import_node_fs2.readFileSync)(parentFile);
5235
+ const bytes = (0, import_node_fs3.readFileSync)(parentFile);
4834
5236
  if (character.parentSha256 && sha256(bytes) !== character.parentSha256) {
4835
5237
  throw new Error(`${spec.styleId}/${spec.assetId}: portrait source changed after the manifest was resolved: ${parentFile}`);
4836
5238
  }
@@ -5036,8 +5438,8 @@ var PixelLabProvider = class _PixelLabProvider {
5036
5438
  const sources = images.map((image, index) => {
5037
5439
  if (!image.base64) throw new Error(`outfit transfer frame ${index} has no image data`);
5038
5440
  const role = meta?.frameRoles?.[index] ?? void 0;
5039
- const file = import_node_path.default.join(dir, `${jobId}-${index}.png`);
5040
- (0, import_node_fs2.writeFileSync)(file, Buffer.from(image.base64, "base64"));
5441
+ const file = import_node_path2.default.join(dir, `${jobId}-${index}.png`);
5442
+ (0, import_node_fs3.writeFileSync)(file, Buffer.from(image.base64, "base64"));
5041
5443
  return { url: `file://${file}`, ...role ? { role } : {} };
5042
5444
  });
5043
5445
  return {
@@ -5188,6 +5590,7 @@ var PixelLabProvider = class _PixelLabProvider {
5188
5590
  async submit(spec, styleImages, context) {
5189
5591
  this.validate(spec, styleImages);
5190
5592
  if (spec.revision) return this.submitRevision(spec);
5593
+ if (spec.vfx) return this.submitVfx(spec, context);
5191
5594
  if (spec.generator === "character") return this.submitCharacter(spec, styleImages, context);
5192
5595
  if (spec.generator === "objectPro") return this.submitObjectPro(spec, styleImages, context);
5193
5596
  if (spec.generator === "uiAsset") {
@@ -5241,7 +5644,7 @@ var PixelLabProvider = class _PixelLabProvider {
5241
5644
  seed: spec.seed
5242
5645
  });
5243
5646
  const jobId = (0, import_node_crypto2.randomUUID)();
5244
- (0, import_node_fs2.writeFileSync)(import_node_path.default.join(_PixelLabProvider.cacheDir(), `${jobId}.png`), png);
5647
+ (0, import_node_fs3.writeFileSync)(import_node_path2.default.join(_PixelLabProvider.cacheDir(), `${jobId}.png`), png);
5245
5648
  return { jobId };
5246
5649
  }
5247
5650
  if (spec.generator === "tiles") {
@@ -5361,7 +5764,7 @@ var PixelLabProvider = class _PixelLabProvider {
5361
5764
  }
5362
5765
  const sourceFiles = revision.sourceMembers ?? [{ file: revision.sourceFile, sha256: revision.sourceSha256, format: revision.sourceFormat }];
5363
5766
  const images = sourceFiles.map((member) => {
5364
- const bytes = (0, import_node_fs2.readFileSync)(member.file);
5767
+ const bytes = (0, import_node_fs3.readFileSync)(member.file);
5365
5768
  if (sha256(bytes) !== member.sha256) {
5366
5769
  throw new Error(`${spec.styleId}/${spec.assetId}: revision source changed after the manifest was resolved`);
5367
5770
  }
@@ -5376,7 +5779,7 @@ var PixelLabProvider = class _PixelLabProvider {
5376
5779
  if (!revision.maskFile || !revision.maskSha256 || !revision.maskFormat) {
5377
5780
  throw new Error(`${spec.styleId}/${spec.assetId}: revision mask is not ready`);
5378
5781
  }
5379
- const maskBytes = (0, import_node_fs2.readFileSync)(revision.maskFile);
5782
+ const maskBytes = (0, import_node_fs3.readFileSync)(revision.maskFile);
5380
5783
  if (sha256(maskBytes) !== revision.maskSha256) {
5381
5784
  throw new Error(`${spec.styleId}/${spec.assetId}: revision mask changed after the manifest was resolved`);
5382
5785
  }
@@ -5401,7 +5804,7 @@ var PixelLabProvider = class _PixelLabProvider {
5401
5804
  if (!revision.maskFile || !revision.maskSha256 || !revision.maskFormat) {
5402
5805
  throw new Error(`${spec.styleId}/${spec.assetId}: revision mask is not ready`);
5403
5806
  }
5404
- const maskBytes = (0, import_node_fs2.readFileSync)(revision.maskFile);
5807
+ const maskBytes = (0, import_node_fs3.readFileSync)(revision.maskFile);
5405
5808
  if (sha256(maskBytes) !== revision.maskSha256) {
5406
5809
  throw new Error(`${spec.styleId}/${spec.assetId}: revision mask changed after the manifest was resolved`);
5407
5810
  }
@@ -5422,7 +5825,7 @@ var PixelLabProvider = class _PixelLabProvider {
5422
5825
  if (!revision.paletteImageSha256 || !revision.paletteImageFormat) {
5423
5826
  throw new Error(`${spec.styleId}/${spec.assetId}: revision palette image is not ready`);
5424
5827
  }
5425
- const paletteBytes = (0, import_node_fs2.readFileSync)(revision.paletteImageFile);
5828
+ const paletteBytes = (0, import_node_fs3.readFileSync)(revision.paletteImageFile);
5426
5829
  if (sha256(paletteBytes) !== revision.paletteImageSha256) {
5427
5830
  throw new Error(`${spec.styleId}/${spec.assetId}: revision palette image changed after the manifest was resolved`);
5428
5831
  }
@@ -5513,7 +5916,7 @@ var PixelLabProvider = class _PixelLabProvider {
5513
5916
  if (!revision.keypointsFile || !revision.keypointsSha256 || !revision.skeleton) {
5514
5917
  throw new Error(`${spec.styleId}/${spec.assetId}: revision keypoints are not ready`);
5515
5918
  }
5516
- const keypointsBytes = (0, import_node_fs2.readFileSync)(revision.keypointsFile);
5919
+ const keypointsBytes = (0, import_node_fs3.readFileSync)(revision.keypointsFile);
5517
5920
  if (sha256(keypointsBytes) !== revision.keypointsSha256) {
5518
5921
  throw new Error(`${spec.styleId}/${spec.assetId}: revision keypoints changed after the manifest was resolved`);
5519
5922
  }
@@ -5555,7 +5958,7 @@ var PixelLabProvider = class _PixelLabProvider {
5555
5958
  if (!revision.lastFrameSha256 || !revision.lastFrameFormat) {
5556
5959
  throw new Error(`${spec.styleId}/${spec.assetId}: revision last frame is not ready`);
5557
5960
  }
5558
- const bytes = (0, import_node_fs2.readFileSync)(revision.lastFrameFile);
5961
+ const bytes = (0, import_node_fs3.readFileSync)(revision.lastFrameFile);
5559
5962
  if (sha256(bytes) !== revision.lastFrameSha256) {
5560
5963
  throw new Error(`${spec.styleId}/${spec.assetId}: revision last frame changed after the manifest was resolved`);
5561
5964
  }
@@ -5570,11 +5973,11 @@ var PixelLabProvider = class _PixelLabProvider {
5570
5973
  cacheCleanupResult(pngs, usage, roles, fps) {
5571
5974
  const jobId = (0, import_node_crypto2.randomUUID)();
5572
5975
  if (!roles) {
5573
- (0, import_node_fs2.writeFileSync)(import_node_path.default.join(_PixelLabProvider.cacheDir(), `${jobId}.png`), pngs[0]);
5976
+ (0, import_node_fs3.writeFileSync)(import_node_path2.default.join(_PixelLabProvider.cacheDir(), `${jobId}.png`), pngs[0]);
5574
5977
  return { jobId, metadata: { revisionUsage: usage } };
5575
5978
  }
5576
5979
  for (const [index, png] of pngs.entries()) {
5577
- (0, import_node_fs2.writeFileSync)(import_node_path.default.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`), png);
5980
+ (0, import_node_fs3.writeFileSync)(import_node_path2.default.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`), png);
5578
5981
  }
5579
5982
  return { jobId, metadata: { revisionUsage: usage, revisionMembers: roles, ...fps != null ? { revisionFps: fps } : {} } };
5580
5983
  }
@@ -5602,10 +6005,11 @@ var PixelLabProvider = class _PixelLabProvider {
5602
6005
  return this.pollAnimateRevision(jobId, context);
5603
6006
  }
5604
6007
  if (context?.spec?.revision) return this.pollRevision(jobId);
6008
+ if (generator === "vfx") return this.pollVfx(jobId, context);
5605
6009
  if (generator === "imageProFlash") return this.pollRevision(jobId);
5606
6010
  if (generator === "pixflux") {
5607
- const file = import_node_path.default.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
5608
- if ((0, import_node_fs2.existsSync)(file)) {
6011
+ const file = import_node_path2.default.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
6012
+ if ((0, import_node_fs3.existsSync)(file)) {
5609
6013
  const sourceUrl = `file://${file}`;
5610
6014
  return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }] };
5611
6015
  }
@@ -5648,6 +6052,120 @@ var PixelLabProvider = class _PixelLabProvider {
5648
6052
  * background job to ask afterwards the way `billedForJob` does for async
5649
6053
  * work.
5650
6054
  */
6055
+ /**
6056
+ * Render a `vfx` asset with the VFX workbench, which is synchronous and
6057
+ * free. The frames are cut from the exported sheet and parked in the
6058
+ * cache like pixflux's bytes; the export's download folder is recorded so
6059
+ * `poll` can cut them again if the cache is cleared.
6060
+ *
6061
+ * A regeneration exports over the effect this asset rendered last time, so
6062
+ * My effects keeps one entry per asset, unless that effect has changed on
6063
+ * pixellab.ai since (someone moved its sliders) or is gone. Then a new
6064
+ * effect is saved and the edited one is left alone.
6065
+ */
6066
+ async submitVfx(spec, context) {
6067
+ const vfx = spec.vfx;
6068
+ if (!this.mcp) throw new Error("PixelLab vfx renders over PixelLab's MCP server, which this provider was built without");
6069
+ let sprite;
6070
+ if (vfx.sprite && "reference" in vfx.sprite) {
6071
+ sprite = vfx.sprite.reference;
6072
+ } else if (vfx.sprite) {
6073
+ const { file } = vfx.sprite;
6074
+ if (!(0, import_node_fs3.existsSync)(file)) throw new Error(`vfx sprite does not exist yet: ${file}`);
6075
+ const bytes = (0, import_node_fs3.readFileSync)(file);
6076
+ if (sha256(bytes) !== vfx.sprite.sha256) {
6077
+ throw new Error(`vfx sprite changed after the manifest was resolved: ${file}; run the command again`);
6078
+ }
6079
+ const format = imageMetadata(bytes)?.format ?? "png";
6080
+ sprite = `data:image/${format};base64,${bytes.toString("base64")}`;
6081
+ }
6082
+ const previous = context?.replacedMetadata?.vfx ?? context?.lastGenerationMetadata?.vfx;
6083
+ let replace;
6084
+ let newEffect;
6085
+ if (previous?.effectId) {
6086
+ const upstream = await readSavedVfxRecipe(this.mcp, previous.effectId);
6087
+ if (!upstream) newEffect = `effect ${previous.effectId} is no longer in My effects`;
6088
+ else if (!previous.recipe || vfxRecipeSha256(upstream) !== vfxRecipeSha256(previous.recipe)) {
6089
+ newEffect = `effect ${previous.effectId} changed on pixellab.ai since it was rendered; it was kept`;
6090
+ } else replace = previous.effectId;
6091
+ }
6092
+ const jobId = (0, import_node_crypto2.randomUUID)();
6093
+ const facts = (exported2, name2) => ({
6094
+ effectId: exported2.effectId,
6095
+ editUrl: exported2.editUrl,
6096
+ name: name2,
6097
+ imageId: exported2.imageId,
6098
+ width: exported2.width,
6099
+ height: exported2.height,
6100
+ frames: exported2.frames,
6101
+ fps: exported2.fps,
6102
+ loop: exported2.loop,
6103
+ anchor: exported2.anchor,
6104
+ downloadBase: exported2.downloadBase,
6105
+ files: exported2.files,
6106
+ replaced: replace ?? null,
6107
+ ...newEffect ? { newEffect } : {}
6108
+ });
6109
+ const { exported, recipe, name } = await runVfxWorkbench(this.mcp, {
6110
+ preset: vfx.preset,
6111
+ recipe: vfx.recipe?.recipe,
6112
+ parts: vfx.parts,
6113
+ sprite,
6114
+ fps: vfx.fps,
6115
+ name: vfx.name,
6116
+ defaultName: vfx.name,
6117
+ replace,
6118
+ notes: `pixelkiln ${spec.styleId}/${spec.assetId}`,
6119
+ // The effect exists from here on; record it before anything else can fail.
6120
+ onExported: (done, effectName) => context?.checkpoint({ jobId, complete: true, metadata: { vfx: facts(done, effectName) } })
6121
+ });
6122
+ await this.cacheVfxFrames(jobId, exported);
6123
+ return { jobId, metadata: { vfx: { ...facts(exported, name), recipe } } };
6124
+ }
6125
+ /** Cut an export's sheet into frames, with a sidecar `poll` reads without any lock metadata. */
6126
+ async cacheVfxFrames(jobId, exported) {
6127
+ const sheetFile = exported.files.find((file) => /\.png$/i.test(file));
6128
+ const atlasFile = exported.files.find((file) => /\.json$/i.test(file));
6129
+ if (!sheetFile) throw new Error(`PixelLab's vfx export listed no spritesheet: ${exported.files.join(", ")}`);
6130
+ const sheet = await this.client.download(`${exported.downloadBase}${encodeURIComponent(sheetFile)}`);
6131
+ const atlas = atlasFile ? JSON.parse((await this.client.download(`${exported.downloadBase}${encodeURIComponent(atlasFile)}`)).toString("utf8")) : void 0;
6132
+ const frames = sliceVfxSheet(sheet, exported, atlas);
6133
+ const dir = _PixelLabProvider.cacheDir("vfx");
6134
+ frames.forEach((frame, index) => (0, import_node_fs3.writeFileSync)(import_node_path2.default.join(dir, `${jobId}-${index}.png`), frame));
6135
+ (0, import_node_fs3.writeFileSync)(
6136
+ import_node_path2.default.join(dir, `${jobId}.json`),
6137
+ JSON.stringify({ effectId: exported.effectId, count: frames.length, fps: exported.fps, loop: exported.loop })
6138
+ );
6139
+ }
6140
+ async pollVfx(jobId, context) {
6141
+ const dir = _PixelLabProvider.cacheDir("vfx");
6142
+ const sidecar = import_node_path2.default.join(dir, `${jobId}.json`);
6143
+ const cached = () => {
6144
+ if (!(0, import_node_fs3.existsSync)(sidecar)) return null;
6145
+ const facts2 = JSON.parse((0, import_node_fs3.readFileSync)(sidecar, "utf8"));
6146
+ const files2 = Array.from({ length: facts2.count }, (_, index) => import_node_path2.default.join(dir, `${jobId}-${index}.png`));
6147
+ return files2.every((file) => (0, import_node_fs3.existsSync)(file)) ? { facts: facts2, files: files2 } : null;
6148
+ };
6149
+ let found = cached();
6150
+ const recorded = context?.metadata?.vfx;
6151
+ if (!found && recorded?.downloadBase && Array.isArray(recorded.files)) {
6152
+ await this.cacheVfxFrames(jobId, recorded);
6153
+ found = cached();
6154
+ }
6155
+ if (!found) {
6156
+ return { status: "failed", error: "vfx frames are no longer cached locally; re-run submit for this asset (free)" };
6157
+ }
6158
+ const { facts, files } = found;
6159
+ const sources = files.length === 1 ? [{ url: `file://${files[0]}` }] : files.map((file, index) => ({ url: `file://${file}`, role: `frame-${String(index).padStart(2, "0")}` }));
6160
+ return {
6161
+ status: "ready",
6162
+ objectId: facts.effectId,
6163
+ sourceUrl: sources[0].url,
6164
+ sources,
6165
+ ...files.length > 1 ? { metadata: { frameSet: { fps: facts.fps, count: files.length, loop: facts.loop } } } : {},
6166
+ billed: null
6167
+ };
6168
+ }
5651
6169
  async pollCachedRevision(jobId, context) {
5652
6170
  const usage = context?.metadata?.revisionUsage;
5653
6171
  const roles = context?.metadata?.revisionMembers;
@@ -5657,10 +6175,10 @@ var PixelLabProvider = class _PixelLabProvider {
5657
6175
  };
5658
6176
  if (Array.isArray(roles) && roles.length) {
5659
6177
  const sources = roles.map((role, index) => ({
5660
- url: `file://${import_node_path.default.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`)}`,
6178
+ url: `file://${import_node_path2.default.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`)}`,
5661
6179
  role: String(role)
5662
6180
  }));
5663
- if (sources.some((source) => !(0, import_node_fs2.existsSync)(source.url.slice("file://".length)))) return gone;
6181
+ if (sources.some((source) => !(0, import_node_fs3.existsSync)(source.url.slice("file://".length)))) return gone;
5664
6182
  const fps = context?.metadata?.revisionFps;
5665
6183
  const frames = sources.every((source) => source.role.startsWith("frame-"));
5666
6184
  return {
@@ -5672,8 +6190,8 @@ var PixelLabProvider = class _PixelLabProvider {
5672
6190
  billed: billedFromUsage(usage)
5673
6191
  };
5674
6192
  }
5675
- const file = import_node_path.default.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
5676
- if (!(0, import_node_fs2.existsSync)(file)) return gone;
6193
+ const file = import_node_path2.default.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
6194
+ if (!(0, import_node_fs3.existsSync)(file)) return gone;
5677
6195
  const sourceUrl = `file://${file}`;
5678
6196
  return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }], billed: billedFromUsage(usage) };
5679
6197
  }
@@ -5729,8 +6247,8 @@ var PixelLabProvider = class _PixelLabProvider {
5729
6247
  }
5730
6248
  const base64 = extractBase64(item);
5731
6249
  if (base64) {
5732
- const file = import_node_path.default.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`);
5733
- (0, import_node_fs2.writeFileSync)(file, Buffer.from(base64, "base64"));
6250
+ const file = import_node_path2.default.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`);
6251
+ (0, import_node_fs3.writeFileSync)(file, Buffer.from(base64, "base64"));
5734
6252
  urls.push(`file://${file}`);
5735
6253
  continue;
5736
6254
  }
@@ -5780,8 +6298,8 @@ var PixelLabProvider = class _PixelLabProvider {
5780
6298
  const candidate = done[key];
5781
6299
  const base64 = extractBase64(candidate);
5782
6300
  if (base64) {
5783
- const file = import_node_path.default.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
5784
- (0, import_node_fs2.writeFileSync)(file, Buffer.from(base64, "base64"));
6301
+ const file = import_node_path2.default.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
6302
+ (0, import_node_fs3.writeFileSync)(file, Buffer.from(base64, "base64"));
5785
6303
  const sourceUrl = `file://${file}`;
5786
6304
  return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }], billed };
5787
6305
  }
@@ -5789,8 +6307,8 @@ var PixelLabProvider = class _PixelLabProvider {
5789
6307
  if (Array.isArray(done.images) && done.images.length) {
5790
6308
  const base64 = extractBase64(done.images[0]);
5791
6309
  if (base64) {
5792
- const file = import_node_path.default.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
5793
- (0, import_node_fs2.writeFileSync)(file, Buffer.from(base64, "base64"));
6310
+ const file = import_node_path2.default.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
6311
+ (0, import_node_fs3.writeFileSync)(file, Buffer.from(base64, "base64"));
5794
6312
  const sourceUrl = `file://${file}`;
5795
6313
  return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }], billed };
5796
6314
  }
@@ -5823,8 +6341,8 @@ var PixelLabProvider = class _PixelLabProvider {
5823
6341
  for (const [index, candidate] of done.images.entries()) {
5824
6342
  const base64 = extractBase64(candidate);
5825
6343
  if (!base64) continue;
5826
- const file = import_node_path.default.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`);
5827
- (0, import_node_fs2.writeFileSync)(file, Buffer.from(base64, "base64"));
6344
+ const file = import_node_path2.default.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`);
6345
+ (0, import_node_fs3.writeFileSync)(file, Buffer.from(base64, "base64"));
5828
6346
  urls.push(`file://${file}`);
5829
6347
  }
5830
6348
  if (urls.length) return { status: "review", candidateUrls: urls, billed };
@@ -5853,8 +6371,8 @@ var PixelLabProvider = class _PixelLabProvider {
5853
6371
  for (const [index, candidate] of images.entries()) {
5854
6372
  const base64 = extractBase64(candidate);
5855
6373
  if (!base64) continue;
5856
- const file = import_node_path.default.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`);
5857
- (0, import_node_fs2.writeFileSync)(file, Buffer.from(base64, "base64"));
6374
+ const file = import_node_path2.default.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`);
6375
+ (0, import_node_fs3.writeFileSync)(file, Buffer.from(base64, "base64"));
5858
6376
  urls.push(`file://${file}`);
5859
6377
  }
5860
6378
  if (urls.length === 1) return { status: "ready", objectId: jobId, sourceUrl: urls[0], sources: [{ url: urls[0] }], billed };
@@ -5951,8 +6469,8 @@ var PixelLabProvider = class _PixelLabProvider {
5951
6469
  for (const [index, tile] of tileset.tiles.entries()) {
5952
6470
  const slug2 = tile.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "tile";
5953
6471
  const role = `tile-${String(index).padStart(2, "0")}-${slug2}`;
5954
- const file = import_node_path.default.join(_PixelLabProvider.cacheDir(), `${tilesetId}-${index}.png`);
5955
- (0, import_node_fs2.writeFileSync)(file, Buffer.from(tile.image.base64, "base64"));
6472
+ const file = import_node_path2.default.join(_PixelLabProvider.cacheDir(), `${tilesetId}-${index}.png`);
6473
+ (0, import_node_fs3.writeFileSync)(file, Buffer.from(tile.image.base64, "base64"));
5956
6474
  sources.push({ url: `file://${file}`, role });
5957
6475
  terrainTiles.push({ role, corners: tile.corners, pattern4x4: tile.pattern_4x4 });
5958
6476
  }
@@ -5981,8 +6499,8 @@ var PixelLabProvider = class _PixelLabProvider {
5981
6499
  const { image, usage } = await this.client.getIsometricTile(tileId);
5982
6500
  const backgroundJobId = context?.metadata?.backgroundJobId;
5983
6501
  const billed = billedFromUsage(usage) ?? await this.billedForJob(backgroundJobId);
5984
- const file = import_node_path.default.join(_PixelLabProvider.cacheDir(), `${tileId}.png`);
5985
- (0, import_node_fs2.writeFileSync)(file, Buffer.from(image.base64, "base64"));
6502
+ const file = import_node_path2.default.join(_PixelLabProvider.cacheDir(), `${tileId}.png`);
6503
+ (0, import_node_fs3.writeFileSync)(file, Buffer.from(image.base64, "base64"));
5986
6504
  return {
5987
6505
  status: "ready",
5988
6506
  objectId: tileId,
@@ -6025,8 +6543,8 @@ var PixelLabProvider = class _PixelLabProvider {
6025
6543
  return { objectId: `${jobId}#${index}`, sourceUrl: url };
6026
6544
  }
6027
6545
  if (generator === "imagePro" || generator === "uiElement") {
6028
- const file = import_node_path.default.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`);
6029
- if (!(0, import_node_fs2.existsSync)(file)) throw new Error(`${generator} job ${jobId} has no cached candidate at index ${index}`);
6546
+ const file = import_node_path2.default.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`);
6547
+ if (!(0, import_node_fs3.existsSync)(file)) throw new Error(`${generator} job ${jobId} has no cached candidate at index ${index}`);
6030
6548
  return { objectId: `${jobId}#${index}`, sourceUrl: `file://${file}` };
6031
6549
  }
6032
6550
  const promoted = await this.client.selectFrames(jobId, [index], commonTag);
@@ -6038,7 +6556,7 @@ var PixelLabProvider = class _PixelLabProvider {
6038
6556
  return { objectId, sourceUrl: firstUrl(obj?.rotation_urls) ?? obj?.preview_url ?? null };
6039
6557
  }
6040
6558
  async download(url) {
6041
- if (url.startsWith("file://")) return (0, import_node_fs2.readFileSync)(url.slice("file://".length));
6559
+ if (url.startsWith("file://")) return (0, import_node_fs3.readFileSync)(url.slice("file://".length));
6042
6560
  return this.client.download(url);
6043
6561
  }
6044
6562
  async balance() {
@@ -6160,7 +6678,7 @@ function extractBase64(value) {
6160
6678
  function readReference(spec, reference) {
6161
6679
  const images = {};
6162
6680
  for (const [direction, image] of Object.entries(reference)) {
6163
- const bytes = (0, import_node_fs2.readFileSync)(image.path);
6681
+ const bytes = (0, import_node_fs3.readFileSync)(image.path);
6164
6682
  if (sha256(bytes) !== image.sha256) {
6165
6683
  throw new Error(`${spec.styleId}/${spec.assetId}: reference (${direction}) changed after the manifest was resolved: ${image.path}`);
6166
6684
  }
@@ -6177,7 +6695,7 @@ function requireStyleObjectId(spec, context) {
6177
6695
  return context.styleObjectId;
6178
6696
  }
6179
6697
  function readPose(spec, what, image) {
6180
- const bytes = (0, import_node_fs2.readFileSync)(image.path);
6698
+ const bytes = (0, import_node_fs3.readFileSync)(image.path);
6181
6699
  if (sha256(bytes) !== image.sha256) {
6182
6700
  throw new Error(`${spec.styleId}/${spec.assetId}: ${what} changed after the manifest was resolved: ${image.path}`);
6183
6701
  }
@@ -6242,7 +6760,7 @@ function findObjectAnimation(object, name, direction, animationId) {
6242
6760
  function mapScenePayload(spec) {
6243
6761
  const scene = spec.scene;
6244
6762
  const read = (file, hash, label) => {
6245
- const bytes = (0, import_node_fs2.readFileSync)(file);
6763
+ const bytes = (0, import_node_fs3.readFileSync)(file);
6246
6764
  if (!hash || sha256(bytes) !== hash) {
6247
6765
  throw new Error(`${spec.styleId}/${spec.assetId}: ${label} changed after the manifest was resolved`);
6248
6766
  }
@@ -6303,9 +6821,9 @@ var RetroDiffusionClient = class {
6303
6821
  if (!response.ok) throw new ProviderError("retrodiffusion", `Retro Diffusion download failed (${response.status})`, { status: response.status });
6304
6822
  return Buffer.from(await response.arrayBuffer());
6305
6823
  }
6306
- async call(path38, init = {}) {
6824
+ async call(path39, init = {}) {
6307
6825
  if (!this.token) throw new Error("RD_API_KEY is not set");
6308
- const response = await this.request(`${this.baseUrl}${path38}`, {
6826
+ const response = await this.request(`${this.baseUrl}${path39}`, {
6309
6827
  ...init,
6310
6828
  headers: {
6311
6829
  "Content-Type": "application/json",
@@ -6615,9 +7133,9 @@ function retroError(value) {
6615
7133
 
6616
7134
  // src/providers/scenario.ts
6617
7135
  var import_node_crypto3 = require("crypto");
6618
- var import_node_fs3 = require("fs");
7136
+ var import_node_fs4 = require("fs");
6619
7137
  var import_node_os2 = __toESM(require("os"), 1);
6620
- var import_node_path2 = __toESM(require("path"), 1);
7138
+ var import_node_path3 = __toESM(require("path"), 1);
6621
7139
  var DEFAULT_BASE_URL2 = "https://api.cloud.scenario.com/v1";
6622
7140
  var SOURCE_PROTOCOL2 = "scenario:";
6623
7141
  var SUBMIT_RETRIES = 8;
@@ -7000,7 +7518,7 @@ var ScenarioProvider = class _ScenarioProvider {
7000
7518
  }
7001
7519
  };
7002
7520
  function uploadCachePath() {
7003
- return process.env.SCENARIO_UPLOAD_CACHE ?? import_node_path2.default.join(import_node_os2.default.tmpdir(), "pixelkiln-scenario", "uploads.json");
7521
+ return process.env.SCENARIO_UPLOAD_CACHE ?? import_node_path3.default.join(import_node_os2.default.tmpdir(), "pixelkiln-scenario", "uploads.json");
7004
7522
  }
7005
7523
  function uploadCacheKey(baseUrl, projectId, digest2) {
7006
7524
  return (0, import_node_crypto3.createHash)("sha256").update(`${baseUrl}
@@ -7009,7 +7527,7 @@ ${digest2}`).digest("hex");
7009
7527
  }
7010
7528
  function readUploadCache() {
7011
7529
  try {
7012
- const value = JSON.parse((0, import_node_fs3.readFileSync)(uploadCachePath(), "utf8"));
7530
+ const value = JSON.parse((0, import_node_fs4.readFileSync)(uploadCachePath(), "utf8"));
7013
7531
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
7014
7532
  } catch {
7015
7533
  return {};
@@ -7018,10 +7536,10 @@ function readUploadCache() {
7018
7536
  function writeUploadCache(key, assetId) {
7019
7537
  try {
7020
7538
  const file = uploadCachePath();
7021
- (0, import_node_fs3.mkdirSync)(import_node_path2.default.dirname(file), { recursive: true });
7539
+ (0, import_node_fs4.mkdirSync)(import_node_path3.default.dirname(file), { recursive: true });
7022
7540
  const tmp = `${file}.${process.pid}.tmp`;
7023
- (0, import_node_fs3.writeFileSync)(tmp, JSON.stringify({ ...readUploadCache(), [key]: assetId }));
7024
- (0, import_node_fs3.renameSync)(tmp, file);
7541
+ (0, import_node_fs4.writeFileSync)(tmp, JSON.stringify({ ...readUploadCache(), [key]: assetId }));
7542
+ (0, import_node_fs4.renameSync)(tmp, file);
7025
7543
  } catch {
7026
7544
  }
7027
7545
  }
@@ -7048,7 +7566,7 @@ function readRevisionSource(spec) {
7048
7566
  if (!revision.sourceFile || !revision.sourceSha256 || !revision.sourceFormat) {
7049
7567
  throw new Error(`${spec.styleId}/${spec.assetId}: revision source is not ready`);
7050
7568
  }
7051
- const bytes = (0, import_node_fs3.readFileSync)(revision.sourceFile);
7569
+ const bytes = (0, import_node_fs4.readFileSync)(revision.sourceFile);
7052
7570
  if (sha256(bytes) !== revision.sourceSha256) {
7053
7571
  throw new Error(`${spec.styleId}/${spec.assetId}: revision source changed after the manifest was resolved`);
7054
7572
  }
@@ -7218,8 +7736,8 @@ function redactScenarioError(value, secrets) {
7218
7736
 
7219
7737
  // src/providers/comfyui.ts
7220
7738
  var import_node_crypto4 = require("crypto");
7221
- var import_promises = require("fs/promises");
7222
- var import_node_path3 = __toESM(require("path"), 1);
7739
+ var import_promises2 = require("fs/promises");
7740
+ var import_node_path4 = __toESM(require("path"), 1);
7223
7741
  var DEFAULT_BASE_URL3 = "http://127.0.0.1:8188";
7224
7742
  var BUILTIN_BINDINGS = /* @__PURE__ */ new Set([
7225
7743
  "prompt",
@@ -7356,10 +7874,10 @@ var ComfyUIProvider = class _ComfyUIProvider {
7356
7874
  }
7357
7875
  async resolveOptions(value, context) {
7358
7876
  const options = parseOptions(value);
7359
- const workflowPath = import_node_path3.default.resolve(context.root, options.workflowFile);
7877
+ const workflowPath = import_node_path4.default.resolve(context.root, options.workflowFile);
7360
7878
  let raw;
7361
7879
  try {
7362
- raw = await (0, import_promises.readFile)(workflowPath, "utf8");
7880
+ raw = await (0, import_promises2.readFile)(workflowPath, "utf8");
7363
7881
  } catch (err) {
7364
7882
  throw new Error(
7365
7883
  `ComfyUI workflow for style "${context.styleId}" could not be read at ${workflowPath}: ${err instanceof Error ? err.message : String(err)}`
@@ -7411,10 +7929,10 @@ var ComfyUIProvider = class _ComfyUIProvider {
7411
7929
  if (typeof item !== "string") {
7412
7930
  throw new Error(`ComfyUI ${label} must be a manifest-relative PNG or JPEG path`);
7413
7931
  }
7414
- const file = import_node_path3.default.resolve(context.root, item);
7932
+ const file = import_node_path4.default.resolve(context.root, item);
7415
7933
  let bytes;
7416
7934
  try {
7417
- bytes = await (0, import_promises.readFile)(file);
7935
+ bytes = await (0, import_promises2.readFile)(file);
7418
7936
  } catch (error) {
7419
7937
  throw new Error(
7420
7938
  `ComfyUI ${label} could not be read at ${file}: ${error instanceof Error ? error.message : String(error)}`
@@ -7571,7 +8089,7 @@ var ComfyUIProvider = class _ComfyUIProvider {
7571
8089
  setBinding(workflow2, binding, input);
7572
8090
  return;
7573
8091
  }
7574
- const bytes = await (0, import_promises.readFile)(input.path);
8092
+ const bytes = await (0, import_promises2.readFile)(input.path);
7575
8093
  if (sha256(bytes) !== input.sha256) {
7576
8094
  throw new Error(`ComfyUI provider input "${name}" changed before upload`);
7577
8095
  }
@@ -7638,7 +8156,7 @@ var ComfyUIProvider = class _ComfyUIProvider {
7638
8156
  if (!spec.revision.sourceSha256 || !spec.revision.sourceFormat) {
7639
8157
  throw new Error("ComfyUI revision source is not ready");
7640
8158
  }
7641
- const sourceBytes = await (0, import_promises.readFile)(spec.revision.sourceFile);
8159
+ const sourceBytes = await (0, import_promises2.readFile)(spec.revision.sourceFile);
7642
8160
  if (sha256(sourceBytes) !== spec.revision.sourceSha256) {
7643
8161
  throw new Error("ComfyUI revision source changed before upload");
7644
8162
  }
@@ -7652,7 +8170,7 @@ var ComfyUIProvider = class _ComfyUIProvider {
7652
8170
  if (!spec.revision.maskSha256 || !spec.revision.maskFormat) {
7653
8171
  throw new Error("ComfyUI revision mask is not ready");
7654
8172
  }
7655
- const maskBytes = await (0, import_promises.readFile)(spec.revision.maskFile);
8173
+ const maskBytes = await (0, import_promises2.readFile)(spec.revision.maskFile);
7656
8174
  if (sha256(maskBytes) !== spec.revision.maskSha256) {
7657
8175
  throw new Error("ComfyUI revision mask changed before upload");
7658
8176
  }
@@ -8372,9 +8890,9 @@ registerProvider({
8372
8890
  });
8373
8891
 
8374
8892
  // src/manifest.ts
8375
- var import_promises3 = require("fs/promises");
8376
- var import_node_fs6 = require("fs");
8377
- var import_node_path6 = __toESM(require("path"), 1);
8893
+ var import_promises4 = require("fs/promises");
8894
+ var import_node_fs7 = require("fs");
8895
+ var import_node_path7 = __toESM(require("path"), 1);
8378
8896
 
8379
8897
  // src/loop-directions.ts
8380
8898
  var SINGLE_OUTPUT_FIELDS = ["file", "cell", "remoteId", "source", "sourceByStyle", "outputRole"];
@@ -8462,17 +8980,17 @@ function isObject2(value) {
8462
8980
  }
8463
8981
 
8464
8982
  // src/outputs.ts
8465
- var import_node_fs5 = require("fs");
8466
- var import_node_path5 = __toESM(require("path"), 1);
8983
+ var import_node_fs6 = require("fs");
8984
+ var import_node_path6 = __toESM(require("path"), 1);
8467
8985
 
8468
8986
  // src/lock.ts
8469
- var import_promises2 = require("fs/promises");
8470
- var import_node_fs4 = require("fs");
8471
- var import_node_path4 = __toESM(require("path"), 1);
8987
+ var import_promises3 = require("fs/promises");
8988
+ var import_node_fs5 = require("fs");
8989
+ var import_node_path5 = __toESM(require("path"), 1);
8472
8990
  async function loadLock(lockPath) {
8473
- if (!(0, import_node_fs4.existsSync)(lockPath)) return { version: 2, entries: {} };
8991
+ if (!(0, import_node_fs5.existsSync)(lockPath)) return { version: 2, entries: {} };
8474
8992
  try {
8475
- return parseLock(JSON.parse(await (0, import_promises2.readFile)(lockPath, "utf8")));
8993
+ return parseLock(JSON.parse(await (0, import_promises3.readFile)(lockPath, "utf8")));
8476
8994
  } catch (err) {
8477
8995
  throw new ProjectError(
8478
8996
  `Lockfile at ${lockPath} is malformed:
@@ -8485,7 +9003,7 @@ var saveQueues = /* @__PURE__ */ new Map();
8485
9003
  var dirtyPatches = /* @__PURE__ */ new WeakMap();
8486
9004
  var dirtyDeletes = /* @__PURE__ */ new WeakMap();
8487
9005
  function saveLock(lockPath, lock) {
8488
- const queueKey = import_node_path4.default.resolve(lockPath);
9006
+ const queueKey = import_node_path5.default.resolve(lockPath);
8489
9007
  const previous = saveQueues.get(queueKey) ?? Promise.resolve();
8490
9008
  const next = previous.catch(() => {
8491
9009
  }).then(() => writeLockNow(lockPath, lock));
@@ -8497,7 +9015,7 @@ function saveLock(lockPath, lock) {
8497
9015
  return next;
8498
9016
  }
8499
9017
  async function writeLockNow(lockPath, lock) {
8500
- await (0, import_promises2.mkdir)(import_node_path4.default.dirname(import_node_path4.default.resolve(lockPath)), { recursive: true });
9018
+ await (0, import_promises3.mkdir)(import_node_path5.default.dirname(import_node_path5.default.resolve(lockPath)), { recursive: true });
8501
9019
  const release = await acquireFileLock(lockPath);
8502
9020
  try {
8503
9021
  await writeLockWhileHeld(lockPath, lock);
@@ -8507,9 +9025,9 @@ async function writeLockNow(lockPath, lock) {
8507
9025
  }
8508
9026
  async function writeLockWhileHeld(file, lock) {
8509
9027
  let disk = { version: 2, entries: {} };
8510
- if ((0, import_node_fs4.existsSync)(file)) {
9028
+ if ((0, import_node_fs5.existsSync)(file)) {
8511
9029
  try {
8512
- disk = parseLock(JSON.parse(await (0, import_promises2.readFile)(file, "utf8")));
9030
+ disk = parseLock(JSON.parse(await (0, import_promises3.readFile)(file, "utf8")));
8513
9031
  } catch (err) {
8514
9032
  throw new ProjectError(
8515
9033
  `Refusing to overwrite malformed lockfile at ${file}: ${err instanceof Error ? err.message : String(err)}`,
@@ -8538,10 +9056,10 @@ async function writeLockWhileHeld(file, lock) {
8538
9056
  }
8539
9057
  const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
8540
9058
  try {
8541
- await (0, import_promises2.writeFile)(tmp, JSON.stringify({ version: 2, entries: sorted }, null, 2) + "\n");
8542
- await (0, import_promises2.rename)(tmp, file);
9059
+ await (0, import_promises3.writeFile)(tmp, JSON.stringify({ version: 2, entries: sorted }, null, 2) + "\n");
9060
+ await (0, import_promises3.rename)(tmp, file);
8543
9061
  } finally {
8544
- await (0, import_promises2.rm)(tmp, { force: true });
9062
+ await (0, import_promises3.rm)(tmp, { force: true });
8545
9063
  }
8546
9064
  const currentDirty = dirtyPatches.get(lock);
8547
9065
  if (currentDirty) {
@@ -8604,18 +9122,18 @@ async function acquireFileLock(file) {
8604
9122
  const deadline = Date.now() + FILE_LOCK_TIMEOUT_MS;
8605
9123
  for (; ; ) {
8606
9124
  try {
8607
- await (0, import_promises2.mkdir)(lockDir);
9125
+ await (0, import_promises3.mkdir)(lockDir);
8608
9126
  return async () => {
8609
- await (0, import_promises2.rmdir)(lockDir).catch(() => {
9127
+ await (0, import_promises3.rmdir)(lockDir).catch(() => {
8610
9128
  });
8611
9129
  };
8612
9130
  } catch (err) {
8613
9131
  const code = err.code;
8614
9132
  if (code !== "EEXIST") throw err;
8615
9133
  try {
8616
- const age = Date.now() - (await (0, import_promises2.stat)(lockDir)).mtimeMs;
9134
+ const age = Date.now() - (await (0, import_promises3.stat)(lockDir)).mtimeMs;
8617
9135
  if (age > STALE_FILE_LOCK_MS) {
8618
- await (0, import_promises2.rmdir)(lockDir);
9136
+ await (0, import_promises3.rmdir)(lockDir);
8619
9137
  continue;
8620
9138
  }
8621
9139
  } catch (lockErr) {
@@ -8642,22 +9160,22 @@ function totalSpend(lock, unit = "generations") {
8642
9160
 
8643
9161
  // src/outputs.ts
8644
9162
  function portableOutputPath(file, manifestDir) {
8645
- const root = import_node_path5.default.resolve(manifestDir);
8646
- const absolute = import_node_path5.default.isAbsolute(file) ? import_node_path5.default.normalize(file) : import_node_path5.default.resolve(root, file);
8647
- const relative = import_node_path5.default.relative(root, absolute);
8648
- const value = import_node_path5.default.isAbsolute(relative) ? absolute : relative;
8649
- return value.split(import_node_path5.default.sep).join("/");
9163
+ const root = import_node_path6.default.resolve(manifestDir);
9164
+ const absolute = import_node_path6.default.isAbsolute(file) ? import_node_path6.default.normalize(file) : import_node_path6.default.resolve(root, file);
9165
+ const relative = import_node_path6.default.relative(root, absolute);
9166
+ const value = import_node_path6.default.isAbsolute(relative) ? absolute : relative;
9167
+ return value.split(import_node_path6.default.sep).join("/");
8650
9168
  }
8651
9169
  function resolveOutputPath(recordedPath, manifestDir) {
8652
- if (import_node_path5.default.isAbsolute(recordedPath)) return import_node_path5.default.normalize(recordedPath);
8653
- if (import_node_path5.default.win32.isAbsolute(recordedPath)) return recordedPath;
8654
- return import_node_path5.default.resolve(manifestDir, recordedPath.split(/[\\/]/).join(import_node_path5.default.sep));
9170
+ if (import_node_path6.default.isAbsolute(recordedPath)) return import_node_path6.default.normalize(recordedPath);
9171
+ if (import_node_path6.default.win32.isAbsolute(recordedPath)) return recordedPath;
9172
+ return import_node_path6.default.resolve(manifestDir, recordedPath.split(/[\\/]/).join(import_node_path6.default.sep));
8655
9173
  }
8656
9174
  function expectedOutputPath(spec, role, index, total, mediaType) {
8657
9175
  return memberPath(spec.outFile, role, index, total, mediaType);
8658
9176
  }
8659
9177
  function memberPath(file, role, index, total, mediaType) {
8660
- const originalExt = import_node_path5.default.extname(file);
9178
+ const originalExt = import_node_path6.default.extname(file);
8661
9179
  const ext = mediaType ? mediaExtension(mediaType) : originalExt || ".png";
8662
9180
  const stem = originalExt ? file.slice(0, -originalExt.length) : file;
8663
9181
  if (total === 1) return `${stem}${ext}`;
@@ -8671,10 +9189,10 @@ function isFrameSetEntry(entry) {
8671
9189
  return entry.generator === "frames" && isMemberSetEntry(entry);
8672
9190
  }
8673
9191
  function sourceIsStem(source, entry, manifestDir) {
8674
- return isMemberSetEntry(entry) && !(0, import_node_fs5.existsSync)(import_node_path5.default.resolve(manifestDir, source));
9192
+ return isMemberSetEntry(entry) && !(0, import_node_fs6.existsSync)(import_node_path6.default.resolve(manifestDir, source));
8675
9193
  }
8676
9194
  function sourceOutputPath(source, entry, index, manifestDir) {
8677
- const absolute = import_node_path5.default.resolve(manifestDir, source);
9195
+ const absolute = import_node_path6.default.resolve(manifestDir, source);
8678
9196
  if (!sourceIsStem(source, entry, manifestDir)) return absolute;
8679
9197
  const output = entry.outputs[index];
8680
9198
  return memberPath(absolute, output.role, index, entry.outputs.length, output.mediaType);
@@ -8761,7 +9279,7 @@ function resolveSpecOutputs(spec, lock, manifestDir) {
8761
9279
  id: spec.assetId,
8762
9280
  index: 0,
8763
9281
  path: spec.outFile,
8764
- absolutePath: import_node_path5.default.resolve(spec.outFile),
9282
+ absolutePath: import_node_path6.default.resolve(spec.outFile),
8765
9283
  sha256: ""
8766
9284
  }];
8767
9285
  }
@@ -8774,12 +9292,17 @@ function frameSetFps(entry) {
8774
9292
  }
8775
9293
  return null;
8776
9294
  }
9295
+ function frameSetLoop(entry) {
9296
+ const frameSet = entry.providerMetadata?.[entry.provider]?.frameSet;
9297
+ const loop = frameSet && typeof frameSet === "object" ? frameSet.loop : void 0;
9298
+ return typeof loop === "boolean" ? loop : null;
9299
+ }
8777
9300
 
8778
9301
  // src/manifest.ts
8779
9302
  async function loadManifest(manifestPath) {
8780
- const abs = import_node_path6.default.resolve(manifestPath);
8781
- if (!(0, import_node_fs6.existsSync)(abs)) throw new ProjectError(`No manifest at ${abs}`);
8782
- const expansion = expandLoopDirections(JSON.parse(await (0, import_promises3.readFile)(abs, "utf8")));
9303
+ const abs = import_node_path7.default.resolve(manifestPath);
9304
+ if (!(0, import_node_fs7.existsSync)(abs)) throw new ProjectError(`No manifest at ${abs}`);
9305
+ const expansion = expandLoopDirections(JSON.parse(await (0, import_promises4.readFile)(abs, "utf8")));
8783
9306
  if (expansion.issues.length) {
8784
9307
  throw new ProjectError(`Manifest at ${abs} is invalid:
8785
9308
  ${expansion.issues.map((i) => ` ${i}`).join("\n")}`);
@@ -8834,7 +9357,7 @@ ${issues}`);
8834
9357
  if (fromStyle) {
8835
9358
  if (!styleIds.has(fromStyle)) {
8836
9359
  unknownReferences.push(`assets.${assetId}.revision.fromStyle: unknown style "${fromStyle}"`);
8837
- } else if (parent && parent.styles.length && !parent.styles.includes(fromStyle)) {
9360
+ } else if (parent && !assetInStyle(parent, fromStyle, parsed.data.styles[fromStyle])) {
8838
9361
  unknownReferences.push(
8839
9362
  `assets.${assetId}.revision.fromStyle: "${asset.revision.from}" is not in style "${fromStyle}"`
8840
9363
  );
@@ -8881,7 +9404,11 @@ ${issues}`);
8881
9404
  ${unknownReferences.map((i) => ` ${i}`).join("\n")}`);
8882
9405
  }
8883
9406
  const loopFamilies = Object.keys(expansion.families).length ? { loopFamilies: expansion.families } : {};
8884
- return { manifest: parsed.data, root: import_node_path6.default.dirname(abs), path: abs, ...loopFamilies };
9407
+ return { manifest: parsed.data, root: import_node_path7.default.dirname(abs), path: abs, ...loopFamilies };
9408
+ }
9409
+ function assetInStyle(asset, styleId, style) {
9410
+ if (asset.styles.length) return asset.styles.includes(styleId);
9411
+ return style?.generator === "vfx" === Boolean(asset.vfx);
8885
9412
  }
8886
9413
  function parentAssetId(asset) {
8887
9414
  return asset?.revision?.from ?? asset?.state?.of ?? asset?.animation?.of ?? asset?.portrait?.of ?? asset?.outfit?.of;
@@ -8985,8 +9512,8 @@ async function resolveSpecs(loaded, filter) {
8985
9512
  const needed = /* @__PURE__ */ new Set();
8986
9513
  for (const assetId of resolutionAssetIds) {
8987
9514
  const asset = manifest.assets[assetId];
8988
- if (asset?.styles.length && !asset.styles.includes(styleId)) continue;
8989
- const fromStyle = asset?.revision?.fromStyle;
9515
+ if (!asset || !assetInStyle(asset, styleId, manifest.styles[styleId])) continue;
9516
+ const fromStyle = asset.revision?.fromStyle;
8990
9517
  if (fromStyle && fromStyle !== styleId) needed.add(fromStyle);
8991
9518
  }
8992
9519
  styleDependencies.set(styleId, needed);
@@ -9015,11 +9542,11 @@ async function resolveSpecs(loaded, filter) {
9015
9542
  const styleFinalizers = /* @__PURE__ */ new Map();
9016
9543
  const styleImageCache = /* @__PURE__ */ new Map();
9017
9544
  async function loadStyleImage(rel, what = "Style image") {
9018
- const abs = import_node_path6.default.resolve(root, rel);
9545
+ const abs = import_node_path7.default.resolve(root, rel);
9019
9546
  let hit = styleImageCache.get(abs);
9020
9547
  if (!hit) {
9021
- if (!(0, import_node_fs6.existsSync)(abs)) throw new Error(`${what} not found: ${abs}`);
9022
- const buf = await (0, import_promises3.readFile)(abs);
9548
+ if (!(0, import_node_fs7.existsSync)(abs)) throw new Error(`${what} not found: ${abs}`);
9549
+ const buf = await (0, import_promises4.readFile)(abs);
9023
9550
  const metadata = imageMetadata(buf);
9024
9551
  if (!metadata) throw new Error(`${what} is not a readable PNG or JPEG: ${abs}`);
9025
9552
  if (metadata.width < 1 || metadata.height < 1) {
@@ -9045,14 +9572,14 @@ async function resolveSpecs(loaded, filter) {
9045
9572
  styleImageDimensions.push(loadedImage);
9046
9573
  }
9047
9574
  const resolvedImages = style.styleImages.map((image) => {
9048
- const hit = styleImageCache.get(import_node_path6.default.resolve(root, image.path));
9575
+ const hit = styleImageCache.get(import_node_path7.default.resolve(root, image.path));
9049
9576
  return { base64: hit.base64, width: hit.width, height: hit.height, format: hit.format };
9050
9577
  });
9051
9578
  const styleSpecs = /* @__PURE__ */ new Map();
9052
9579
  const providerInputIdentities = /* @__PURE__ */ new Map();
9053
9580
  for (const [assetId, asset] of Object.entries(manifest.assets)) {
9054
9581
  if (!resolutionAssetIds.has(assetId)) continue;
9055
- if (asset.styles.length && !asset.styles.includes(styleId)) continue;
9582
+ if (!assetInStyle(asset, styleId, style)) continue;
9056
9583
  const generator = style.generator;
9057
9584
  if (!activeProvider.supports(generator)) {
9058
9585
  throw new Error(`Provider "${activeProvider.id}" does not support generator "${generator}"`);
@@ -9072,6 +9599,15 @@ async function resolveSpecs(loaded, filter) {
9072
9599
  const providerInputs = inputResolution.inputs;
9073
9600
  const providerInputIdentity = inputResolution.identity ?? providerInputs;
9074
9601
  providerInputIdentities.set(assetId, providerInputIdentity);
9602
+ if (asset.vfx && generator !== "vfx") {
9603
+ throw new Error(`assets.${assetId}: vfx needs a vfx style; "${styleId}" generates ${generator}`);
9604
+ }
9605
+ if (generator === "vfx" && !asset.vfx && !asset.mirror && !asset.revision) {
9606
+ throw new Error(
9607
+ `assets.${assetId}: style "${styleId}" renders effects; give the asset a vfx block, or make it a mirror or revision of one`
9608
+ );
9609
+ }
9610
+ const vfx = generator === "vfx" && asset.vfx ? await resolveVfxShape(root, assetId, asset.vfx, `pixelkiln:${styleId}/${assetId}`) : void 0;
9075
9611
  let width;
9076
9612
  let height;
9077
9613
  let size;
@@ -9099,6 +9635,11 @@ async function resolveSpecs(loaded, filter) {
9099
9635
  size = asset.size ?? style.size ?? 64;
9100
9636
  width = size;
9101
9637
  height = size;
9638
+ } else if (generator === "vfx") {
9639
+ const canvas = vfx?.recipe?.recipe.canvas;
9640
+ width = typeof canvas?.w === "number" ? canvas.w : asset.width ?? style.size ?? 64;
9641
+ height = typeof canvas?.h === "number" ? canvas.h : asset.height ?? style.size ?? 64;
9642
+ size = Math.max(width, height);
9102
9643
  } else if (generator === "uiAsset" || generator === "uiElement") {
9103
9644
  width = asset.width ?? style.size ?? 256;
9104
9645
  height = asset.height ?? style.size ?? 256;
@@ -9141,14 +9682,14 @@ async function resolveSpecs(loaded, filter) {
9141
9682
  terrainUpperDescription = wrap(parsed.upper);
9142
9683
  terrainTransitionDescription = parsed.transition ? wrap(parsed.transition) : void 0;
9143
9684
  prompt = subject.trim();
9144
- } else if ((generator === "character" || generator === "objectPro") && characterKind !== "base") {
9685
+ } else if ((generator === "character" || generator === "objectPro") && characterKind !== "base" || vfx) {
9145
9686
  prompt = subject.trim();
9146
9687
  } else {
9147
9688
  prompt = [style.promptPrefix, subject, style.promptSuffix].map((p) => p.trim()).filter(Boolean).join(", ");
9148
9689
  }
9149
- const relFile = asset.file ?? import_node_path6.default.join(asset.category ?? "", `${assetId}.png`);
9150
- const outFile = import_node_path6.default.resolve(root, style.outDir, relFile);
9151
- const qualityOutFile = style.quality ? import_node_path6.default.resolve(root, style.quality.outDir, pngPath(relFile)) : void 0;
9690
+ const relFile = asset.file ?? import_node_path7.default.join(asset.category ?? "", `${assetId}.png`);
9691
+ const outFile = import_node_path7.default.resolve(root, style.outDir, relFile);
9692
+ const qualityOutFile = style.quality ? import_node_path7.default.resolve(root, style.quality.outDir, pngPath(relFile)) : void 0;
9152
9693
  const tileSize = generator === "tiles" ? size : style.tileSize ?? 32;
9153
9694
  const tileVariations = tileFeatureOutputCount(generator === "tiles" ? style.tileFeature : void 0) ?? tileVariationCount(countNumberedDescriptions(prompt));
9154
9695
  const terrainTiles = generator === "terrain" ? terrainTileCount(style.terrainTransitionSize) : 0;
@@ -9219,6 +9760,7 @@ async function resolveSpecs(loaded, filter) {
9219
9760
  // A portrait needs a character style (refused above), so an
9220
9761
  // objectPro asset is only ever a base, state, or animation.
9221
9762
  ...generator === "objectPro" && !asset.revision && characterKind !== "portrait" ? { objectPro: await resolveObjectProShape(assetId, asset, style, characterKind, { root, load: loadStyleImage }) } : {},
9763
+ ...vfx ? { vfx } : {},
9222
9764
  cost: generator === "tiles" ? tilesCost(tileSize, tileVariations) : generator === "terrain" ? tilesCost(size, terrainTiles) : generator === "isometricTile" ? isometricTileCost() : generationCost(width, height, generator),
9223
9765
  costUnit: "generations",
9224
9766
  candidates: generator === "tiles" ? tileVariations : generator === "terrain" ? terrainTiles : generator === "1dir" || generator === "imagePro" ? candidateCount(size) : 1
@@ -9243,7 +9785,7 @@ async function resolveSpecs(loaded, filter) {
9243
9785
  minGridConfidence: style.quality.minGridConfidence,
9244
9786
  ...style.quality.minTransparency == null ? {} : { minTransparency: style.quality.minTransparency },
9245
9787
  ...style.quality.fixerRevision ? { fixerRevision: style.quality.fixerRevision } : {},
9246
- ...style.quality.fixerPython ? { fixerPython: import_node_path6.default.resolve(root, style.quality.fixerPython) } : {},
9788
+ ...style.quality.fixerPython ? { fixerPython: import_node_path7.default.resolve(root, style.quality.fixerPython) } : {},
9247
9789
  ...style.quality.fps == null ? {} : { fps: style.quality.fps }
9248
9790
  }
9249
9791
  } : {},
@@ -9326,7 +9868,7 @@ async function resolveSpecs(loaded, filter) {
9326
9868
  const file = memberPath(anchorSpec.outFile, "south", 0, anchorSpec.character.directions, MediaType.PNG);
9327
9869
  resolved.character = {
9328
9870
  ...resolved.character,
9329
- styleAnchor: { assetId: anchorId, spec: anchorSpec, file, sha256: (0, import_node_fs6.existsSync)(file) ? await sha256File(file) : null }
9871
+ styleAnchor: { assetId: anchorId, spec: anchorSpec, file, sha256: (0, import_node_fs7.existsSync)(file) ? await sha256File(file) : null }
9330
9872
  };
9331
9873
  }
9332
9874
  const characterParent = asset.state?.of ?? asset.animation?.of ?? asset.portrait?.of;
@@ -9339,7 +9881,7 @@ async function resolveSpecs(loaded, filter) {
9339
9881
  parentAssetId: characterParent,
9340
9882
  parentSpec,
9341
9883
  parentFile,
9342
- parentSha256: (0, import_node_fs6.existsSync)(parentFile) ? await sha256File(parentFile) : null,
9884
+ parentSha256: (0, import_node_fs7.existsSync)(parentFile) ? await sha256File(parentFile) : null,
9343
9885
  // A state and its animations keep the engine and rotations of the base.
9344
9886
  mode: resolved.character.kind === "base" ? resolved.character.mode : parentSpec.character?.mode ?? resolved.character.mode,
9345
9887
  directions: resolved.character.kind === "base" ? resolved.character.directions : parentSpec.character?.directions ?? resolved.character.directions
@@ -9357,7 +9899,7 @@ async function resolveSpecs(loaded, filter) {
9357
9899
  parentAssetId: characterParent,
9358
9900
  parentSpec,
9359
9901
  parentFile,
9360
- parentSha256: (0, import_node_fs6.existsSync)(parentFile) ? await sha256File(parentFile) : null,
9902
+ parentSha256: (0, import_node_fs7.existsSync)(parentFile) ? await sha256File(parentFile) : null,
9361
9903
  // A state and its animations keep the base's own rotation count.
9362
9904
  directions: resolved.objectPro.kind === "base" ? resolved.objectPro.directions : parentSpec.objectPro.directions
9363
9905
  };
@@ -9379,7 +9921,7 @@ async function resolveSpecs(loaded, filter) {
9379
9921
  animation: sourceSpec.character.animation,
9380
9922
  outfit: {
9381
9923
  reference: {
9382
- path: import_node_path6.default.resolve(root, asset.outfit.reference),
9924
+ path: import_node_path7.default.resolve(root, asset.outfit.reference),
9383
9925
  sha256: referenceImage.hash,
9384
9926
  width: referenceImage.width,
9385
9927
  height: referenceImage.height,
@@ -9402,23 +9944,23 @@ async function resolveSpecs(loaded, filter) {
9402
9944
  }
9403
9945
  const sourceStyleId = asset.revision.fromStyle && asset.revision.fromStyle !== styleId ? asset.revision.fromStyle : void 0;
9404
9946
  const sourceSpec = sourceStyleId ? await styleFinalizers.get(sourceStyleId)(asset.revision.from) : await finalize(asset.revision.from);
9405
- const sourceStem = sourceSpec.quality?.outFile ?? (sourceSpec.source ? import_node_path6.default.resolve(root, sourceSpec.source) : sourceSpec.outFile);
9947
+ const sourceStem = sourceSpec.quality?.outFile ?? (sourceSpec.source ? import_node_path7.default.resolve(root, sourceSpec.source) : sourceSpec.outFile);
9406
9948
  const sourceMembers = await revisionSourceMembers(sourceStem, `revision source ${asset.revision.from}`);
9407
9949
  const sourceFile = sourceMembers?.[0]?.file ?? sourceStem;
9408
9950
  const sourceImage = sourceMembers ? { ...sourceMembers[0], hash: sha256(JSON.stringify(sourceMembers.map((member) => member.sha256))) } : await optionalRevisionImage(sourceFile, "revision source");
9409
9951
  const sourceFps = sourceSpec.character?.animation?.fps ?? sourceSpec.objectPro?.animation?.fps ?? sourceSpec.revision?.fps ?? sourceSpec.revision?.sourceFps;
9410
- const maskFile = asset.revision.mask ? import_node_path6.default.resolve(root, asset.revision.mask) : void 0;
9952
+ const maskFile = asset.revision.mask ? import_node_path7.default.resolve(root, asset.revision.mask) : void 0;
9411
9953
  const maskImage = maskFile ? await optionalRevisionImage(maskFile, "revision mask", "png") : null;
9412
9954
  if (sourceImage && maskImage && (sourceImage.width !== maskImage.width || sourceImage.height !== maskImage.height)) {
9413
9955
  throw new Error(
9414
9956
  `Revision mask for ${styleId}/${assetId} is ${maskImage.width}x${maskImage.height}; source ${asset.revision.from} is ${sourceImage.width}x${sourceImage.height}`
9415
9957
  );
9416
9958
  }
9417
- const paletteImageFile = asset.revision.paletteImage ? import_node_path6.default.resolve(root, asset.revision.paletteImage) : void 0;
9959
+ const paletteImageFile = asset.revision.paletteImage ? import_node_path7.default.resolve(root, asset.revision.paletteImage) : void 0;
9418
9960
  const paletteImage = paletteImageFile ? await optionalRevisionImage(paletteImageFile, "revision palette image", "png") : null;
9419
- const lastFrameFile = asset.revision.lastFrame ? import_node_path6.default.resolve(root, asset.revision.lastFrame) : void 0;
9961
+ const lastFrameFile = asset.revision.lastFrame ? import_node_path7.default.resolve(root, asset.revision.lastFrame) : void 0;
9420
9962
  const lastFrame = lastFrameFile ? await optionalRevisionImage(lastFrameFile, "revision last frame") : null;
9421
- const keypointsFile = asset.revision.keypointsFile ? import_node_path6.default.resolve(root, asset.revision.keypointsFile) : void 0;
9963
+ const keypointsFile = asset.revision.keypointsFile ? import_node_path7.default.resolve(root, asset.revision.keypointsFile) : void 0;
9422
9964
  const keypoints = keypointsFile ? await optionalSkeletonSet(keypointsFile) : null;
9423
9965
  resolved.revision = {
9424
9966
  mode: asset.revision.mode,
@@ -9588,7 +10130,7 @@ async function resolveSpecs(loaded, filter) {
9588
10130
  return specs;
9589
10131
  }
9590
10132
  function pngPath(file) {
9591
- const extension = import_node_path6.default.extname(file);
10133
+ const extension = import_node_path7.default.extname(file);
9592
10134
  return extension ? `${file.slice(0, -extension.length)}.png` : `${file}.png`;
9593
10135
  }
9594
10136
  async function resolveMapScene(root, styleId, assetId, generator, scene, provider) {
@@ -9598,12 +10140,12 @@ async function resolveMapScene(root, styleId, assetId, generator, scene, provide
9598
10140
  if (!provider.supportsMapScene) {
9599
10141
  throw new Error(`assets.${assetId}.scene: provider "${provider.id}" cannot draw into a scene; PixelLab's /map-objects can`);
9600
10142
  }
9601
- const file = import_node_path6.default.resolve(root, scene.image);
10143
+ const file = import_node_path7.default.resolve(root, scene.image);
9602
10144
  const image = await optionalRevisionImage(file, "scene image", "png");
9603
10145
  const base = { file, sha256: image?.hash ?? null, width: image?.width ?? null, height: image?.height ?? null };
9604
10146
  const placement = scene.placement;
9605
10147
  if ("mask" in placement) {
9606
- const maskFile = import_node_path6.default.resolve(root, placement.mask);
10148
+ const maskFile = import_node_path7.default.resolve(root, placement.mask);
9607
10149
  const mask = await optionalRevisionImage(maskFile, "scene mask", "png");
9608
10150
  if (image && mask && (image.width !== mask.width || image.height !== mask.height)) {
9609
10151
  throw new Error(
@@ -9617,9 +10159,38 @@ async function resolveMapScene(root, styleId, assetId, generator, scene, provide
9617
10159
  }
9618
10160
  return "oval" in placement ? { ...base, placement: { type: "oval", fraction: placement.oval } } : { ...base, placement: { type: "rectangle", fraction: placement.rectangle } };
9619
10161
  }
10162
+ async function resolveVfxShape(root, assetId, vfx, defaultName) {
10163
+ let recipe;
10164
+ let recordedSprite;
10165
+ if (vfx.recipe) {
10166
+ const file = import_node_path7.default.resolve(root, vfx.recipe);
10167
+ if (!(0, import_node_fs7.existsSync)(file)) throw new Error(`assets.${assetId}.vfx.recipe: VFX recipe not found: ${file}`);
10168
+ const read = await readVfxRecipe(file).catch((error) => {
10169
+ throw new Error(`assets.${assetId}.vfx.recipe: ${error instanceof Error ? error.message : String(error)}`);
10170
+ });
10171
+ recipe = { file, sha256: vfxRecipeSha256(read.recipe), recipe: read.recipe };
10172
+ recordedSprite = read.sprite;
10173
+ }
10174
+ const spriteRef = vfx.sprite ? isVfxReference(vfx.sprite) ? vfx.sprite : import_node_path7.default.resolve(root, vfx.sprite) : recordedSprite;
10175
+ let sprite;
10176
+ if (spriteRef && isVfxReference(spriteRef)) {
10177
+ sprite = { reference: spriteRef };
10178
+ } else if (spriteRef) {
10179
+ const image = await optionalRevisionImage(spriteRef, `assets.${assetId}.vfx.sprite`);
10180
+ sprite = { file: spriteRef, sha256: image?.hash ?? null, width: image?.width ?? null, height: image?.height ?? null, format: image?.format ?? null };
10181
+ }
10182
+ return {
10183
+ ...vfx.preset ? { preset: vfx.preset } : {},
10184
+ ...recipe ? { recipe } : {},
10185
+ ...vfx.parts?.length ? { parts: vfx.parts } : {},
10186
+ ...sprite ? { sprite } : {},
10187
+ ...vfx.fps !== void 0 ? { fps: vfx.fps } : {},
10188
+ name: vfx.name ?? defaultName
10189
+ };
10190
+ }
9620
10191
  async function optionalRevisionImage(file, label, requiredFormat) {
9621
- if (!(0, import_node_fs6.existsSync)(file)) return null;
9622
- const bytes = await (0, import_promises3.readFile)(file);
10192
+ if (!(0, import_node_fs7.existsSync)(file)) return null;
10193
+ const bytes = await (0, import_promises4.readFile)(file);
9623
10194
  const metadata = imageMetadata(bytes);
9624
10195
  if (!metadata) throw new Error(`${label} is not a readable PNG or JPEG: ${file}`);
9625
10196
  if (requiredFormat && metadata.format !== requiredFormat) {
@@ -9628,8 +10199,8 @@ async function optionalRevisionImage(file, label, requiredFormat) {
9628
10199
  return { hash: sha256(bytes), ...metadata };
9629
10200
  }
9630
10201
  async function optionalSkeletonSet(file) {
9631
- if (!(0, import_node_fs6.existsSync)(file)) return null;
9632
- const bytes = await (0, import_promises3.readFile)(file);
10202
+ if (!(0, import_node_fs7.existsSync)(file)) return null;
10203
+ const bytes = await (0, import_promises4.readFile)(file);
9633
10204
  let json;
9634
10205
  try {
9635
10206
  json = JSON.parse(bytes.toString("utf8"));
@@ -9639,17 +10210,17 @@ async function optionalSkeletonSet(file) {
9639
10210
  return { hash: sha256(bytes), skeleton: parseSkeletonSet(json, file) };
9640
10211
  }
9641
10212
  async function revisionSourceMembers(stem, label) {
9642
- if ((0, import_node_fs6.existsSync)(stem)) return null;
10213
+ if ((0, import_node_fs7.existsSync)(stem)) return null;
9643
10214
  const at = (role) => memberPath(stem, role, 0, 2, MediaType.PNG);
9644
10215
  let roles = [];
9645
10216
  for (const set of [CHARACTER_DIRECTIONS_8, CHARACTER_DIRECTIONS_4]) {
9646
- if (set.every((direction) => (0, import_node_fs6.existsSync)(at(direction)))) {
10217
+ if (set.every((direction) => (0, import_node_fs7.existsSync)(at(direction)))) {
9647
10218
  roles = [...set];
9648
10219
  break;
9649
10220
  }
9650
10221
  }
9651
10222
  if (!roles.length) {
9652
- for (let index = 0; (0, import_node_fs6.existsSync)(at(`frame-${String(index).padStart(2, "0")}`)); index++) {
10223
+ for (let index = 0; (0, import_node_fs7.existsSync)(at(`frame-${String(index).padStart(2, "0")}`)); index++) {
9653
10224
  roles.push(`frame-${String(index).padStart(2, "0")}`);
9654
10225
  }
9655
10226
  }
@@ -9675,8 +10246,8 @@ async function resolveStyleImages(loaded, styleId) {
9675
10246
  if (!style) throw new Error(`Unknown style "${styleId}"`);
9676
10247
  const out = [];
9677
10248
  for (const img of style.styleImages) {
9678
- const file = import_node_path6.default.resolve(loaded.root, img.path);
9679
- const buf = await (0, import_promises3.readFile)(file);
10249
+ const file = import_node_path7.default.resolve(loaded.root, img.path);
10250
+ const buf = await (0, import_promises4.readFile)(file);
9680
10251
  const metadata = imageMetadata(buf);
9681
10252
  if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${file}`);
9682
10253
  if (metadata.width < 1 || metadata.height < 1) {
@@ -9706,7 +10277,7 @@ async function resolveCharacterShape(asset, style, kind, files) {
9706
10277
  };
9707
10278
  if (asset.concept) {
9708
10279
  const image = await files.load(asset.concept, "Concept image");
9709
- shape.concept = { path: import_node_path6.default.resolve(files.root, asset.concept), sha256: image.hash, width: image.width, height: image.height, format: image.format };
10280
+ shape.concept = { path: import_node_path7.default.resolve(files.root, asset.concept), sha256: image.hash, width: image.width, height: image.height, format: image.format };
9710
10281
  }
9711
10282
  if (asset.reference) {
9712
10283
  const byDirection = typeof asset.reference === "string" ? { south: asset.reference } : asset.reference;
@@ -9714,7 +10285,7 @@ async function resolveCharacterShape(asset, style, kind, files) {
9714
10285
  for (const [direction, rel] of Object.entries(byDirection)) {
9715
10286
  const image = await files.load(rel, `Reference sprite (${direction})`);
9716
10287
  shape.reference[direction] = {
9717
- path: import_node_path6.default.resolve(files.root, rel),
10288
+ path: import_node_path7.default.resolve(files.root, rel),
9718
10289
  sha256: image.hash,
9719
10290
  width: image.width,
9720
10291
  height: image.height,
@@ -9735,7 +10306,7 @@ async function resolveCharacterShape(asset, style, kind, files) {
9735
10306
  const animationMode = asset.animation.mode ?? (asset.animation.template ? "template" : "v3");
9736
10307
  const pose = async (rel, what) => {
9737
10308
  const image = await files.load(rel, what);
9738
- return { path: import_node_path6.default.resolve(files.root, rel), sha256: image.hash, width: image.width, height: image.height, format: image.format };
10309
+ return { path: import_node_path7.default.resolve(files.root, rel), sha256: image.hash, width: image.width, height: image.height, format: image.format };
9739
10310
  };
9740
10311
  shape.animation = {
9741
10312
  mode: animationMode,
@@ -9766,7 +10337,7 @@ async function resolveObjectProShape(assetId, asset, style, kind, files) {
9766
10337
  const south = typeof asset.reference === "string" ? asset.reference : asset.reference.south;
9767
10338
  if (south) {
9768
10339
  const image = await files.load(south, "Reference sprite");
9769
- shape.reference = { path: import_node_path6.default.resolve(files.root, south), sha256: image.hash, width: image.width, height: image.height, format: image.format };
10340
+ shape.reference = { path: import_node_path7.default.resolve(files.root, south), sha256: image.hash, width: image.width, height: image.height, format: image.format };
9770
10341
  }
9771
10342
  }
9772
10343
  if (asset.state) {
@@ -9794,7 +10365,7 @@ async function resolveObjectProShape(assetId, asset, style, kind, files) {
9794
10365
  }
9795
10366
  const pose = async (rel, what) => {
9796
10367
  const image = await files.load(rel, what);
9797
- return { path: import_node_path6.default.resolve(files.root, rel), sha256: image.hash, width: image.width, height: image.height, format: image.format };
10368
+ return { path: import_node_path7.default.resolve(files.root, rel), sha256: image.hash, width: image.width, height: image.height, format: image.format };
9798
10369
  };
9799
10370
  shape.animation = {
9800
10371
  mode: asset.animation.mode ?? "v3",
@@ -9814,11 +10385,11 @@ function characterDirections(directions) {
9814
10385
  }
9815
10386
 
9816
10387
  // src/project.ts
9817
- var import_node_path14 = __toESM(require("path"), 1);
10388
+ var import_node_path15 = __toESM(require("path"), 1);
9818
10389
 
9819
10390
  // src/env.ts
9820
- var import_node_fs7 = require("fs");
9821
- var import_node_path7 = __toESM(require("path"), 1);
10391
+ var import_node_fs8 = require("fs");
10392
+ var import_node_path8 = __toESM(require("path"), 1);
9822
10393
  function loadEnvFiles(dir) {
9823
10394
  const loaded = [];
9824
10395
  for (const [file, vars] of envFiles(dir)) {
@@ -9832,10 +10403,10 @@ function loadEnvFiles(dir) {
9832
10403
  function envFiles(dir) {
9833
10404
  const out = [];
9834
10405
  for (const name of [".env.local", ".env"]) {
9835
- const file = import_node_path7.default.join(dir, name);
9836
- if (!(0, import_node_fs7.existsSync)(file)) continue;
10406
+ const file = import_node_path8.default.join(dir, name);
10407
+ if (!(0, import_node_fs8.existsSync)(file)) continue;
9837
10408
  try {
9838
- out.push([file, parseEnv((0, import_node_fs7.readFileSync)(file, "utf8"))]);
10409
+ out.push([file, parseEnv((0, import_node_fs8.readFileSync)(file, "utf8"))]);
9839
10410
  } catch {
9840
10411
  }
9841
10412
  }
@@ -9861,12 +10432,12 @@ function parseEnv(contents) {
9861
10432
  }
9862
10433
 
9863
10434
  // src/pipeline/plan.ts
9864
- var import_node_fs10 = require("fs");
9865
- var import_node_path13 = __toESM(require("path"), 1);
10435
+ var import_node_fs11 = require("fs");
10436
+ var import_node_path14 = __toESM(require("path"), 1);
9866
10437
 
9867
10438
  // src/pipeline/mirror.ts
9868
- var import_promises4 = require("fs/promises");
9869
- var import_node_path8 = __toESM(require("path"), 1);
10439
+ var import_promises5 = require("fs/promises");
10440
+ var import_node_path9 = __toESM(require("path"), 1);
9870
10441
 
9871
10442
  // src/palette.ts
9872
10443
  function normalizePalette(values) {
@@ -10003,12 +10574,12 @@ async function deriveMirror(spec, lock, lockPath, opts = {}) {
10003
10574
  const outputs = [];
10004
10575
  const written = [];
10005
10576
  for (const [index, member] of members.entries()) {
10006
- const bytes = flipHorizontal(await (0, import_promises4.readFile)(member.file));
10577
+ const bytes = flipHorizontal(await (0, import_promises5.readFile)(member.file));
10007
10578
  const target = expectedOutputPath(spec, member.role, index, members.length, MediaType.PNG);
10008
- await (0, import_promises4.mkdir)(import_node_path8.default.dirname(target), { recursive: true });
10579
+ await (0, import_promises5.mkdir)(import_node_path9.default.dirname(target), { recursive: true });
10009
10580
  const tmp = `${target}.pixelkiln.tmp`;
10010
- await (0, import_promises4.writeFile)(tmp, bytes);
10011
- await (0, import_promises4.rename)(tmp, target);
10581
+ await (0, import_promises5.writeFile)(tmp, bytes);
10582
+ await (0, import_promises5.rename)(tmp, target);
10012
10583
  written.push(target);
10013
10584
  outputs.push({
10014
10585
  path: portableOutputPath(target, spec.root),
@@ -10067,20 +10638,20 @@ async function deriveMirror(spec, lock, lockPath, opts = {}) {
10067
10638
  }
10068
10639
 
10069
10640
  // src/pipeline/quality-profile.ts
10070
- var import_node_fs8 = require("fs");
10071
- var import_node_path11 = __toESM(require("path"), 1);
10641
+ var import_node_fs9 = require("fs");
10642
+ var import_node_path12 = __toESM(require("path"), 1);
10072
10643
 
10073
10644
  // src/pipeline/refine.ts
10074
- var import_node_path10 = __toESM(require("path"), 1);
10645
+ var import_node_path11 = __toESM(require("path"), 1);
10075
10646
  var import_node_child_process2 = require("child_process");
10076
- var import_promises6 = require("fs/promises");
10647
+ var import_promises7 = require("fs/promises");
10077
10648
  var import_node_os3 = require("os");
10078
10649
  var import_node_util = require("util");
10079
10650
 
10080
10651
  // src/artifacts.ts
10081
- var import_node_path9 = __toESM(require("path"), 1);
10652
+ var import_node_path10 = __toESM(require("path"), 1);
10082
10653
  var import_node_crypto6 = require("crypto");
10083
- var import_promises5 = require("fs/promises");
10654
+ var import_promises6 = require("fs/promises");
10084
10655
  var activeTransactions = /* @__PURE__ */ new Set();
10085
10656
  function message(error) {
10086
10657
  return error instanceof Error ? error.message : String(error);
@@ -10089,7 +10660,7 @@ function digest(data) {
10089
10660
  return (0, import_node_crypto6.createHash)("sha256").update(data).digest("hex");
10090
10661
  }
10091
10662
  function portableRelative(from, to) {
10092
- return import_node_path9.default.relative(from, import_node_path9.default.resolve(to)).split(import_node_path9.default.sep).join("/") || ".";
10663
+ return import_node_path10.default.relative(from, import_node_path10.default.resolve(to)).split(import_node_path10.default.sep).join("/") || ".";
10093
10664
  }
10094
10665
  function canonical2(value) {
10095
10666
  if (value === null || typeof value === "string" || typeof value === "boolean") return value;
@@ -10139,12 +10710,12 @@ function parseArtifactManifest(absolute, data) {
10139
10710
  return raw;
10140
10711
  }
10141
10712
  async function readArtifactBundleManifest(manifestPath) {
10142
- const absolute = import_node_path9.default.resolve(manifestPath);
10143
- return parseArtifactManifest(absolute, await (0, import_promises5.readFile)(absolute, "utf8"));
10713
+ const absolute = import_node_path10.default.resolve(manifestPath);
10714
+ return parseArtifactManifest(absolute, await (0, import_promises6.readFile)(absolute, "utf8"));
10144
10715
  }
10145
10716
  async function readOptional(file) {
10146
10717
  try {
10147
- return await (0, import_promises5.readFile)(file);
10718
+ return await (0, import_promises6.readFile)(file);
10148
10719
  } catch (error) {
10149
10720
  if (isCode(error, "ENOENT")) return null;
10150
10721
  throw error;
@@ -10163,8 +10734,8 @@ function processIsAlive(pid) {
10163
10734
  }
10164
10735
  }
10165
10736
  function validTemporaryPath(candidate, destination, type) {
10166
- return import_node_path9.default.dirname(candidate) === import_node_path9.default.dirname(destination) && import_node_path9.default.basename(candidate).startsWith(
10167
- `.${import_node_path9.default.basename(destination)}.pixelkiln-${type}-`
10737
+ return import_node_path10.default.dirname(candidate) === import_node_path10.default.dirname(destination) && import_node_path10.default.basename(candidate).startsWith(
10738
+ `.${import_node_path10.default.basename(destination)}.pixelkiln-${type}-`
10168
10739
  );
10169
10740
  }
10170
10741
  function parseTransaction(journal, data) {
@@ -10180,14 +10751,14 @@ function parseTransaction(journal, data) {
10180
10751
  return raw;
10181
10752
  }
10182
10753
  async function removeTransactionFiles(journal) {
10183
- await (0, import_promises5.rm)(journal, { force: true });
10184
- await (0, import_promises5.rm)(transactionMarker(journal), { force: true });
10754
+ await (0, import_promises6.rm)(journal, { force: true });
10755
+ await (0, import_promises6.rm)(transactionMarker(journal), { force: true });
10185
10756
  }
10186
10757
  async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
10187
- const journal = import_node_path9.default.resolve(recoveryFile);
10758
+ const journal = import_node_path10.default.resolve(recoveryFile);
10188
10759
  const bytes = await readOptional(journal);
10189
10760
  if (!bytes) {
10190
- await (0, import_promises5.rm)(transactionMarker(journal), { force: true });
10761
+ await (0, import_promises6.rm)(transactionMarker(journal), { force: true });
10191
10762
  return;
10192
10763
  }
10193
10764
  if (activeTransactions.has(journal)) {
@@ -10200,7 +10771,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
10200
10771
  );
10201
10772
  }
10202
10773
  for (const entry of transaction.entries) {
10203
- 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")) {
10774
+ 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_path10.default.resolve(entry.destination)) || !validTemporaryPath(entry.stage, entry.destination, "stage") || entry.backup !== void 0 && !validTemporaryPath(entry.backup, entry.destination, "backup")) {
10204
10775
  throw new Error(
10205
10776
  `Refusing unsafe artifact recovery from ${journal}; its destinations or temporary paths do not match the current bundle.`
10206
10777
  );
@@ -10226,7 +10797,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
10226
10797
  }
10227
10798
  await remove2(entry.destination, errors);
10228
10799
  try {
10229
- await (0, import_promises5.rename)(entry.backup, entry.destination);
10800
+ await (0, import_promises6.rename)(entry.backup, entry.destination);
10230
10801
  } catch (error) {
10231
10802
  errors.push(
10232
10803
  `could not restore ${entry.destination}; previous file remains at ${entry.backup}: ${message(error)}`
@@ -10246,7 +10817,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
10246
10817
  await removeTransactionFiles(journal);
10247
10818
  }
10248
10819
  function createArtifactBundleManifest(manifestPath, outputs, provenance) {
10249
- const root = import_node_path9.default.dirname(import_node_path9.default.resolve(manifestPath));
10820
+ const root = import_node_path10.default.dirname(import_node_path10.default.resolve(manifestPath));
10250
10821
  const sources = provenance.sources.map((source) => ({
10251
10822
  ...source,
10252
10823
  path: portableRelative(root, source.path)
@@ -10276,11 +10847,11 @@ function withArtifactManifest(manifestPath, outputs, provenance) {
10276
10847
  ];
10277
10848
  }
10278
10849
  async function writeManagedArtifactBundle(manifestPath, outputs, provenance, options = {}) {
10279
- const absoluteManifest = import_node_path9.default.resolve(manifestPath);
10850
+ const absoluteManifest = import_node_path10.default.resolve(manifestPath);
10280
10851
  const recoveryFile = `${absoluteManifest}.transaction`;
10281
10852
  const allowedDestinations = /* @__PURE__ */ new Set([
10282
10853
  absoluteManifest,
10283
- ...outputs.map((output) => import_node_path9.default.resolve(output.path))
10854
+ ...outputs.map((output) => import_node_path10.default.resolve(output.path))
10284
10855
  ]);
10285
10856
  await recoverArtifactTransaction(recoveryFile, allowedDestinations);
10286
10857
  const existingManifestBytes = await readOptional(absoluteManifest);
@@ -10298,13 +10869,13 @@ async function writeManagedArtifactBundle(manifestPath, outputs, provenance, opt
10298
10869
  }
10299
10870
  }
10300
10871
  if (!options.force) {
10301
- const root = import_node_path9.default.dirname(absoluteManifest);
10872
+ const root = import_node_path10.default.dirname(absoluteManifest);
10302
10873
  const recorded = new Map(
10303
- previous?.outputs.map((output) => [import_node_path9.default.resolve(root, output.path), output.sha256]) ?? []
10874
+ previous?.outputs.map((output) => [import_node_path10.default.resolve(root, output.path), output.sha256]) ?? []
10304
10875
  );
10305
10876
  const conflicts = [];
10306
10877
  for (const output of outputs) {
10307
- const destination = import_node_path9.default.resolve(output.path);
10878
+ const destination = import_node_path10.default.resolve(output.path);
10308
10879
  const current = await readOptional(destination);
10309
10880
  if (!current || digest(current) === digest(output.data)) continue;
10310
10881
  const expected = recorded.get(destination);
@@ -10326,15 +10897,15 @@ Pass { force: true } (CLI: --force) to take ownership and replace the complete b
10326
10897
  );
10327
10898
  }
10328
10899
  async function verifyArtifactBundle(manifestPath) {
10329
- const absolute = import_node_path9.default.resolve(manifestPath);
10900
+ const absolute = import_node_path10.default.resolve(manifestPath);
10330
10901
  const raw = await readArtifactBundleManifest(absolute);
10331
- const root = import_node_path9.default.dirname(absolute);
10902
+ const root = import_node_path10.default.dirname(absolute);
10332
10903
  const changedSources = [];
10333
10904
  const changedOutputs = [];
10334
10905
  for (const source of raw.sources) {
10335
10906
  let actual = null;
10336
10907
  try {
10337
- actual = digest(await (0, import_promises5.readFile)(import_node_path9.default.resolve(root, source.path)));
10908
+ actual = digest(await (0, import_promises6.readFile)(import_node_path10.default.resolve(root, source.path)));
10338
10909
  } catch (error) {
10339
10910
  if (!isCode(error, "ENOENT")) throw error;
10340
10911
  }
@@ -10342,7 +10913,7 @@ async function verifyArtifactBundle(manifestPath) {
10342
10913
  }
10343
10914
  for (const output of raw.outputs) {
10344
10915
  try {
10345
- if (digest(await (0, import_promises5.readFile)(import_node_path9.default.resolve(root, output.path))) !== output.sha256) {
10916
+ if (digest(await (0, import_promises6.readFile)(import_node_path10.default.resolve(root, output.path))) !== output.sha256) {
10346
10917
  changedOutputs.push(output.path);
10347
10918
  }
10348
10919
  } catch (error) {
@@ -10368,7 +10939,7 @@ function isCode(error, code) {
10368
10939
  }
10369
10940
  async function remove2(file, errors) {
10370
10941
  try {
10371
- await (0, import_promises5.rm)(file, { force: true });
10942
+ await (0, import_promises6.rm)(file, { force: true });
10372
10943
  } catch (error) {
10373
10944
  errors?.push(`could not remove ${file}: ${message(error)}`);
10374
10945
  }
@@ -10381,7 +10952,7 @@ async function rollback(prepared) {
10381
10952
  for (const artifact of [...prepared].reverse()) {
10382
10953
  if (!artifact.backup || !artifact.backedUp) continue;
10383
10954
  try {
10384
- await (0, import_promises5.rename)(artifact.backup, artifact.destination);
10955
+ await (0, import_promises6.rename)(artifact.backup, artifact.destination);
10385
10956
  artifact.backedUp = false;
10386
10957
  } catch (error) {
10387
10958
  errors.push(
@@ -10398,7 +10969,7 @@ async function writeArtifactBundle(files, options = {}) {
10398
10969
  if (!files.length) throw new Error("An artifact bundle must contain at least one file.");
10399
10970
  const normalized = files.map((file) => {
10400
10971
  if (!file.path.trim()) throw new Error("Artifact paths cannot be empty.");
10401
- return { destination: import_node_path9.default.resolve(file.path), data: Buffer.from(file.data) };
10972
+ return { destination: import_node_path10.default.resolve(file.path), data: Buffer.from(file.data) };
10402
10973
  });
10403
10974
  const destinations = /* @__PURE__ */ new Set();
10404
10975
  for (const artifact of normalized) {
@@ -10414,7 +10985,7 @@ async function writeArtifactBundle(files, options = {}) {
10414
10985
  const unchanged = [];
10415
10986
  for (const artifact of normalized) {
10416
10987
  try {
10417
- const current = await (0, import_promises5.readFile)(artifact.destination);
10988
+ const current = await (0, import_promises6.readFile)(artifact.destination);
10418
10989
  if (current.equals(artifact.data)) {
10419
10990
  unchanged.push(artifact.destination);
10420
10991
  } else {
@@ -10428,21 +10999,21 @@ async function writeArtifactBundle(files, options = {}) {
10428
10999
  if (!changed.length) return { changed: [], unchanged };
10429
11000
  const token = (0, import_node_crypto6.randomUUID)();
10430
11001
  for (const [index, artifact] of changed.entries()) {
10431
- const basename = import_node_path9.default.basename(artifact.destination);
10432
- artifact.stage = import_node_path9.default.join(
10433
- import_node_path9.default.dirname(artifact.destination),
11002
+ const basename = import_node_path10.default.basename(artifact.destination);
11003
+ artifact.stage = import_node_path10.default.join(
11004
+ import_node_path10.default.dirname(artifact.destination),
10434
11005
  `.${basename}.pixelkiln-stage-${token}-${index}`
10435
11006
  );
10436
11007
  if (artifact.existed) {
10437
- artifact.backup = import_node_path9.default.join(
10438
- import_node_path9.default.dirname(artifact.destination),
11008
+ artifact.backup = import_node_path10.default.join(
11009
+ import_node_path10.default.dirname(artifact.destination),
10439
11010
  `.${basename}.pixelkiln-backup-${token}-${index}`
10440
11011
  );
10441
11012
  }
10442
11013
  }
10443
- const journal = options.recoveryFile ? import_node_path9.default.resolve(options.recoveryFile) : null;
11014
+ const journal = options.recoveryFile ? import_node_path10.default.resolve(options.recoveryFile) : null;
10444
11015
  if (journal) {
10445
- await (0, import_promises5.mkdir)(import_node_path9.default.dirname(journal), { recursive: true });
11016
+ await (0, import_promises6.mkdir)(import_node_path10.default.dirname(journal), { recursive: true });
10446
11017
  const transaction = {
10447
11018
  format: "pixelkiln-artifact-transaction",
10448
11019
  version: 1,
@@ -10456,7 +11027,7 @@ async function writeArtifactBundle(files, options = {}) {
10456
11027
  }))
10457
11028
  };
10458
11029
  try {
10459
- await (0, import_promises5.writeFile)(journal, JSON.stringify(transaction, null, 2) + "\n", { flag: "wx" });
11030
+ await (0, import_promises6.writeFile)(journal, JSON.stringify(transaction, null, 2) + "\n", { flag: "wx" });
10460
11031
  activeTransactions.add(journal);
10461
11032
  } catch (error) {
10462
11033
  throw new Error(
@@ -10467,9 +11038,9 @@ async function writeArtifactBundle(files, options = {}) {
10467
11038
  }
10468
11039
  try {
10469
11040
  for (const [index, artifact] of changed.entries()) {
10470
- await (0, import_promises5.mkdir)(import_node_path9.default.dirname(artifact.destination), { recursive: true });
11041
+ await (0, import_promises6.mkdir)(import_node_path10.default.dirname(artifact.destination), { recursive: true });
10471
11042
  await options.beforeStage?.(artifact.destination, index);
10472
- await (0, import_promises5.writeFile)(artifact.stage, artifact.data, { flag: "wx" });
11043
+ await (0, import_promises6.writeFile)(artifact.stage, artifact.data, { flag: "wx" });
10473
11044
  }
10474
11045
  } catch (error) {
10475
11046
  const cleanupErrors2 = await rollback(changed);
@@ -10482,17 +11053,17 @@ async function writeArtifactBundle(files, options = {}) {
10482
11053
  try {
10483
11054
  for (const artifact of changed) {
10484
11055
  if (!artifact.existed) continue;
10485
- await (0, import_promises5.rename)(artifact.destination, artifact.backup);
11056
+ await (0, import_promises6.rename)(artifact.destination, artifact.backup);
10486
11057
  artifact.backedUp = true;
10487
11058
  }
10488
11059
  for (const [index, artifact] of changed.entries()) {
10489
11060
  await options.beforePromote?.(artifact.destination, index);
10490
- await (0, import_promises5.rename)(artifact.stage, artifact.destination);
11061
+ await (0, import_promises6.rename)(artifact.stage, artifact.destination);
10491
11062
  artifact.stage = void 0;
10492
11063
  artifact.promoted = true;
10493
11064
  }
10494
11065
  if (journal) {
10495
- await (0, import_promises5.writeFile)(transactionMarker(journal), `${token}
11066
+ await (0, import_promises6.writeFile)(transactionMarker(journal), `${token}
10496
11067
  `, { flag: "wx" });
10497
11068
  durableCommit = true;
10498
11069
  await options.afterCommit?.();
@@ -10733,16 +11304,16 @@ function sameFrameGrid(first, next) {
10733
11304
  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;
10734
11305
  }
10735
11306
  async function refineAsset(options) {
10736
- const source = import_node_path10.default.resolve(options.source);
10737
- const output = import_node_path10.default.resolve(options.output);
11307
+ const source = import_node_path11.default.resolve(options.source);
11308
+ const output = import_node_path11.default.resolve(options.output);
10738
11309
  if (source === output) throw new Error("Refinement source and output must be different files.");
10739
- const record = import_node_path10.default.resolve(qualityRecordPath(output));
11310
+ const record = import_node_path11.default.resolve(qualityRecordPath(output));
10740
11311
  const palette = normalizePalette(options.palette).map((color) => color.hex);
10741
11312
  const minGridConfidence = requireConfidence(options.minGridConfidence ?? "high");
10742
11313
  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.");
10743
11314
  let sourcePng;
10744
11315
  try {
10745
- sourcePng = decodePng(await (0, import_promises6.readFile)(source));
11316
+ sourcePng = decodePng(await (0, import_promises7.readFile)(source));
10746
11317
  } catch (error) {
10747
11318
  throw new Error(`Cannot read refinement source ${source}: ${message2(error)}`, { cause: error });
10748
11319
  }
@@ -10759,8 +11330,8 @@ async function refineAsset(options) {
10759
11330
  );
10760
11331
  }
10761
11332
  }
10762
- const temp = await (0, import_promises6.mkdtemp)(import_node_path10.default.join((0, import_node_os3.tmpdir)(), "pixelkiln-refine-"));
10763
- const recovered = import_node_path10.default.join(temp, "native.png");
11333
+ const temp = await (0, import_promises7.mkdtemp)(import_node_path11.default.join((0, import_node_os3.tmpdir)(), "pixelkiln-refine-"));
11334
+ const recovered = import_node_path11.default.join(temp, "native.png");
10764
11335
  const fixerCommand = compatibilityCommand ?? options.fixerPython ?? process.env.PIXELKILN_PIXEL_FIXER_PYTHON ?? "python3";
10765
11336
  const args = compatibilityCommand ? [...options.fixerArgsPrefix ?? [], source, "--extract", recovered] : ["-c", PYTHON_FIXER_BRIDGE, source, recovered];
10766
11337
  try {
@@ -10781,7 +11352,7 @@ async function refineAsset(options) {
10781
11352
  );
10782
11353
  }
10783
11354
  const detection = parseDetection(firstJsonObject(stdout));
10784
- const native = decodePng(await (0, import_promises6.readFile)(recovered));
11355
+ const native = decodePng(await (0, import_promises7.readFile)(recovered));
10785
11356
  const quantized = quantizeToPalette(native, palette);
10786
11357
  const png = encodeRgbaPng(quantized.width, quantized.height, quantized.pixels);
10787
11358
  const audit = auditRefinement(
@@ -10831,7 +11402,7 @@ async function refineAsset(options) {
10831
11402
  );
10832
11403
  return { source, output, record, detection, palette, audit };
10833
11404
  } finally {
10834
- await (0, import_promises6.rm)(temp, { recursive: true, force: true });
11405
+ await (0, import_promises7.rm)(temp, { recursive: true, force: true });
10835
11406
  }
10836
11407
  }
10837
11408
  async function refineFrameSet(options) {
@@ -10845,18 +11416,18 @@ async function refineFrameSet(options) {
10845
11416
  if (roles.some((role) => !role.trim()) || new Set(roles).size !== roles.length) {
10846
11417
  throw new Error("Frame-set roles must be non-empty and unique.");
10847
11418
  }
10848
- const output = import_node_path10.default.resolve(options.output);
10849
- const record = import_node_path10.default.resolve(qualityRecordPath(output));
11419
+ const output = import_node_path11.default.resolve(options.output);
11420
+ const record = import_node_path11.default.resolve(qualityRecordPath(output));
10850
11421
  const palette = normalizeRefinementPalette(options.palette);
10851
11422
  const finalOutputs = options.sources.map((source, index) => qualityFrameOutputPath(output, source.role, index));
10852
11423
  if (new Set(finalOutputs).size !== finalOutputs.length) {
10853
11424
  throw new Error("Frame-set roles resolve to duplicate output filenames.");
10854
11425
  }
10855
- const sourceSet = new Set(options.sources.map((source) => import_node_path10.default.resolve(source.path)));
11426
+ const sourceSet = new Set(options.sources.map((source) => import_node_path11.default.resolve(source.path)));
10856
11427
  if (finalOutputs.some((file) => sourceSet.has(file))) {
10857
11428
  throw new Error("Frame-set refinement sources and outputs must be different files.");
10858
11429
  }
10859
- const temp = await (0, import_promises6.mkdtemp)(import_node_path10.default.join((0, import_node_os3.tmpdir)(), "pixelkiln-refine-frames-"));
11430
+ const temp = await (0, import_promises7.mkdtemp)(import_node_path11.default.join((0, import_node_os3.tmpdir)(), "pixelkiln-refine-frames-"));
10860
11431
  try {
10861
11432
  const frames = [];
10862
11433
  const outputs = [];
@@ -10864,8 +11435,8 @@ async function refineFrameSet(options) {
10864
11435
  const sourcePaths = [];
10865
11436
  let firstOptions = null;
10866
11437
  for (const [index, sourceEntry] of options.sources.entries()) {
10867
- const source = import_node_path10.default.resolve(sourceEntry.path);
10868
- const temporaryOutput = import_node_path10.default.join(temp, `${String(index).padStart(2, "0")}.png`);
11438
+ const source = import_node_path11.default.resolve(sourceEntry.path);
11439
+ const temporaryOutput = import_node_path11.default.join(temp, `${String(index).padStart(2, "0")}.png`);
10869
11440
  const refined = await refineAsset({
10870
11441
  source,
10871
11442
  output: temporaryOutput,
@@ -10897,7 +11468,7 @@ async function refineFrameSet(options) {
10897
11468
  });
10898
11469
  outputs.push({
10899
11470
  path: finalOutputs[index],
10900
- data: await (0, import_promises6.readFile)(temporaryOutput)
11471
+ data: await (0, import_promises7.readFile)(temporaryOutput)
10901
11472
  });
10902
11473
  }
10903
11474
  const first = frames[0];
@@ -10940,23 +11511,23 @@ async function refineFrameSet(options) {
10940
11511
  );
10941
11512
  return {
10942
11513
  sources: sourcePaths,
10943
- outputs: outputs.map((item) => import_node_path10.default.resolve(item.path)),
11514
+ outputs: outputs.map((item) => import_node_path11.default.resolve(item.path)),
10944
11515
  record,
10945
11516
  frames,
10946
11517
  palette,
10947
11518
  audit
10948
11519
  };
10949
11520
  } finally {
10950
- await (0, import_promises6.rm)(temp, { recursive: true, force: true });
11521
+ await (0, import_promises7.rm)(temp, { recursive: true, force: true });
10951
11522
  }
10952
11523
  }
10953
11524
  async function checkQualityRecord(recordPath) {
10954
- const record = import_node_path10.default.resolve(recordPath);
11525
+ const record = import_node_path11.default.resolve(recordPath);
10955
11526
  const manifest = await readArtifactBundleManifest(record);
10956
11527
  const options = parseRecordOptions(manifest, record);
10957
11528
  const verification = await verifyArtifactBundle(record);
10958
- const sources = manifest.sources.map((source2) => import_node_path10.default.resolve(import_node_path10.default.dirname(record), source2.path));
10959
- const outputs = manifest.outputs.map((output2) => import_node_path10.default.resolve(import_node_path10.default.dirname(record), output2.path));
11529
+ const sources = manifest.sources.map((source2) => import_node_path11.default.resolve(import_node_path11.default.dirname(record), source2.path));
11530
+ const outputs = manifest.outputs.map((output2) => import_node_path11.default.resolve(import_node_path11.default.dirname(record), output2.path));
10960
11531
  const source = sources[0] ?? "";
10961
11532
  const output = outputs[0] ?? "";
10962
11533
  const expectedCount = options.frameSet?.count ?? 1;
@@ -10989,7 +11560,7 @@ async function checkQualityRecord(recordPath) {
10989
11560
  async function approveQualityRecord(recordPath, approval) {
10990
11561
  const reviewer = approval.reviewer.trim();
10991
11562
  if (!reviewer) throw new Error("A human reviewer name is required.");
10992
- const record = import_node_path10.default.resolve(recordPath);
11563
+ const record = import_node_path11.default.resolve(recordPath);
10993
11564
  const manifest = await readArtifactBundleManifest(record);
10994
11565
  const options = parseRecordOptions(manifest, record);
10995
11566
  const verification = await verifyArtifactBundle(record);
@@ -11010,14 +11581,14 @@ async function approveQualityRecord(recordPath, approval) {
11010
11581
  `A refinement record must contain exactly ${expectedCount} source(s) and output(s).`
11011
11582
  );
11012
11583
  }
11013
- const root = import_node_path10.default.dirname(record);
11584
+ const root = import_node_path11.default.dirname(record);
11014
11585
  const outputs = await Promise.all(manifest.outputs.map(async (entry) => {
11015
- const outputPath = import_node_path10.default.resolve(root, entry.path);
11016
- return { path: outputPath, data: await (0, import_promises6.readFile)(outputPath) };
11586
+ const outputPath = import_node_path11.default.resolve(root, entry.path);
11587
+ return { path: outputPath, data: await (0, import_promises7.readFile)(outputPath) };
11017
11588
  }));
11018
11589
  const sources = manifest.sources.map((source) => ({
11019
11590
  ...source,
11020
- path: import_node_path10.default.resolve(root, source.path)
11591
+ path: import_node_path11.default.resolve(root, source.path)
11021
11592
  }));
11022
11593
  const approved = {
11023
11594
  ...options,
@@ -11049,8 +11620,8 @@ async function qualitySource(spec, lock) {
11049
11620
  if (spec.generator === "frames") {
11050
11621
  return { items: null, reason: "frame-set quality requires ordered downloaded provider outputs" };
11051
11622
  }
11052
- const source = import_node_path11.default.resolve(spec.root, spec.source);
11053
- return (0, import_node_fs8.existsSync)(source) ? { items: [{ role: "source", path: source }] } : { items: null, reason: `declared source is missing: ${spec.source}` };
11623
+ const source = import_node_path12.default.resolve(spec.root, spec.source);
11624
+ return (0, import_node_fs9.existsSync)(source) ? { items: [{ role: "source", path: source }] } : { items: null, reason: `declared source is missing: ${spec.source}` };
11054
11625
  }
11055
11626
  const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
11056
11627
  if (!entry) return { items: null, reason: "raw provider output is not in the lockfile" };
@@ -11079,7 +11650,7 @@ async function qualitySource(spec, lock) {
11079
11650
  }
11080
11651
  if (role) roles.add(role);
11081
11652
  const source = currentEntryOutputPath(entry, spec, index);
11082
- if (!(0, import_node_fs8.existsSync)(source)) return { items: null, reason: `raw provider output is missing: ${source}` };
11653
+ if (!(0, import_node_fs9.existsSync)(source)) return { items: null, reason: `raw provider output is missing: ${source}` };
11083
11654
  if (await sha256File(source) !== output.sha256) {
11084
11655
  return { items: null, reason: "raw provider output was modified after download" };
11085
11656
  }
@@ -11098,7 +11669,7 @@ async function inspectQualityProfile(spec, lock) {
11098
11669
  }
11099
11670
  const sourcePaths = source.items.map((item) => item.path);
11100
11671
  const expectedOutputs = spec.generator === "frames" ? source.items.map((item, index) => qualityFrameOutputPath(output, item.role, index)) : [output];
11101
- if (!(0, import_node_fs8.existsSync)(record)) {
11672
+ if (!(0, import_node_fs9.existsSync)(record)) {
11102
11673
  return {
11103
11674
  key,
11104
11675
  state: "needs-refinement",
@@ -11116,7 +11687,7 @@ async function inspectQualityProfile(spec, lock) {
11116
11687
  const expectedRevision = spec.quality.fixerRevision ?? PIXEL_ART_FIXER_REVISION;
11117
11688
  const expectedTransparency = spec.quality.minTransparency ?? null;
11118
11689
  const expectedFrameSet = spec.generator === "frames" ? { fps: spec.quality.fps ?? 12, count: source.items.length, roles: source.items.map((item) => item.role) } : null;
11119
- 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);
11690
+ const contractChanged = JSON.stringify(check.sources.map((item) => import_node_path12.default.resolve(item))) !== JSON.stringify(sourcePaths.map((item) => import_node_path12.default.resolve(item))) || JSON.stringify(check.outputs.map((item) => import_node_path12.default.resolve(item))) !== JSON.stringify(expectedOutputs.map((item) => import_node_path12.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);
11120
11691
  if (contractChanged) {
11121
11692
  return {
11122
11693
  key,
@@ -11279,8 +11850,8 @@ async function requireApprovedQualitySources(specs, lock, assetIds, outputRoles)
11279
11850
  }
11280
11851
 
11281
11852
  // src/pipeline/revision.ts
11282
- var import_node_fs9 = require("fs");
11283
- var import_node_path12 = __toESM(require("path"), 1);
11853
+ var import_node_fs10 = require("fs");
11854
+ var import_node_path13 = __toESM(require("path"), 1);
11284
11855
  async function inspectRevisionReadiness(spec, lock) {
11285
11856
  if (spec.mirror) return inspectMirror(spec, lock, /* @__PURE__ */ new Set());
11286
11857
  if (spec.character?.kind === "outfit") return inspectOutfit(spec, lock, /* @__PURE__ */ new Set());
@@ -11288,6 +11859,7 @@ async function inspectRevisionReadiness(spec, lock) {
11288
11859
  if (spec.character?.styleAnchor) return inspectStyleAnchor(spec, lock, /* @__PURE__ */ new Set());
11289
11860
  if (spec.objectPro?.parentSpec) return inspectObjectProParent(spec, lock, /* @__PURE__ */ new Set());
11290
11861
  if (spec.scene) return inspectScene(spec);
11862
+ if (spec.vfx?.sprite && "file" in spec.vfx.sprite) return inspectVfxSprite(spec);
11291
11863
  if (!spec.revision) return null;
11292
11864
  return inspectRevision(spec, lock, /* @__PURE__ */ new Set());
11293
11865
  }
@@ -11296,7 +11868,7 @@ async function inspectStyleAnchor(spec, lock, seen) {
11296
11868
  const label = `style anchor ${spec.styleId}/${anchor.assetId}`;
11297
11869
  const dependency = await inspectParent(anchor.spec, lock, seen);
11298
11870
  if (!dependency.ready) return { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
11299
- if (!anchor.sha256 || !(0, import_node_fs9.existsSync)(anchor.file)) return { ready: false, reason: `${label} has no generated south-facing file yet` };
11871
+ if (!anchor.sha256 || !(0, import_node_fs10.existsSync)(anchor.file)) return { ready: false, reason: `${label} has no generated south-facing file yet` };
11300
11872
  if (await sha256File(anchor.file) !== anchor.sha256) {
11301
11873
  return { ready: false, reason: `${label} changed after the manifest was resolved` };
11302
11874
  }
@@ -11310,11 +11882,17 @@ async function inspectScene(spec) {
11310
11882
  const files = [{ label: "scene image", file: scene.file, sha256: scene.sha256 }];
11311
11883
  if (scene.placement.type === "mask") files.push({ label: "scene mask", file: scene.placement.file, sha256: scene.placement.sha256 });
11312
11884
  for (const { label, file, sha256: sha2562 } of files) {
11313
- if (!sha2562 || !(0, import_node_fs9.existsSync)(file)) return { ready: false, reason: `${label} does not exist yet: ${file}` };
11885
+ if (!sha2562 || !(0, import_node_fs10.existsSync)(file)) return { ready: false, reason: `${label} does not exist yet: ${file}` };
11314
11886
  if (await sha256File(file) !== sha2562) return { ready: false, reason: `${label} changed after the manifest was resolved` };
11315
11887
  }
11316
11888
  return { ready: true, reason: "scene is current" };
11317
11889
  }
11890
+ async function inspectVfxSprite(spec) {
11891
+ const sprite = spec.vfx.sprite;
11892
+ if (!sprite.sha256 || !(0, import_node_fs10.existsSync)(sprite.file)) return { ready: false, reason: `vfx sprite does not exist yet: ${sprite.file}` };
11893
+ if (await sha256File(sprite.file) !== sprite.sha256) return { ready: false, reason: "vfx sprite changed after the manifest was resolved" };
11894
+ return { ready: true, reason: "vfx sprite is current" };
11895
+ }
11318
11896
  async function inspectMirror(spec, lock, seen) {
11319
11897
  const source = spec.mirror.sourceSpec;
11320
11898
  const label = `mirror source ${spec.styleId}/${spec.mirror.sourceAssetId}`;
@@ -11327,7 +11905,7 @@ async function inspectOutfit(spec, lock, seen) {
11327
11905
  const label = `outfit source ${spec.styleId}/${character.parentAssetId}`;
11328
11906
  const dependency = await inspectParent(character.parentSpec, lock, seen);
11329
11907
  if (!dependency.ready) return { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
11330
- if (!(0, import_node_fs9.existsSync)(outfit.reference.path)) {
11908
+ if (!(0, import_node_fs10.existsSync)(outfit.reference.path)) {
11331
11909
  return { ready: false, reason: `outfit reference is missing: ${outfit.reference.path}` };
11332
11910
  }
11333
11911
  if (await sha256File(outfit.reference.path) !== outfit.reference.sha256) {
@@ -11341,7 +11919,7 @@ async function inspectCharacterParent(spec, lock, seen) {
11341
11919
  const label = `${character.kind} parent ${spec.styleId}/${character.parentAssetId}`;
11342
11920
  const dependency = await inspectParent(parentSpec, lock, seen);
11343
11921
  if (!dependency.ready) return { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
11344
- if (!character.parentSha256 || !character.parentFile || !(0, import_node_fs9.existsSync)(character.parentFile)) {
11922
+ if (!character.parentSha256 || !character.parentFile || !(0, import_node_fs10.existsSync)(character.parentFile)) {
11345
11923
  return { ready: false, reason: `${label} has no generated south-facing file yet` };
11346
11924
  }
11347
11925
  if (await sha256File(character.parentFile) !== character.parentSha256) {
@@ -11359,7 +11937,7 @@ async function inspectObjectProParent(spec, lock, seen) {
11359
11937
  const label = `${object.kind} parent ${spec.styleId}/${object.parentAssetId}`;
11360
11938
  const dependency = await inspectParent(parentSpec, lock, seen);
11361
11939
  if (!dependency.ready) return { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
11362
- if (!object.parentSha256 || !object.parentFile || !(0, import_node_fs9.existsSync)(object.parentFile)) {
11940
+ if (!object.parentSha256 || !object.parentFile || !(0, import_node_fs10.existsSync)(object.parentFile)) {
11363
11941
  return { ready: false, reason: `${label} has no generated south-facing file yet` };
11364
11942
  }
11365
11943
  if (await sha256File(object.parentFile) !== object.parentSha256) {
@@ -11385,7 +11963,7 @@ async function inspectRevision(spec, lock, seen) {
11385
11963
  const members = await inspectSourceMembers(spec, lock);
11386
11964
  if (!members.ready) return members;
11387
11965
  } else {
11388
- if (!revision.sourceSha256 || !(0, import_node_fs9.existsSync)(revision.sourceFile)) {
11966
+ if (!revision.sourceSha256 || !(0, import_node_fs10.existsSync)(revision.sourceFile)) {
11389
11967
  return { ready: false, reason: `revision source is missing: ${revision.sourceFile}` };
11390
11968
  }
11391
11969
  if (await sha256File(revision.sourceFile) !== revision.sourceSha256) {
@@ -11393,7 +11971,7 @@ async function inspectRevision(spec, lock, seen) {
11393
11971
  }
11394
11972
  }
11395
11973
  if (revision.maskFile) {
11396
- if (!revision.maskSha256 || !(0, import_node_fs9.existsSync)(revision.maskFile)) {
11974
+ if (!revision.maskSha256 || !(0, import_node_fs10.existsSync)(revision.maskFile)) {
11397
11975
  return { ready: false, reason: `revision mask is missing: ${revision.maskFile}` };
11398
11976
  }
11399
11977
  if (await sha256File(revision.maskFile) !== revision.maskSha256) {
@@ -11401,7 +11979,7 @@ async function inspectRevision(spec, lock, seen) {
11401
11979
  }
11402
11980
  }
11403
11981
  if (revision.lastFrameFile) {
11404
- if (!revision.lastFrameSha256 || !(0, import_node_fs9.existsSync)(revision.lastFrameFile)) {
11982
+ if (!revision.lastFrameSha256 || !(0, import_node_fs10.existsSync)(revision.lastFrameFile)) {
11405
11983
  return { ready: false, reason: `revision last frame is missing: ${revision.lastFrameFile}` };
11406
11984
  }
11407
11985
  if (await sha256File(revision.lastFrameFile) !== revision.lastFrameSha256) {
@@ -11414,7 +11992,7 @@ async function inspectSourceMembers(spec, lock) {
11414
11992
  const revision = spec.revision;
11415
11993
  const members = revision.sourceMembers;
11416
11994
  for (const member of members) {
11417
- if (!(0, import_node_fs9.existsSync)(member.file)) return { ready: false, reason: `revision source member is missing: ${member.file}` };
11995
+ if (!(0, import_node_fs10.existsSync)(member.file)) return { ready: false, reason: `revision source member is missing: ${member.file}` };
11418
11996
  if (await sha256File(member.file) !== member.sha256) {
11419
11997
  return { ready: false, reason: `revision source member changed after the manifest was resolved: ${member.file}` };
11420
11998
  }
@@ -11423,8 +12001,8 @@ async function inspectSourceMembers(spec, lock) {
11423
12001
  if (parent.source) return { ready: true, reason: "declared source set is present" };
11424
12002
  const entry = lock.entries[lockKey(parent.styleId, parent.assetId)];
11425
12003
  if (!entry) return { ready: false, reason: "revision source is not in the lockfile" };
11426
- const recorded = entry.outputs.map((_, index) => import_node_path12.default.resolve(currentEntryOutputPath(entry, parent, index))).sort();
11427
- const found = members.map((member) => import_node_path12.default.resolve(member.file)).sort();
12004
+ const recorded = entry.outputs.map((_, index) => import_node_path13.default.resolve(currentEntryOutputPath(entry, parent, index))).sort();
12005
+ const found = members.map((member) => import_node_path13.default.resolve(member.file)).sort();
11428
12006
  if (recorded.length !== found.length || recorded.some((file, index) => file !== found[index])) {
11429
12007
  return {
11430
12008
  ready: false,
@@ -11473,8 +12051,8 @@ async function inspectParent(spec, lock, seen) {
11473
12051
  };
11474
12052
  }
11475
12053
  if (spec.source) {
11476
- const source = import_node_path12.default.resolve(spec.root, spec.source);
11477
- return (0, import_node_fs9.existsSync)(source) ? { ready: true, reason: "declared source is present" } : { ready: false, reason: `declared source is missing: ${spec.source}` };
12054
+ const source = import_node_path13.default.resolve(spec.root, spec.source);
12055
+ return (0, import_node_fs10.existsSync)(source) ? { ready: true, reason: "declared source is present" } : { ready: false, reason: `declared source is missing: ${spec.source}` };
11478
12056
  }
11479
12057
  const entry = lock.entries[key];
11480
12058
  if (!entry) return { ready: false, reason: "parent is not in the lockfile" };
@@ -11488,7 +12066,7 @@ async function inspectParent(spec, lock, seen) {
11488
12066
  for (let index = 0; index < entry.outputs.length; index++) {
11489
12067
  const output = entry.outputs[index];
11490
12068
  const file = currentEntryOutputPath(entry, spec, index);
11491
- if (!(0, import_node_fs9.existsSync)(file)) return { ready: false, reason: `parent output is missing: ${file}` };
12069
+ if (!(0, import_node_fs10.existsSync)(file)) return { ready: false, reason: `parent output is missing: ${file}` };
11492
12070
  if (await sha256File(file) !== output.sha256) {
11493
12071
  return { ready: false, reason: `parent output was modified: ${file}` };
11494
12072
  }
@@ -11551,8 +12129,8 @@ async function buildPlan(specs, lock, opts = {}) {
11551
12129
  } else if (opts.force) {
11552
12130
  state = "missing";
11553
12131
  reason = "--force";
11554
- } else if (!entry && spec.source && spec.generator !== "frames") {
11555
- if ((0, import_node_fs10.existsSync)(import_node_path13.default.resolve(spec.root, spec.source))) {
12132
+ } else if (!entry && spec.source && spec.generator !== "frames" && spec.generator !== "vfx") {
12133
+ if ((0, import_node_fs11.existsSync)(import_node_path14.default.resolve(spec.root, spec.source))) {
11556
12134
  state = "ok";
11557
12135
  reason = `placed from ${spec.source}; not generated`;
11558
12136
  } else {
@@ -11560,7 +12138,7 @@ async function buildPlan(specs, lock, opts = {}) {
11560
12138
  reason = `declared source is missing: ${spec.source}`;
11561
12139
  }
11562
12140
  } else if (!entry) {
11563
- if ((0, import_node_fs10.existsSync)(spec.outFile)) {
12141
+ if ((0, import_node_fs11.existsSync)(spec.outFile)) {
11564
12142
  state = "untracked";
11565
12143
  reason = "file on disk, no provenance recorded";
11566
12144
  } else {
@@ -11573,7 +12151,7 @@ async function buildPlan(specs, lock, opts = {}) {
11573
12151
  } else if (spec.mirror && !mirrorCurrent(entry, lock.entries[lockKey(spec.styleId, spec.mirror.sourceAssetId)])) {
11574
12152
  state = "stale";
11575
12153
  reason = `mirror source ${spec.mirror.sourceAssetId} changed; flip it again (no generation cost)`;
11576
- } else if (spec.mirror && entry.status === "downloaded" && entry.outputs.some((_output, index) => !(0, import_node_fs10.existsSync)(currentEntryOutputPath(entry, spec, index)))) {
12154
+ } else if (spec.mirror && entry.status === "downloaded" && entry.outputs.some((_output, index) => !(0, import_node_fs11.existsSync)(currentEntryOutputPath(entry, spec, index)))) {
11577
12155
  state = "stale";
11578
12156
  reason = "mirror output missing on disk; flip it again (no generation cost)";
11579
12157
  } else if (spec.character?.kind === "outfit" && entry.outfit?.sourceSha256 !== mirrorSourceHash(lock.entries[lockKey(spec.styleId, spec.character.parentAssetId)] ?? { outputs: [] })) {
@@ -11601,9 +12179,9 @@ async function buildPlan(specs, lock, opts = {}) {
11601
12179
  } else if (entry.status !== "downloaded") {
11602
12180
  state = "in-flight";
11603
12181
  reason = `awaiting ${entry.status}`;
11604
- } else if (entry.outputs.length === 0 || entry.outputs.some((_output, index) => !(0, import_node_fs10.existsSync)(currentEntryOutputPath(entry, spec, index)))) {
12182
+ } else if (entry.outputs.length === 0 || entry.outputs.some((_output, index) => !(0, import_node_fs11.existsSync)(currentEntryOutputPath(entry, spec, index)))) {
11605
12183
  state = "orphaned";
11606
- 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(", ")}`;
12184
+ reason = entry.outputs.length === 0 ? "no recorded output files" : `missing on disk: ${entry.outputs.filter((_output, index) => !(0, import_node_fs11.existsSync)(currentEntryOutputPath(entry, spec, index))).map((o) => o.role ?? "asset").join(", ")}`;
11607
12185
  } else if (await anyOutputModified(entry, spec)) {
11608
12186
  state = "orphaned";
11609
12187
  reason = "output modified since download";
@@ -11694,13 +12272,13 @@ function summarize(plan) {
11694
12272
 
11695
12273
  // src/project.ts
11696
12274
  async function openProject(manifestPath = "pixelkiln.manifest.json", opts = {}) {
11697
- const absolute = import_node_path14.default.resolve(manifestPath);
11698
- const root = import_node_path14.default.dirname(absolute);
12275
+ const absolute = import_node_path15.default.resolve(manifestPath);
12276
+ const root = import_node_path15.default.dirname(absolute);
11699
12277
  if (opts.env !== false) {
11700
12278
  loadEnvFiles(root);
11701
- if (import_node_path14.default.resolve(process.cwd()) !== root) loadEnvFiles(process.cwd());
12279
+ if (import_node_path15.default.resolve(process.cwd()) !== root) loadEnvFiles(process.cwd());
11702
12280
  }
11703
- const lockPath = import_node_path14.default.resolve(opts.lockPath ?? import_node_path14.default.join(root, "pixelkiln.lock.json"));
12281
+ const lockPath = import_node_path15.default.resolve(opts.lockPath ?? import_node_path15.default.join(root, "pixelkiln.lock.json"));
11704
12282
  const loaded = await loadManifest(absolute);
11705
12283
  const specs = await resolveSpecs(loaded, { styles: opts.styles ?? [], assets: opts.assets ?? [] });
11706
12284
  const lock = await loadLock(lockPath);
@@ -11767,9 +12345,9 @@ function shouldPersistSourceUrl(value) {
11767
12345
  }
11768
12346
 
11769
12347
  // src/recipes.ts
11770
- var import_node_path15 = __toESM(require("path"), 1);
11771
- var import_node_fs11 = require("fs");
11772
- var import_promises7 = require("fs/promises");
12348
+ var import_node_path16 = __toESM(require("path"), 1);
12349
+ var import_node_fs12 = require("fs");
12350
+ var import_promises8 = require("fs/promises");
11773
12351
  var import_node_url = require("url");
11774
12352
  var import_zod4 = require("zod");
11775
12353
  var SHA256_RE = /^[0-9a-f]{64}$/;
@@ -11778,7 +12356,7 @@ var VERSION_RE = /^\d+\.\d+\.\d+$/;
11778
12356
  var RECIPE_FILE = "pixelkiln.recipe.json";
11779
12357
  var RECIPE_DIR_TOKEN = "{{recipeDir}}";
11780
12358
  var PortablePathSchema = import_zod4.z.string().min(1).refine((value) => {
11781
- if (value.includes("\\") || import_node_path15.default.posix.isAbsolute(value)) return false;
12359
+ if (value.includes("\\") || import_node_path16.default.posix.isAbsolute(value)) return false;
11782
12360
  const parts = value.split("/");
11783
12361
  return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
11784
12362
  }, "expected a portable relative path without '.', '..', or backslashes");
@@ -11954,10 +12532,10 @@ function message3(error) {
11954
12532
  return error instanceof Error ? error.message : String(error);
11955
12533
  }
11956
12534
  async function readRecipeFile(recipePath, bundled) {
11957
- const absolute = import_node_path15.default.resolve(recipePath);
12535
+ const absolute = import_node_path16.default.resolve(recipePath);
11958
12536
  let raw;
11959
12537
  try {
11960
- raw = JSON.parse(await (0, import_promises7.readFile)(absolute, "utf8"));
12538
+ raw = JSON.parse(await (0, import_promises8.readFile)(absolute, "utf8"));
11961
12539
  } catch (error) {
11962
12540
  throw new Error(`Could not read recipe ${absolute}: ${message3(error)}`, { cause: error });
11963
12541
  }
@@ -11968,13 +12546,13 @@ async function readRecipeFile(recipePath, bundled) {
11968
12546
  ` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
11969
12547
  );
11970
12548
  }
11971
- return { recipe: parsed.data, authored: raw, path: absolute, dir: import_node_path15.default.dirname(absolute), bundled };
12549
+ return { recipe: parsed.data, authored: raw, path: absolute, dir: import_node_path16.default.dirname(absolute), bundled };
11972
12550
  }
11973
12551
  async function walkRecipeFiles(root) {
11974
- if (!(0, import_node_fs11.existsSync)(root)) return [];
12552
+ if (!(0, import_node_fs12.existsSync)(root)) return [];
11975
12553
  const out = [];
11976
- for (const entry of await (0, import_promises7.readdir)(root, { withFileTypes: true })) {
11977
- const resolved = import_node_path15.default.join(root, entry.name);
12554
+ for (const entry of await (0, import_promises8.readdir)(root, { withFileTypes: true })) {
12555
+ const resolved = import_node_path16.default.join(root, entry.name);
11978
12556
  if (entry.isDirectory()) out.push(...await walkRecipeFiles(resolved));
11979
12557
  else if (entry.isFile() && entry.name === RECIPE_FILE) out.push(resolved);
11980
12558
  }
@@ -11985,7 +12563,7 @@ function bundledRecipeRoot() {
11985
12563
  }
11986
12564
  async function listBundledRecipes(root = bundledRecipeRoot()) {
11987
12565
  const recipes = await Promise.all(
11988
- (await walkRecipeFiles(import_node_path15.default.resolve(root))).map((file) => readRecipeFile(file, true))
12566
+ (await walkRecipeFiles(import_node_path16.default.resolve(root))).map((file) => readRecipeFile(file, true))
11989
12567
  );
11990
12568
  return recipes.sort(
11991
12569
  (a, b) => a.recipe.id.localeCompare(b.recipe.id) || compareVersions(b.recipe.version, a.recipe.version)
@@ -12008,9 +12586,9 @@ function parseSelector(selector) {
12008
12586
  return { id: match[1], version: match[2] };
12009
12587
  }
12010
12588
  async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
12011
- const local = import_node_path15.default.resolve(target);
12012
- if ((0, import_node_fs11.existsSync)(local)) {
12013
- const recipePath = import_node_path15.default.basename(local) === RECIPE_FILE ? local : import_node_path15.default.join(local, RECIPE_FILE);
12589
+ const local = import_node_path16.default.resolve(target);
12590
+ if ((0, import_node_fs12.existsSync)(local)) {
12591
+ const recipePath = import_node_path16.default.basename(local) === RECIPE_FILE ? local : import_node_path16.default.join(local, RECIPE_FILE);
12014
12592
  return readRecipeFile(recipePath, false);
12015
12593
  }
12016
12594
  if (target.startsWith(".") || target.startsWith("/") || target.includes("\\")) {
@@ -12024,8 +12602,8 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
12024
12602
  return matches[0];
12025
12603
  }
12026
12604
  async function verifyFile(root, file) {
12027
- const absolute = import_node_path15.default.resolve(root, ...file.path.split("/"));
12028
- if (!(0, import_node_fs11.existsSync)(absolute)) {
12605
+ const absolute = import_node_path16.default.resolve(root, ...file.path.split("/"));
12606
+ if (!(0, import_node_fs12.existsSync)(absolute)) {
12029
12607
  return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
12030
12608
  }
12031
12609
  const actualSha256 = await sha256File(absolute);
@@ -12040,7 +12618,7 @@ async function verifyRecipe(target, options = {}) {
12040
12618
  const loaded = await resolveRecipe(target, options.bundledRoot);
12041
12619
  const actualIntegrity = recipeDigest(loaded.authored);
12042
12620
  const files = await Promise.all(loaded.recipe.files.map((file) => verifyFile(loaded.dir, file)));
12043
- const modelRoot = options.modelRoot ? import_node_path15.default.resolve(options.modelRoot) : null;
12621
+ const modelRoot = options.modelRoot ? import_node_path16.default.resolve(options.modelRoot) : null;
12044
12622
  const models = modelRoot ? await Promise.all(loaded.recipe.models.map(async (model) => ({
12045
12623
  ...await verifyFile(modelRoot, model),
12046
12624
  source: model.source,
@@ -12074,7 +12652,7 @@ async function verifyRecipe(target, options = {}) {
12074
12652
  };
12075
12653
  }
12076
12654
  function renderStyle(style, destination, cwd) {
12077
- const portableDir = import_node_path15.default.relative(cwd, destination).split(import_node_path15.default.sep).join("/") || ".";
12655
+ const portableDir = import_node_path16.default.relative(cwd, destination).split(import_node_path16.default.sep).join("/") || ".";
12078
12656
  const replace = (value) => {
12079
12657
  if (typeof value === "string") return value.replaceAll(RECIPE_DIR_TOKEN, portableDir);
12080
12658
  if (Array.isArray(value)) return value.map(replace);
@@ -12086,25 +12664,25 @@ function renderStyle(style, destination, cwd) {
12086
12664
  return replace(style);
12087
12665
  }
12088
12666
  async function installRecipe(target, options = {}) {
12089
- const cwd = import_node_path15.default.resolve(options.cwd ?? process.cwd());
12667
+ const cwd = import_node_path16.default.resolve(options.cwd ?? process.cwd());
12090
12668
  const loaded = await resolveRecipe(target, options.bundledRoot);
12091
12669
  const verification = await verifyRecipe(loaded.path);
12092
12670
  if (!verification.ok) {
12093
12671
  throw new Error(`Recipe ${loaded.recipe.id}@${loaded.recipe.version} failed integrity verification.`);
12094
12672
  }
12095
- 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);
12673
+ const destination = options.out ? import_node_path16.default.resolve(cwd, options.out) : import_node_path16.default.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
12096
12674
  const sources = [
12097
12675
  { path: RECIPE_FILE, source: loaded.path },
12098
- ...loaded.recipe.files.map((file) => ({ path: file.path, source: import_node_path15.default.join(loaded.dir, ...file.path.split("/")) }))
12676
+ ...loaded.recipe.files.map((file) => ({ path: file.path, source: import_node_path16.default.join(loaded.dir, ...file.path.split("/")) }))
12099
12677
  ];
12100
12678
  const files = await Promise.all(sources.map(async (file) => ({
12101
- path: import_node_path15.default.join(destination, ...file.path.split("/")),
12102
- data: await (0, import_promises7.readFile)(file.source)
12679
+ path: import_node_path16.default.join(destination, ...file.path.split("/")),
12680
+ data: await (0, import_promises8.readFile)(file.source)
12103
12681
  })));
12104
12682
  if (!options.force) {
12105
12683
  for (const file of files) {
12106
- if (!(0, import_node_fs11.existsSync)(file.path)) continue;
12107
- const current = await (0, import_promises7.readFile)(file.path);
12684
+ if (!(0, import_node_fs12.existsSync)(file.path)) continue;
12685
+ const current = await (0, import_promises8.readFile)(file.path);
12108
12686
  if (!current.equals(file.data)) {
12109
12687
  throw new OverwriteRefusedError(
12110
12688
  `Recipe destination has local changes: ${file.path}. Choose another --out or pass --force to replace declared recipe files.`,
@@ -12128,9 +12706,9 @@ async function installRecipe(target, options = {}) {
12128
12706
  }
12129
12707
 
12130
12708
  // src/pipeline/audit.ts
12131
- var import_promises8 = require("fs/promises");
12132
- var import_node_fs12 = require("fs");
12133
- var import_node_path16 = __toESM(require("path"), 1);
12709
+ var import_promises9 = require("fs/promises");
12710
+ var import_node_fs13 = require("fs");
12711
+ var import_node_path17 = __toESM(require("path"), 1);
12134
12712
  function paletteDistance(asset, reference) {
12135
12713
  if (!asset.length || !reference.length) return 0;
12136
12714
  let total = 0;
@@ -12166,12 +12744,12 @@ async function auditStyle(loaded, specs, styleId, lock) {
12166
12744
  const unreadable = [];
12167
12745
  for (const spec of mine) {
12168
12746
  for (const output of resolveSpecOutputs(spec, lock, loaded.root)) {
12169
- if (!(0, import_node_fs12.existsSync)(output.absolutePath)) {
12747
+ if (!(0, import_node_fs13.existsSync)(output.absolutePath)) {
12170
12748
  missing.push(output.id);
12171
12749
  continue;
12172
12750
  }
12173
12751
  try {
12174
- const png = decodePng(await (0, import_promises8.readFile)(output.absolutePath));
12752
+ const png = decodePng(await (0, import_promises9.readFile)(output.absolutePath));
12175
12753
  const palette = extractPalette(png, 12);
12176
12754
  assets.push({
12177
12755
  assetId: spec.assetId,
@@ -12195,10 +12773,10 @@ async function auditStyle(loaded, specs, styleId, lock) {
12195
12773
  let referenceFromStyleImages = false;
12196
12774
  const refPalettes = [];
12197
12775
  for (const img of style.styleImages) {
12198
- const abs = import_node_path16.default.resolve(loaded.root, img.path);
12199
- if (!(0, import_node_fs12.existsSync)(abs)) continue;
12776
+ const abs = import_node_path17.default.resolve(loaded.root, img.path);
12777
+ if (!(0, import_node_fs13.existsSync)(abs)) continue;
12200
12778
  try {
12201
- refPalettes.push(extractPalette(decodePng(await (0, import_promises8.readFile)(abs)), 12));
12779
+ refPalettes.push(extractPalette(decodePng(await (0, import_promises9.readFile)(abs)), 12));
12202
12780
  } catch {
12203
12781
  }
12204
12782
  }
@@ -12274,20 +12852,20 @@ function hex(c) {
12274
12852
  }
12275
12853
 
12276
12854
  // src/pixellab-utils.ts
12277
- var import_node_fs13 = require("fs");
12278
- var import_promises9 = require("fs/promises");
12279
- var import_node_path17 = __toESM(require("path"), 1);
12855
+ var import_node_fs14 = require("fs");
12856
+ var import_promises10 = require("fs/promises");
12857
+ var import_node_path18 = __toESM(require("path"), 1);
12280
12858
  var UNZOOM_MIN_SIDE = 256;
12281
12859
  var UNZOOM_MAX_AREA = 2048 * 2048;
12282
12860
  function defaultUnzoomOut(input) {
12283
- const ext = import_node_path17.default.extname(input);
12861
+ const ext = import_node_path18.default.extname(input);
12284
12862
  return `${ext ? input.slice(0, -ext.length) : input}.unzoomed.png`;
12285
12863
  }
12286
12864
  async function unzoomFile(client2, input, opts = {}) {
12287
- const out = import_node_path17.default.resolve(opts.out ?? defaultUnzoomOut(input));
12288
- if (import_node_path17.default.resolve(input) === out) throw new Error("unzoom will not overwrite its own input; pass a different --out");
12289
- if ((0, import_node_fs13.existsSync)(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
12290
- const bytes = await (0, import_promises9.readFile)(input);
12865
+ const out = import_node_path18.default.resolve(opts.out ?? defaultUnzoomOut(input));
12866
+ if (import_node_path18.default.resolve(input) === out) throw new Error("unzoom will not overwrite its own input; pass a different --out");
12867
+ if ((0, import_node_fs14.existsSync)(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
12868
+ const bytes = await (0, import_promises10.readFile)(input);
12291
12869
  const metadata = imageMetadata(bytes);
12292
12870
  if (!metadata) throw new Error(`${input} is not a readable PNG or JPEG`);
12293
12871
  const { width, height, format } = metadata;
@@ -12300,22 +12878,22 @@ async function unzoomFile(client2, input, opts = {}) {
12300
12878
  throw new Error(`${input} is ${width}x${height}; PixelLab's unzoom takes at most 2048x2048 worth of pixels`);
12301
12879
  }
12302
12880
  const res = await client2.unzoom({ image: { base64: bytes.toString("base64"), format }, quantize: opts.quantize });
12303
- await (0, import_promises9.mkdir)(import_node_path17.default.dirname(out), { recursive: true });
12304
- await (0, import_promises9.writeFile)(out, res.png);
12881
+ await (0, import_promises10.mkdir)(import_node_path18.default.dirname(out), { recursive: true });
12882
+ await (0, import_promises10.writeFile)(out, res.png);
12305
12883
  return { out, original: res.originalSize, unzoomed: res.unzoomedSize, zoomFactor: res.zoomFactor, usage: res.usage };
12306
12884
  }
12307
12885
  var FONT_WEIGHTS = ["Bold", "Regular"];
12308
12886
  var FONT_GLYPH_SIZES = [8, 16, 32, 64];
12309
12887
  var FONT_COST_GENERATIONS = 15;
12310
12888
  function fontOutputPaths(out) {
12311
- const base = import_node_path17.default.resolve(out.replace(/\.(ttf|png)$/i, ""));
12889
+ const base = import_node_path18.default.resolve(out.replace(/\.(ttf|png)$/i, ""));
12312
12890
  return { ttf: `${base}.ttf`, atlas: `${base}.png` };
12313
12891
  }
12314
12892
  async function generateFont(client2, opts) {
12315
12893
  const { ttf, atlas } = fontOutputPaths(opts.out);
12316
12894
  if (!opts.force) {
12317
12895
  for (const file of [ttf, atlas]) {
12318
- if ((0, import_node_fs13.existsSync)(file)) throw new Error(`${file} already exists; pass --force to replace it`);
12896
+ if ((0, import_node_fs14.existsSync)(file)) throw new Error(`${file} already exists; pass --force to replace it`);
12319
12897
  }
12320
12898
  }
12321
12899
  const sleep3 = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
@@ -12342,9 +12920,9 @@ async function generateFont(client2, opts) {
12342
12920
  client2.download(job.download_ttf_url),
12343
12921
  client2.download(job.download_atlas_url)
12344
12922
  ]);
12345
- await (0, import_promises9.mkdir)(import_node_path17.default.dirname(ttf), { recursive: true });
12346
- await (0, import_promises9.writeFile)(ttf, ttfBytes);
12347
- await (0, import_promises9.writeFile)(atlas, atlasBytes);
12923
+ await (0, import_promises10.mkdir)(import_node_path18.default.dirname(ttf), { recursive: true });
12924
+ await (0, import_promises10.writeFile)(ttf, ttfBytes);
12925
+ await (0, import_promises10.writeFile)(atlas, atlasBytes);
12348
12926
  return { jobId, ttf, atlas, glyphPx: job.glyph_px ?? null, usage: job.usage };
12349
12927
  }
12350
12928
  if (Date.now() - started > timeout) {
@@ -12358,14 +12936,14 @@ async function generateFont(client2, opts) {
12358
12936
  var PIXELATE_COST_GENERATIONS = 6;
12359
12937
  var PIXELATE_MAX_SIDE = 2048;
12360
12938
  function defaultPixelateOut(input) {
12361
- const ext = import_node_path17.default.extname(input);
12939
+ const ext = import_node_path18.default.extname(input);
12362
12940
  return `${ext ? input.slice(0, -ext.length) : input}.pixel.png`;
12363
12941
  }
12364
12942
  async function pixelateFile(client2, input, opts = {}) {
12365
- const out = import_node_path17.default.resolve(opts.out ?? defaultPixelateOut(input));
12366
- if (import_node_path17.default.resolve(input) === out) throw new Error("pixelate will not overwrite its own input; pass a different --out");
12367
- if ((0, import_node_fs13.existsSync)(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
12368
- const bytes = await (0, import_promises9.readFile)(input);
12943
+ const out = import_node_path18.default.resolve(opts.out ?? defaultPixelateOut(input));
12944
+ if (import_node_path18.default.resolve(input) === out) throw new Error("pixelate will not overwrite its own input; pass a different --out");
12945
+ if ((0, import_node_fs14.existsSync)(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
12946
+ const bytes = await (0, import_promises10.readFile)(input);
12369
12947
  const metadata = imageMetadata(bytes);
12370
12948
  if (!metadata) throw new Error(`${input} is not a readable PNG or JPEG`);
12371
12949
  const { width, height, format } = metadata;
@@ -12391,8 +12969,8 @@ async function pixelateFile(client2, input, opts = {}) {
12391
12969
  }
12392
12970
  const png = Buffer.from(first.base64, "base64");
12393
12971
  const size = imageMetadata(png);
12394
- await (0, import_promises9.mkdir)(import_node_path17.default.dirname(out), { recursive: true });
12395
- await (0, import_promises9.writeFile)(out, png);
12972
+ await (0, import_promises10.mkdir)(import_node_path18.default.dirname(out), { recursive: true });
12973
+ await (0, import_promises10.writeFile)(out, png);
12396
12974
  return {
12397
12975
  jobId,
12398
12976
  out,
@@ -12412,15 +12990,15 @@ async function pixelateFile(client2, input, opts = {}) {
12412
12990
  }
12413
12991
 
12414
12992
  // src/pipeline/quality-regression.ts
12415
- var import_node_path18 = __toESM(require("path"), 1);
12416
- var import_node_fs14 = require("fs");
12417
- var import_promises10 = require("fs/promises");
12993
+ var import_node_path19 = __toESM(require("path"), 1);
12994
+ var import_node_fs15 = require("fs");
12995
+ var import_promises11 = require("fs/promises");
12418
12996
  var import_zod5 = require("zod");
12419
12997
  var SHA256_RE2 = /^[0-9a-f]{64}$/;
12420
12998
  var HEX_RE = /^#[0-9a-f]{6}$/;
12421
12999
  var MAX_REDMEAN_DISTANCE = 765;
12422
13000
  var QualityPathSchema = import_zod5.z.string().min(1).refine(
12423
- (value) => !import_node_path18.default.posix.isAbsolute(value) && !value.includes("\\"),
13001
+ (value) => !import_node_path19.default.posix.isAbsolute(value) && !value.includes("\\"),
12424
13002
  "expected a portable relative path"
12425
13003
  );
12426
13004
  var DEFAULT_QUALITY_TOLERANCES = {
@@ -12567,10 +13145,10 @@ function measureDecodedImageQuality(png, digest2) {
12567
13145
  };
12568
13146
  }
12569
13147
  async function measureImageQuality(file) {
12570
- const absolute = import_node_path18.default.resolve(file);
13148
+ const absolute = import_node_path19.default.resolve(file);
12571
13149
  let bytes;
12572
13150
  try {
12573
- bytes = await (0, import_promises10.readFile)(absolute);
13151
+ bytes = await (0, import_promises11.readFile)(absolute);
12574
13152
  } catch (error) {
12575
13153
  throw new Error(`Cannot read quality image ${absolute}: ${message4(error)}`, { cause: error });
12576
13154
  }
@@ -12589,11 +13167,11 @@ function resolveQualityInputs(raw, inputsFilePath) {
12589
13167
  }
12590
13168
  const ids = parsed.data.map((entry) => entry.id);
12591
13169
  if (new Set(ids).size !== ids.length) throw new Error("--inputs contains a duplicate quality case id.");
12592
- const root = import_node_path18.default.dirname(import_node_path18.default.resolve(inputsFilePath));
13170
+ const root = import_node_path19.default.dirname(import_node_path19.default.resolve(inputsFilePath));
12593
13171
  return parsed.data.map((entry) => ({
12594
13172
  id: entry.id,
12595
- file: import_node_path18.default.resolve(root, entry.path),
12596
- ...entry.record ? { record: import_node_path18.default.resolve(root, entry.record) } : {},
13173
+ file: import_node_path19.default.resolve(root, entry.path),
13174
+ ...entry.record ? { record: import_node_path19.default.resolve(root, entry.record) } : {},
12597
13175
  tolerances: QualityToleranceSchema.parse({
12598
13176
  ...DEFAULT_QUALITY_TOLERANCES,
12599
13177
  ...entry.tolerances
@@ -12601,7 +13179,7 @@ function resolveQualityInputs(raw, inputsFilePath) {
12601
13179
  }));
12602
13180
  }
12603
13181
  function portableRelative2(from, to) {
12604
- return import_node_path18.default.relative(from, import_node_path18.default.resolve(to)).split(import_node_path18.default.sep).join("/") || ".";
13182
+ return import_node_path19.default.relative(from, import_node_path19.default.resolve(to)).split(import_node_path19.default.sep).join("/") || ".";
12605
13183
  }
12606
13184
  async function verifyRecordForImage(recordPath, imagePath) {
12607
13185
  const verification = await verifyArtifactBundle(recordPath);
@@ -12616,20 +13194,20 @@ async function verifyRecordForImage(recordPath, imagePath) {
12616
13194
  if (record.kind !== "refine" || options === null || typeof options !== "object" || options.schema !== "pixelkiln-quality") {
12617
13195
  throw new Error(`Quality record ${recordPath} is not a PixelKiln refinement record.`);
12618
13196
  }
12619
- const outputs = record.outputs.map((output) => import_node_path18.default.resolve(import_node_path18.default.dirname(recordPath), output.path));
12620
- if (!outputs.includes(import_node_path18.default.resolve(imagePath))) {
13197
+ const outputs = record.outputs.map((output) => import_node_path19.default.resolve(import_node_path19.default.dirname(recordPath), output.path));
13198
+ if (!outputs.includes(import_node_path19.default.resolve(imagePath))) {
12621
13199
  throw new Error(`Quality record ${recordPath} does not own ${imagePath}.`);
12622
13200
  }
12623
13201
  }
12624
13202
  async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
12625
13203
  if (!inputs.length) throw new Error("Cannot snapshot an empty quality baseline.");
12626
- const absolute = import_node_path18.default.resolve(baselinePath);
13204
+ const absolute = import_node_path19.default.resolve(baselinePath);
12627
13205
  for (const input of inputs) {
12628
- if (import_node_path18.default.resolve(input.file) === absolute || input.record && import_node_path18.default.resolve(input.record) === absolute) {
13206
+ if (import_node_path19.default.resolve(input.file) === absolute || input.record && import_node_path19.default.resolve(input.record) === absolute) {
12629
13207
  throw new Error(`Quality baseline output would overwrite an input: ${absolute}.`);
12630
13208
  }
12631
13209
  }
12632
- const root = import_node_path18.default.dirname(absolute);
13210
+ const root = import_node_path19.default.dirname(absolute);
12633
13211
  const cases = [];
12634
13212
  for (const input of [...inputs].sort((a, b) => a.id.localeCompare(b.id))) {
12635
13213
  const expected = await measureImageQuality(input.file);
@@ -12656,8 +13234,8 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
12656
13234
  cases
12657
13235
  });
12658
13236
  const data = Buffer.from(JSON.stringify(baseline, null, 2) + "\n");
12659
- if ((0, import_node_fs14.existsSync)(absolute) && !options.force) {
12660
- const current = await (0, import_promises10.readFile)(absolute);
13237
+ if ((0, import_node_fs15.existsSync)(absolute) && !options.force) {
13238
+ const current = await (0, import_promises11.readFile)(absolute);
12661
13239
  if (!current.equals(data)) {
12662
13240
  throw new OverwriteRefusedError(
12663
13241
  `Quality baseline already exists with different content: ${absolute}. Pass --force to replace it.`,
@@ -12669,10 +13247,10 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
12669
13247
  return { baseline, path: absolute, changed: result.changed.length > 0 };
12670
13248
  }
12671
13249
  async function readQualityBaseline(baselinePath) {
12672
- const absolute = import_node_path18.default.resolve(baselinePath);
13250
+ const absolute = import_node_path19.default.resolve(baselinePath);
12673
13251
  let raw;
12674
13252
  try {
12675
- raw = JSON.parse(await (0, import_promises10.readFile)(absolute, "utf8"));
13253
+ raw = JSON.parse(await (0, import_promises11.readFile)(absolute, "utf8"));
12676
13254
  } catch (error) {
12677
13255
  throw new Error(`Could not read quality baseline ${absolute}: ${message4(error)}`, { cause: error });
12678
13256
  }
@@ -12742,12 +13320,12 @@ function regressionViolations(expected, actual, tolerances) {
12742
13320
  return { violations, warnings };
12743
13321
  }
12744
13322
  async function checkQualityBaseline(baselinePath) {
12745
- const absolute = import_node_path18.default.resolve(baselinePath);
13323
+ const absolute = import_node_path19.default.resolve(baselinePath);
12746
13324
  const baseline = await readQualityBaseline(absolute);
12747
- const root = import_node_path18.default.dirname(absolute);
13325
+ const root = import_node_path19.default.dirname(absolute);
12748
13326
  const cases = [];
12749
13327
  for (const entry of baseline.cases) {
12750
- const file = import_node_path18.default.resolve(root, entry.file);
13328
+ const file = import_node_path19.default.resolve(root, entry.file);
12751
13329
  let actual = null;
12752
13330
  const violations = [];
12753
13331
  const warnings = [];
@@ -12760,7 +13338,7 @@ async function checkQualityBaseline(baselinePath) {
12760
13338
  violations.push(message4(error));
12761
13339
  }
12762
13340
  if (entry.record) {
12763
- const recordPath = import_node_path18.default.resolve(root, entry.record.path);
13341
+ const recordPath = import_node_path19.default.resolve(root, entry.record.path);
12764
13342
  try {
12765
13343
  const recordHash = await sha256File(recordPath);
12766
13344
  if (recordHash !== entry.record.sha256) violations.push("quality record hash changed");
@@ -12796,14 +13374,14 @@ async function checkQualityBaseline(baselinePath) {
12796
13374
  }
12797
13375
 
12798
13376
  // src/pipeline/cache-health.ts
12799
- var import_node_fs18 = require("fs");
12800
- var import_promises13 = require("fs/promises");
12801
- var import_node_path22 = __toESM(require("path"), 1);
13377
+ var import_node_fs19 = require("fs");
13378
+ var import_promises14 = require("fs/promises");
13379
+ var import_node_path23 = __toESM(require("path"), 1);
12802
13380
 
12803
13381
  // src/cache.ts
12804
- var import_promises11 = require("fs/promises");
12805
- var import_node_fs15 = require("fs");
12806
- var import_node_path19 = __toESM(require("path"), 1);
13382
+ var import_promises12 = require("fs/promises");
13383
+ var import_node_fs16 = require("fs");
13384
+ var import_node_path20 = __toESM(require("path"), 1);
12807
13385
  var import_zod6 = require("zod");
12808
13386
  var HashCacheSchema = import_zod6.z.object({
12809
13387
  version: import_zod6.z.literal(1),
@@ -12817,10 +13395,10 @@ function isSha256Hash(value) {
12817
13395
  function parseCache(value) {
12818
13396
  return HashCacheSchema.parse(value);
12819
13397
  }
12820
- async function loadCache(path38) {
12821
- if (!(0, import_node_fs15.existsSync)(path38)) return { version: 1, hashes: {} };
13398
+ async function loadCache(path39) {
13399
+ if (!(0, import_node_fs16.existsSync)(path39)) return { version: 1, hashes: {} };
12822
13400
  try {
12823
- const parsed = HashCacheSchema.safeParse(JSON.parse(await (0, import_promises11.readFile)(path38, "utf8")));
13401
+ const parsed = HashCacheSchema.safeParse(JSON.parse(await (0, import_promises12.readFile)(path39, "utf8")));
12824
13402
  if (!parsed.success) return { version: 1, hashes: {} };
12825
13403
  return {
12826
13404
  version: 1,
@@ -12832,20 +13410,20 @@ async function loadCache(path38) {
12832
13410
  return { version: 1, hashes: {} };
12833
13411
  }
12834
13412
  }
12835
- async function saveCache(path38, cache) {
13413
+ async function saveCache(path39, cache) {
12836
13414
  const sorted = {};
12837
13415
  for (const key of Object.keys(cache.hashes).sort()) {
12838
13416
  const hash = cache.hashes[key];
12839
13417
  if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
12840
13418
  sorted[key] = hash;
12841
13419
  }
12842
- await (0, import_promises11.mkdir)(import_node_path19.default.dirname(import_node_path19.default.resolve(path38)), { recursive: true });
12843
- const tmp = `${path38}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
13420
+ await (0, import_promises12.mkdir)(import_node_path20.default.dirname(import_node_path20.default.resolve(path39)), { recursive: true });
13421
+ const tmp = `${path39}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
12844
13422
  try {
12845
- await (0, import_promises11.writeFile)(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
12846
- await (0, import_promises11.rename)(tmp, path38);
13423
+ await (0, import_promises12.writeFile)(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
13424
+ await (0, import_promises12.rename)(tmp, path39);
12847
13425
  } finally {
12848
- await (0, import_promises11.rm)(tmp, { force: true });
13426
+ await (0, import_promises12.rm)(tmp, { force: true });
12849
13427
  }
12850
13428
  }
12851
13429
  function pruneCache(cache, liveIds, namespace = "") {
@@ -12867,20 +13445,20 @@ function cachePathFor(lockPath) {
12867
13445
  }
12868
13446
 
12869
13447
  // src/pipeline/history.ts
12870
- var import_node_fs17 = require("fs");
12871
- var import_node_path21 = __toESM(require("path"), 1);
13448
+ var import_node_fs18 = require("fs");
13449
+ var import_node_path22 = __toESM(require("path"), 1);
12872
13450
 
12873
13451
  // src/pipeline/fetch.ts
12874
- var import_node_fs16 = require("fs");
12875
- var import_promises12 = require("fs/promises");
12876
- var import_node_path20 = __toESM(require("path"), 1);
13452
+ var import_node_fs17 = require("fs");
13453
+ var import_promises13 = require("fs/promises");
13454
+ var import_node_path21 = __toESM(require("path"), 1);
12877
13455
  async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12878
13456
  const log = opts.onProgress ?? (() => {
12879
13457
  });
12880
13458
  const result = { downloaded: 0, skipped: 0, failed: 0, ...opts.refresh ? { unchanged: 0 } : {} };
12881
13459
  const specByKey = new Map(specs.map((s) => [lockKey(s.styleId, s.assetId), s]));
12882
13460
  normalizeLockOutputPaths(lock, specs);
12883
- 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"));
13461
+ const cacheDir = opts.cacheDir === false ? null : import_node_path21.default.resolve(opts.cacheDir ?? import_node_path21.default.join(import_node_path21.default.dirname(lockPath), ".pixelkiln", "cache"));
12884
13462
  const pending = Object.entries(lock.entries).filter(([key, e]) => {
12885
13463
  if (e.provider !== provider.id || !specByKey.has(key)) return false;
12886
13464
  if (specByKey.get(key)?.mirror) return false;
@@ -12891,15 +13469,15 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12891
13469
  if (spec && e.specHash === spec.specHash && e.outputs.length && !postprocessCurrent(e, spec)) {
12892
13470
  return Boolean(opts.force) || e.outputs.every((output) => {
12893
13471
  const file = resolveOutputPath(output.path, spec.root);
12894
- return (0, import_node_fs16.existsSync)(file) && sha256((0, import_node_fs16.readFileSync)(file)) === output.sha256;
13472
+ return (0, import_node_fs17.existsSync)(file) && sha256((0, import_node_fs17.readFileSync)(file)) === output.sha256;
12895
13473
  });
12896
13474
  }
12897
13475
  if (!opts.repair) return false;
12898
13476
  if (e.outputs.length === 0 || !spec) return true;
12899
13477
  return e.outputs.some((output) => {
12900
13478
  const file = resolveOutputPath(output.path, spec.root);
12901
- if (!(0, import_node_fs16.existsSync)(file)) return true;
12902
- const current = sha256((0, import_node_fs16.readFileSync)(file));
13479
+ if (!(0, import_node_fs17.existsSync)(file)) return true;
13480
+ const current = sha256((0, import_node_fs17.readFileSync)(file));
12903
13481
  if (current === output.sha256) return false;
12904
13482
  return Boolean(opts.force) || (e.supersededOutputs ?? []).some((old) => old.sha256 === current);
12905
13483
  });
@@ -12982,21 +13560,21 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12982
13560
  if (!source.url) throw new Error(`no durable source URL to refresh ${source.role ?? "asset"} from`);
12983
13561
  fresh = await provider.download(source.url);
12984
13562
  if (recorded && sha256(fresh) === recordedRaw && !reapply) {
12985
- log(` current ${import_node_path20.default.relative(process.cwd(), target)}`);
13563
+ log(` current ${import_node_path21.default.relative(process.cwd(), target)}`);
12986
13564
  outputs.push({ ...recorded, path: portableOutputPath(target, spec.root) });
12987
13565
  continue;
12988
13566
  }
12989
13567
  }
12990
13568
  let raw = null;
12991
13569
  let final = null;
12992
- if ((0, import_node_fs16.existsSync)(target)) {
13570
+ if ((0, import_node_fs17.existsSync)(target)) {
12993
13571
  const currentHash = await sha256File(target);
12994
13572
  if (recorded && currentHash === recorded.sha256) {
12995
13573
  if (!fresh && !reapply) {
12996
13574
  if (cacheDir) {
12997
13575
  await cacheMedia(
12998
13576
  cacheDir,
12999
- await (0, import_promises12.readFile)(target),
13577
+ await (0, import_promises13.readFile)(target),
13000
13578
  recorded.mediaType ?? MediaType.PNG,
13001
13579
  recorded.sha256
13002
13580
  );
@@ -13005,10 +13583,10 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
13005
13583
  continue;
13006
13584
  }
13007
13585
  if (reapply && !fresh) {
13008
- if (!recorded.raw) raw = await (0, import_promises12.readFile)(target);
13009
- log(` reapply ${import_node_path20.default.relative(process.cwd(), target)}`);
13586
+ if (!recorded.raw) raw = await (0, import_promises13.readFile)(target);
13587
+ log(` reapply ${import_node_path21.default.relative(process.cwd(), target)}`);
13010
13588
  } else {
13011
- log(` changed ${import_node_path20.default.relative(process.cwd(), target)} (upstream)`);
13589
+ log(` changed ${import_node_path21.default.relative(process.cwd(), target)} (upstream)`);
13012
13590
  }
13013
13591
  } else {
13014
13592
  const superseded = (entry.supersededOutputs ?? []).find(
@@ -13029,14 +13607,14 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
13029
13607
  }
13030
13608
  if (outgoing && cacheDir) {
13031
13609
  try {
13032
- const oldBytes = await (0, import_promises12.readFile)(target);
13610
+ const oldBytes = await (0, import_promises13.readFile)(target);
13033
13611
  const oldMediaType = detectMediaType(oldBytes);
13034
13612
  if (oldMediaType) await cacheMedia(cacheDir, oldBytes, oldMediaType, currentHash);
13035
13613
  } catch {
13036
13614
  }
13037
13615
  }
13038
13616
  log(
13039
- ` replace ${import_node_path20.default.relative(process.cwd(), target)}` + (opts.force ? " (--force)" : " (previous tracked generation)")
13617
+ ` replace ${import_node_path21.default.relative(process.cwd(), target)}` + (opts.force ? " (--force)" : " (previous tracked generation)")
13040
13618
  );
13041
13619
  }
13042
13620
  }
@@ -13045,7 +13623,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
13045
13623
  } else if (!raw && recorded && cacheDir) {
13046
13624
  if (!reapply) final = await readCachedMedia(cacheDir, recorded.sha256, expectedMediaType);
13047
13625
  if (!final && recordedRaw) raw = await readCachedMedia(cacheDir, recordedRaw, expectedMediaType);
13048
- if (final || raw) log(` cached ${import_node_path20.default.relative(process.cwd(), target)}`);
13626
+ if (final || raw) log(` cached ${import_node_path21.default.relative(process.cwd(), target)}`);
13049
13627
  }
13050
13628
  if (!final && !raw) {
13051
13629
  if (!source.url) {
@@ -13072,13 +13650,13 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
13072
13650
  buf = processed.bytes;
13073
13651
  if (processed.changed) {
13074
13652
  rawHash = sha256(raw);
13075
- log(` snapped ${import_node_path20.default.relative(process.cwd(), target)} to ${wanted.palette.colors.length} colours`);
13653
+ log(` snapped ${import_node_path21.default.relative(process.cwd(), target)} to ${wanted.palette.colors.length} colours`);
13076
13654
  }
13077
13655
  }
13078
13656
  if (cacheDir) await cacheMedia(cacheDir, buf, mediaType);
13079
- await (0, import_promises12.mkdir)(import_node_path20.default.dirname(target), { recursive: true });
13657
+ await (0, import_promises13.mkdir)(import_node_path21.default.dirname(target), { recursive: true });
13080
13658
  const tmp = `${target}.pixelkiln.tmp`;
13081
- await (0, import_promises12.writeFile)(tmp, buf);
13659
+ await (0, import_promises13.writeFile)(tmp, buf);
13082
13660
  outputs.push({
13083
13661
  path: portableOutputPath(target, spec.root),
13084
13662
  sha256: sha256(buf),
@@ -13088,9 +13666,9 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
13088
13666
  });
13089
13667
  upsert(lock, key, { outputs: mergeOutputs(entry.outputs, outputs) });
13090
13668
  await saveLock(lockPath, lock);
13091
- await (0, import_promises12.rename)(tmp, target);
13669
+ await (0, import_promises13.rename)(tmp, target);
13092
13670
  wrote = true;
13093
- log(` wrote ${import_node_path20.default.relative(process.cwd(), target)}`);
13671
+ log(` wrote ${import_node_path21.default.relative(process.cwd(), target)}`);
13094
13672
  }
13095
13673
  if (opts.refresh && !wrote) {
13096
13674
  if (rotated) {
@@ -13134,10 +13712,10 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
13134
13712
  return result;
13135
13713
  }
13136
13714
  async function readCachedMedia(cacheDir, hash, mediaType) {
13137
- const file = import_node_path20.default.join(cacheDir, cacheFileName(hash, mediaType));
13138
- if (!(0, import_node_fs16.existsSync)(file)) return null;
13715
+ const file = import_node_path21.default.join(cacheDir, cacheFileName(hash, mediaType));
13716
+ if (!(0, import_node_fs17.existsSync)(file)) return null;
13139
13717
  try {
13140
- const buf = await (0, import_promises12.readFile)(file);
13718
+ const buf = await (0, import_promises13.readFile)(file);
13141
13719
  if (sha256(buf) !== hash) return null;
13142
13720
  validateMedia(buf, mediaType);
13143
13721
  return buf;
@@ -13148,17 +13726,17 @@ async function readCachedMedia(cacheDir, hash, mediaType) {
13148
13726
  async function cacheMedia(cacheDir, buf, mediaType, knownHash) {
13149
13727
  const hash = knownHash ?? sha256(buf);
13150
13728
  validateMedia(buf, mediaType);
13151
- const file = import_node_path20.default.join(cacheDir, cacheFileName(hash, mediaType));
13729
+ const file = import_node_path21.default.join(cacheDir, cacheFileName(hash, mediaType));
13152
13730
  if (await readCachedMedia(cacheDir, hash, mediaType)) return;
13153
- await (0, import_promises12.mkdir)(cacheDir, { recursive: true });
13731
+ await (0, import_promises13.mkdir)(cacheDir, { recursive: true });
13154
13732
  const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
13155
- await (0, import_promises12.writeFile)(tmp, buf);
13733
+ await (0, import_promises13.writeFile)(tmp, buf);
13156
13734
  try {
13157
- await (0, import_promises12.rename)(tmp, file);
13735
+ await (0, import_promises13.rename)(tmp, file);
13158
13736
  } catch (err) {
13159
- if (!(0, import_node_fs16.existsSync)(file)) throw err;
13737
+ if (!(0, import_node_fs17.existsSync)(file)) throw err;
13160
13738
  } finally {
13161
- await (0, import_promises12.rm)(tmp, { force: true });
13739
+ await (0, import_promises13.rm)(tmp, { force: true });
13162
13740
  }
13163
13741
  }
13164
13742
  function mergeOutputs(previous, current) {
@@ -13176,7 +13754,7 @@ async function pushTags(provider, specs, lock, opts = {}) {
13176
13754
  for (const [key, entry] of Object.entries(lock.entries)) {
13177
13755
  const spec = specByKey.get(key);
13178
13756
  if (!spec || entry.provider !== provider.id || !entry.objectId) continue;
13179
- if (entry.generator === "pixflux") continue;
13757
+ if (entry.generator === "pixflux" || entry.generator === "vfx") continue;
13180
13758
  if (!provider.setTags) return tagged;
13181
13759
  try {
13182
13760
  await provider.setTags(entry.objectId, spec.tags, entry.generator);
@@ -13275,10 +13853,10 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
13275
13853
  }
13276
13854
  for (let i = 0; i < entry.outputs.length; i++) {
13277
13855
  const file = currentEntryOutputPath(entry, spec, i);
13278
- if (!(0, import_node_fs17.existsSync)(file)) continue;
13856
+ if (!(0, import_node_fs18.existsSync)(file)) continue;
13279
13857
  if (await sha256File(file) !== entry.outputs[i].sha256 && !opts.force) {
13280
13858
  throw new OverwriteRefusedError(
13281
- `${import_node_path21.default.relative(process.cwd(), file)} was changed after download; keep it with pixelkiln edit, or pass --force to replace it`,
13859
+ `${import_node_path22.default.relative(process.cwd(), file)} was changed after download; keep it with pixelkiln edit, or pass --force to replace it`,
13282
13860
  [file]
13283
13861
  );
13284
13862
  }
@@ -13330,11 +13908,11 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
13330
13908
 
13331
13909
  // src/pipeline/cache-health.ts
13332
13910
  async function inspectCaches(lock, lockPath, options = {}) {
13333
- if (options.prune && !(0, import_node_fs18.existsSync)(lockPath)) {
13334
- throw new Error(`Refusing to prune without an existing lockfile at ${import_node_path22.default.resolve(lockPath)}`);
13911
+ if (options.prune && !(0, import_node_fs19.existsSync)(lockPath)) {
13912
+ throw new Error(`Refusing to prune without an existing lockfile at ${import_node_path23.default.resolve(lockPath)}`);
13335
13913
  }
13336
- const contentDir = import_node_path22.default.resolve(import_node_path22.default.dirname(lockPath), ".pixelkiln", "cache");
13337
- const remotePath = import_node_path22.default.resolve(cachePathFor(lockPath));
13914
+ const contentDir = import_node_path23.default.resolve(import_node_path23.default.dirname(lockPath), ".pixelkiln", "cache");
13915
+ const remotePath = import_node_path23.default.resolve(cachePathFor(lockPath));
13338
13916
  const referenced = referencedHashes(lock);
13339
13917
  let content = await inspectContentCache(contentDir, referenced);
13340
13918
  let remoteHashes = await inspectRemoteHashCache(remotePath);
@@ -13346,7 +13924,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
13346
13924
  ]);
13347
13925
  for (const name of names) {
13348
13926
  try {
13349
- await (0, import_promises13.rm)(import_node_path22.default.join(contentDir, name), { force: true });
13927
+ await (0, import_promises14.rm)(import_node_path23.default.join(contentDir, name), { force: true });
13350
13928
  removed.contentFiles++;
13351
13929
  } catch {
13352
13930
  }
@@ -13355,7 +13933,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
13355
13933
  await saveCache(remotePath, { version: 1, hashes: {} });
13356
13934
  removed.resetRemoteHashCache = true;
13357
13935
  } else if (remoteHashes.invalidIds.length) {
13358
- const cache = parseCache(JSON.parse(await (0, import_promises13.readFile)(remotePath, "utf8")));
13936
+ const cache = parseCache(JSON.parse(await (0, import_promises14.readFile)(remotePath, "utf8")));
13359
13937
  for (const id of remoteHashes.invalidIds) delete cache.hashes[id];
13360
13938
  removed.remoteHashEntries = remoteHashes.invalidIds.length;
13361
13939
  await saveCache(remotePath, cache);
@@ -13373,7 +13951,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
13373
13951
  async function inspectContentCache(contentDir, referenced) {
13374
13952
  const report = {
13375
13953
  path: contentDir,
13376
- exists: (0, import_node_fs18.existsSync)(contentDir),
13954
+ exists: (0, import_node_fs19.existsSync)(contentDir),
13377
13955
  files: 0,
13378
13956
  bytes: 0,
13379
13957
  valid: 0,
@@ -13387,19 +13965,19 @@ async function inspectContentCache(contentDir, referenced) {
13387
13965
  return report;
13388
13966
  }
13389
13967
  const present = /* @__PURE__ */ new Set();
13390
- const entries = await (0, import_promises13.readdir)(contentDir, { withFileTypes: true });
13968
+ const entries = await (0, import_promises14.readdir)(contentDir, { withFileTypes: true });
13391
13969
  for (const entry of entries) {
13392
13970
  if (!entry.isFile()) {
13393
13971
  report.invalid.push({ name: entry.name, reason: "not a regular file" });
13394
13972
  continue;
13395
13973
  }
13396
13974
  report.files++;
13397
- const file = import_node_path22.default.join(contentDir, entry.name);
13975
+ const file = import_node_path23.default.join(contentDir, entry.name);
13398
13976
  const mediaType = mediaTypeFromExtension(entry.name);
13399
13977
  const expected = mediaType ? entry.name.slice(0, -4) : "";
13400
13978
  let bytes;
13401
13979
  try {
13402
- bytes = await (0, import_promises13.readFile)(file);
13980
+ bytes = await (0, import_promises14.readFile)(file);
13403
13981
  report.bytes += bytes.length;
13404
13982
  } catch (err) {
13405
13983
  report.invalid.push({
@@ -13437,7 +14015,7 @@ async function inspectContentCache(contentDir, referenced) {
13437
14015
  async function inspectRemoteHashCache(remotePath) {
13438
14016
  const report = {
13439
14017
  path: remotePath,
13440
- exists: (0, import_node_fs18.existsSync)(remotePath),
14018
+ exists: (0, import_node_fs19.existsSync)(remotePath),
13441
14019
  entries: 0,
13442
14020
  valid: 0,
13443
14021
  invalidIds: [],
@@ -13446,7 +14024,7 @@ async function inspectRemoteHashCache(remotePath) {
13446
14024
  if (!report.exists) return report;
13447
14025
  let cache;
13448
14026
  try {
13449
- cache = parseCache(JSON.parse(await (0, import_promises13.readFile)(remotePath, "utf8")));
14027
+ cache = parseCache(JSON.parse(await (0, import_promises14.readFile)(remotePath, "utf8")));
13450
14028
  } catch (err) {
13451
14029
  report.error = err instanceof Error ? err.message : String(err);
13452
14030
  return report;
@@ -13461,20 +14039,20 @@ async function inspectRemoteHashCache(remotePath) {
13461
14039
  }
13462
14040
 
13463
14041
  // src/pipeline/pack.ts
13464
- var import_node_fs19 = require("fs");
13465
- var import_node_path23 = __toESM(require("path"), 1);
14042
+ var import_node_fs20 = require("fs");
14043
+ var import_node_path24 = __toESM(require("path"), 1);
13466
14044
  var DEFAULT_FRAME_SET_FPS = 12;
13467
14045
  function resolvePackInputs(raw, inputsFilePath) {
13468
14046
  if (!Array.isArray(raw) || !raw.length) {
13469
14047
  throw new Error("--inputs must be a non-empty JSON array of { id, path }");
13470
14048
  }
13471
- const baseDir = import_node_path23.default.dirname(import_node_path23.default.resolve(inputsFilePath));
14049
+ const baseDir = import_node_path24.default.dirname(import_node_path24.default.resolve(inputsFilePath));
13472
14050
  return raw.map((entry, i) => {
13473
14051
  const e = entry;
13474
14052
  if (typeof e.id !== "string" || typeof e.path !== "string") {
13475
14053
  throw new Error(`--inputs[${i}] needs string "id" and "path"`);
13476
14054
  }
13477
- return { id: e.id, path: import_node_path23.default.resolve(baseDir, e.path) };
14055
+ return { id: e.id, path: import_node_path24.default.resolve(baseDir, e.path) };
13478
14056
  });
13479
14057
  }
13480
14058
  function packSprites(inputs, options = {}) {
@@ -13495,7 +14073,7 @@ function packSprites(inputs, options = {}) {
13495
14073
  for (const input of ordered) {
13496
14074
  let sourceHash = null;
13497
14075
  try {
13498
- const bytes = (0, import_node_fs19.readFileSync)(input.path);
14076
+ const bytes = (0, import_node_fs20.readFileSync)(input.path);
13499
14077
  sourceHash = sha256(bytes);
13500
14078
  const png = decodePng(bytes);
13501
14079
  sprites.push({ id: input.id, width: png.width, height: png.height, pixels: png.pixels });
@@ -13558,7 +14136,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
13558
14136
  }
13559
14137
  const inputs2 = Object.entries(options.sourceOverrides).map(([id, source]) => ({
13560
14138
  id,
13561
- path: import_node_path23.default.resolve(source)
14139
+ path: import_node_path24.default.resolve(source)
13562
14140
  }));
13563
14141
  if (!inputs2.length) throw new Error(`No approved quality outputs for style "${styleId}".`);
13564
14142
  const packed2 = packSprites(inputs2, options);
@@ -13582,7 +14160,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
13582
14160
  let outputs = resolveEntryOutputs(entry, id, manifestDir);
13583
14161
  const source = sources[id];
13584
14162
  if (source && outputs.length <= 1) {
13585
- inputs.push({ id, path: import_node_path23.default.resolve(manifestDir, source) });
14163
+ inputs.push({ id, path: import_node_path24.default.resolve(manifestDir, source) });
13586
14164
  continue;
13587
14165
  }
13588
14166
  if (source && sourceIsStem(source, entry, manifestDir)) {
@@ -13611,12 +14189,13 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
13611
14189
  id,
13612
14190
  kind: isFrames ? "frames" : "members",
13613
14191
  ...fps ? { fps } : {},
14192
+ ...isFrames && frameSetLoop(entry) === false ? { loop: false } : {},
13614
14193
  frames: selected.map((output) => output.id)
13615
14194
  });
13616
14195
  }
13617
14196
  }
13618
14197
  for (const [id, source] of Object.entries(sources)) {
13619
- if (!locked.has(id)) inputs.push({ id, path: import_node_path23.default.resolve(manifestDir, source) });
14198
+ if (!locked.has(id)) inputs.push({ id, path: import_node_path24.default.resolve(manifestDir, source) });
13620
14199
  }
13621
14200
  if (!inputs.length) {
13622
14201
  throw new Error(`No readable sprites for style "${styleId}"; ${noOutput.length} skipped.`);
@@ -13653,7 +14232,7 @@ function mountSprites(placements, options) {
13653
14232
  for (const p of placements) {
13654
14233
  let sourceHash = null;
13655
14234
  try {
13656
- const bytes = (0, import_node_fs19.readFileSync)(p.path);
14235
+ const bytes = (0, import_node_fs20.readFileSync)(p.path);
13657
14236
  sourceHash = sha256(bytes);
13658
14237
  const png = decodePng(bytes);
13659
14238
  if (png.width > cellWidth || png.height > cellHeight) {
@@ -13724,7 +14303,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
13724
14303
  const source = sources[id];
13725
14304
  const entry = lock.entries[`${styleId}/${id}`];
13726
14305
  if (source && !(entry && sourceIsStem(source, entry, manifestDir))) {
13727
- placements.push({ id, path: import_node_path23.default.resolve(manifestDir, source), cell });
14306
+ placements.push({ id, path: import_node_path24.default.resolve(manifestDir, source), cell });
13728
14307
  continue;
13729
14308
  }
13730
14309
  if (source && entry) {
@@ -13757,7 +14336,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
13757
14336
  `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}`
13758
14337
  );
13759
14338
  }
13760
- const basePng = mount.base ? (0, import_node_fs19.readFileSync)(import_node_path23.default.resolve(manifestDir, mount.base)) : void 0;
14339
+ const basePng = mount.base ? (0, import_node_fs20.readFileSync)(import_node_path24.default.resolve(manifestDir, mount.base)) : void 0;
13761
14340
  const mounted = mountSprites(placements, {
13762
14341
  cellWidth: mount.cellWidth,
13763
14342
  cellHeight: mount.cellHeight,
@@ -13767,7 +14346,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
13767
14346
  ...mounted,
13768
14347
  sources: mount.base ? [{
13769
14348
  id: "$base",
13770
- path: import_node_path23.default.resolve(manifestDir, mount.base),
14349
+ path: import_node_path24.default.resolve(manifestDir, mount.base),
13771
14350
  sha256: sha256(basePng),
13772
14351
  included: true
13773
14352
  }, ...mounted.sources] : mounted.sources,
@@ -14149,7 +14728,7 @@ function renderGodotSpriteFrames(atlas, opts) {
14149
14728
  );
14150
14729
  };
14151
14730
  for (const set of atlas.sets ?? []) {
14152
- if (set.kind === "frames") animation(set.id, set.frames, set.fps ?? GODOT_DEFAULT_SPEED, true);
14731
+ if (set.kind === "frames") animation(set.id, set.frames, set.fps ?? GODOT_DEFAULT_SPEED, set.loop ?? true);
14153
14732
  else for (const id of set.frames) animation(id, [id], GODOT_DEFAULT_SPEED, false);
14154
14733
  }
14155
14734
  for (const frame of atlas.frames) {
@@ -14404,6 +14983,8 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
14404
14983
  const anchor = spec.character?.styleAnchor;
14405
14984
  const anchorEntry = anchor ? lock.entries[lockKey(anchor.spec.styleId, anchor.spec.assetId)] : void 0;
14406
14985
  const replacedMetadata = previousEntry?.providerMetadata?.[provider.id];
14986
+ const lastGeneration = previousEntry?.status === "downloaded" ? previousEntry : previousEntry?.history?.[0];
14987
+ const lastGenerationMetadata = lastGeneration?.providerMetadata?.[provider.id];
14407
14988
  const parentOutputs = spec.character?.kind === "outfit" && parentEntry ? parentEntry.outputs.map((output, index) => ({
14408
14989
  path: currentEntryOutputPath(parentEntry, spec.character.parentSpec, index),
14409
14990
  role: output.role
@@ -14416,6 +14997,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
14416
14997
  ...anchorEntry?.objectId ? { styleObjectId: anchorEntry.objectId } : {},
14417
14998
  ...parentOutputs ? { parentOutputs } : {},
14418
14999
  ...replacedMetadata ? { replacedMetadata } : {},
15000
+ ...lastGenerationMetadata ? { lastGenerationMetadata } : {},
14419
15001
  ...previousMetadata ? { previousMetadata } : {},
14420
15002
  checkpoint: async (checkpoint) => {
14421
15003
  if (!checkpoint.jobId) throw new Error("Provider checkpoint returned an empty job id");
@@ -14578,9 +15160,9 @@ async function poll(provider, lock, lockPath, opts = {}) {
14578
15160
  }
14579
15161
 
14580
15162
  // src/pipeline/doctor.ts
14581
- var import_node_fs20 = require("fs");
14582
- var import_promises14 = require("fs/promises");
14583
- var import_node_path24 = __toESM(require("path"), 1);
15163
+ var import_node_fs21 = require("fs");
15164
+ var import_promises15 = require("fs/promises");
15165
+ var import_node_path25 = __toESM(require("path"), 1);
14584
15166
  async function doctor(loaded, specs, lock, lockPath, opts = {}) {
14585
15167
  const checks = [];
14586
15168
  const add = (id, level, message6) => checks.push({ id, level, message: message6 });
@@ -14591,14 +15173,14 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
14591
15173
  );
14592
15174
  add("lock", "ok", `${Object.keys(lock.entries).length} valid v2 lock entr(ies)`);
14593
15175
  const dirs = [...new Set(specs.flatMap((spec) => [
14594
- import_node_path24.default.dirname(spec.outFile),
14595
- ...spec.quality ? [import_node_path24.default.dirname(spec.quality.outFile)] : []
15176
+ import_node_path25.default.dirname(spec.outFile),
15177
+ ...spec.quality ? [import_node_path25.default.dirname(spec.quality.outFile)] : []
14596
15178
  ]))];
14597
15179
  const unwritable = [];
14598
15180
  for (const dir of dirs) {
14599
15181
  const ancestor = await nearestExistingDirectory(dir);
14600
15182
  try {
14601
- await (0, import_promises14.access)(ancestor, import_node_fs20.constants.W_OK);
15183
+ await (0, import_promises15.access)(ancestor, import_node_fs21.constants.W_OK);
14602
15184
  } catch {
14603
15185
  unwritable.push(dir);
14604
15186
  }
@@ -14615,7 +15197,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
14615
15197
  for (let index = 0; index < entry.outputs.length; index++) {
14616
15198
  const output = entry.outputs[index];
14617
15199
  const spec = specByKey.get(key);
14618
- const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, import_node_path24.default.dirname(lockPath));
15200
+ const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, import_node_path25.default.dirname(lockPath));
14619
15201
  const owner = outputOwners.get(absolute);
14620
15202
  if (owner && owner !== key) duplicateOutputs.push(`${output.path} (${owner}, ${key})`);
14621
15203
  outputOwners.set(absolute, key);
@@ -14630,7 +15212,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
14630
15212
  } else {
14631
15213
  add("lock-outputs", "ok", `${outputOwners.size} recorded output path(s) are uniquely owned`);
14632
15214
  }
14633
- const cacheDir = import_node_path24.default.join(import_node_path24.default.dirname(lockPath), ".pixelkiln", "cache");
15215
+ const cacheDir = import_node_path25.default.join(import_node_path25.default.dirname(lockPath), ".pixelkiln", "cache");
14634
15216
  const stranded = [];
14635
15217
  const invalidState = [];
14636
15218
  const staleMapUrls = [];
@@ -14646,11 +15228,11 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
14646
15228
  const hasSource = Boolean(entry.sourceUrl || entry.sourceUrls?.length);
14647
15229
  let hasCache = false;
14648
15230
  for (const output of entry.outputs) {
14649
- const cached = import_node_path24.default.join(
15231
+ const cached = import_node_path25.default.join(
14650
15232
  cacheDir,
14651
15233
  cacheFileName(output.sha256, output.mediaType ?? MediaType.PNG)
14652
15234
  );
14653
- if ((0, import_node_fs20.existsSync)(cached) && await sha256File(cached) === output.sha256) {
15235
+ if ((0, import_node_fs21.existsSync)(cached) && await sha256File(cached) === output.sha256) {
14654
15236
  hasCache = true;
14655
15237
  break;
14656
15238
  }
@@ -14743,19 +15325,19 @@ async function nearestExistingDirectory(start) {
14743
15325
  let current = start;
14744
15326
  for (; ; ) {
14745
15327
  try {
14746
- if ((await (0, import_promises14.stat)(current)).isDirectory()) return current;
15328
+ if ((await (0, import_promises15.stat)(current)).isDirectory()) return current;
14747
15329
  } catch {
14748
15330
  }
14749
- const parent = import_node_path24.default.dirname(current);
15331
+ const parent = import_node_path25.default.dirname(current);
14750
15332
  if (parent === current) return current;
14751
15333
  current = parent;
14752
15334
  }
14753
15335
  }
14754
15336
 
14755
15337
  // src/pipeline/adopt.ts
14756
- var import_node_fs21 = require("fs");
14757
- var import_promises15 = require("fs/promises");
14758
- var import_node_path25 = __toESM(require("path"), 1);
15338
+ var import_node_fs22 = require("fs");
15339
+ var import_promises16 = require("fs/promises");
15340
+ var import_node_path26 = __toESM(require("path"), 1);
14759
15341
  async function adopt(provider, specs, lock, lockPath, opts = {}) {
14760
15342
  const log = opts.onProgress ?? (() => {
14761
15343
  });
@@ -14766,11 +15348,11 @@ async function adopt(provider, specs, lock, lockPath, opts = {}) {
14766
15348
  const localByHash = /* @__PURE__ */ new Map();
14767
15349
  const unmatchedLocal = [];
14768
15350
  for (const spec of specs) {
14769
- if (!(0, import_node_fs21.existsSync)(spec.outFile)) {
15351
+ if (!(0, import_node_fs22.existsSync)(spec.outFile)) {
14770
15352
  unmatchedLocal.push(`${spec.styleId}/${spec.assetId} (no file at ${spec.outFile})`);
14771
15353
  continue;
14772
15354
  }
14773
- const bytes = await (0, import_promises15.readFile)(spec.outFile);
15355
+ const bytes = await (0, import_promises16.readFile)(spec.outFile);
14774
15356
  try {
14775
15357
  decodePng(bytes);
14776
15358
  } catch (err) {
@@ -14888,13 +15470,13 @@ async function tagAdopted(provider, specs, lock, opts = {}) {
14888
15470
  }
14889
15471
 
14890
15472
  // src/pick/server.ts
14891
- var import_node_fs23 = require("fs");
14892
- var import_promises18 = require("fs/promises");
15473
+ var import_node_fs24 = require("fs");
15474
+ var import_promises19 = require("fs/promises");
14893
15475
 
14894
15476
  // src/pipeline/init.ts
14895
- var import_promises16 = require("fs/promises");
14896
- var import_node_fs22 = require("fs");
14897
- var import_node_path26 = __toESM(require("path"), 1);
15477
+ var import_promises17 = require("fs/promises");
15478
+ var import_node_fs23 = require("fs");
15479
+ var import_node_path27 = __toESM(require("path"), 1);
14898
15480
  function pngSize(buf) {
14899
15481
  if (buf.length < 24) return null;
14900
15482
  const isPng = buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834;
@@ -14907,8 +15489,8 @@ function slugify(value) {
14907
15489
  }
14908
15490
  async function walk(dir, exclude) {
14909
15491
  const out = [];
14910
- for (const entry of await (0, import_promises16.readdir)(dir, { withFileTypes: true })) {
14911
- const full = import_node_path26.default.join(dir, entry.name);
15492
+ for (const entry of await (0, import_promises17.readdir)(dir, { withFileTypes: true })) {
15493
+ const full = import_node_path27.default.join(dir, entry.name);
14912
15494
  if (exclude.some((x) => full.includes(x))) continue;
14913
15495
  if (entry.isDirectory()) out.push(...await walk(full, exclude));
14914
15496
  else if (entry.name.toLowerCase().endsWith(".png")) out.push(full);
@@ -14922,14 +15504,14 @@ async function scanAssets(root, opts = {}) {
14922
15504
  const skipped = [];
14923
15505
  const seen = /* @__PURE__ */ new Map();
14924
15506
  for (const file of files.sort()) {
14925
- const size = pngSize(await (0, import_promises16.readFile)(file));
15507
+ const size = pngSize(await (0, import_promises17.readFile)(file));
14926
15508
  if (!size) {
14927
15509
  skipped.push(`${file} (not a readable PNG)`);
14928
15510
  continue;
14929
15511
  }
14930
- const rel = import_node_path26.default.relative(root, file);
14931
- const category = import_node_path26.default.dirname(rel) === "." ? "" : import_node_path26.default.dirname(rel);
14932
- let id = slugify(import_node_path26.default.basename(rel));
15512
+ const rel = import_node_path27.default.relative(root, file);
15513
+ const category = import_node_path27.default.dirname(rel) === "." ? "" : import_node_path27.default.dirname(rel);
15514
+ let id = slugify(import_node_path27.default.basename(rel));
14933
15515
  if (seen.has(id)) {
14934
15516
  const n = seen.get(id) + 1;
14935
15517
  seen.set(id, n);
@@ -15393,7 +15975,7 @@ document.getElementById('submit').onclick = async () => {
15393
15975
 
15394
15976
  // src/pick/review-server.ts
15395
15977
  var import_node_http = require("http");
15396
- var import_promises17 = require("fs/promises");
15978
+ var import_promises18 = require("fs/promises");
15397
15979
  function serveReviewPage(opts) {
15398
15980
  const log = opts.onProgress ?? (() => {
15399
15981
  });
@@ -15402,7 +15984,7 @@ function serveReviewPage(opts) {
15402
15984
  if (req.method === "GET" && req.url && opts.assets?.has(req.url)) {
15403
15985
  try {
15404
15986
  const asset = opts.assets.get(req.url);
15405
- const bytes = await (0, import_promises17.readFile)(asset.path);
15987
+ const bytes = await (0, import_promises18.readFile)(asset.path);
15406
15988
  res.writeHead(200, {
15407
15989
  "Content-Type": asset.contentType,
15408
15990
  "Cache-Control": "no-store",
@@ -15478,13 +16060,13 @@ async function currentArt(entry, spec) {
15478
16060
  const previous = primaryOutput({ ...entry, outputs: entry.supersededOutputs });
15479
16061
  if (!previous) return null;
15480
16062
  const file = resolveOutputPath(previous.path, spec.root);
15481
- if (!(0, import_node_fs23.existsSync)(file)) return null;
16063
+ if (!(0, import_node_fs24.existsSync)(file)) return null;
15482
16064
  const contentType = previous.mediaType ?? mediaTypeFromExtension(file);
15483
16065
  if (!contentType) return null;
15484
16066
  let width = entry.width;
15485
16067
  let height = entry.height;
15486
16068
  if (contentType === "image/png") {
15487
- const size = pngSize(await (0, import_promises18.readFile)(file));
16069
+ const size = pngSize(await (0, import_promises19.readFile)(file));
15488
16070
  if (size) ({ width, height } = size);
15489
16071
  }
15490
16072
  return { path: file, contentType, width, height };
@@ -15492,7 +16074,7 @@ async function currentArt(entry, spec) {
15492
16074
  async function cachedFrame(url) {
15493
16075
  if (!url.startsWith("file://")) return null;
15494
16076
  const file = url.slice("file://".length);
15495
- const contentType = mediaTypeFromExtension(file) ?? detectMediaType(await (0, import_promises18.readFile)(file).catch(() => Buffer.alloc(0))) ?? "application/octet-stream";
16077
+ const contentType = mediaTypeFromExtension(file) ?? detectMediaType(await (0, import_promises19.readFile)(file).catch(() => Buffer.alloc(0))) ?? "application/octet-stream";
15496
16078
  return { path: file, contentType };
15497
16079
  }
15498
16080
  async function prepareReview(provider, lock, opts = {}) {
@@ -15674,14 +16256,14 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
15674
16256
 
15675
16257
  // src/gallery/snapshot.ts
15676
16258
  var import_node_crypto7 = require("crypto");
15677
- var import_node_fs29 = require("fs");
15678
- var import_promises26 = require("fs/promises");
15679
- var import_node_path34 = __toESM(require("path"), 1);
16259
+ var import_node_fs30 = require("fs");
16260
+ var import_promises27 = require("fs/promises");
16261
+ var import_node_path35 = __toESM(require("path"), 1);
15680
16262
 
15681
16263
  // src/workspace.ts
15682
- var import_promises19 = require("fs/promises");
15683
- var import_node_fs24 = require("fs");
15684
- var import_node_path27 = __toESM(require("path"), 1);
16264
+ var import_promises20 = require("fs/promises");
16265
+ var import_node_fs25 = require("fs");
16266
+ var import_node_path28 = __toESM(require("path"), 1);
15685
16267
  var import_zod7 = require("zod");
15686
16268
  var WorkspaceProjectSchema = import_zod7.z.object({
15687
16269
  id: import_zod7.z.string().min(1),
@@ -15706,10 +16288,10 @@ ${parsed.error.issues.slice(0, 5).map((i) => ` ${i.path.join(".")}: ${i.message
15706
16288
  );
15707
16289
  }
15708
16290
  async function loadWorkspace(workspacePath) {
15709
- if (!(0, import_node_fs24.existsSync)(workspacePath)) return { version: 1, projects: [] };
16291
+ if (!(0, import_node_fs25.existsSync)(workspacePath)) return { version: 1, projects: [] };
15710
16292
  let raw;
15711
16293
  try {
15712
- raw = JSON.parse(await (0, import_promises19.readFile)(workspacePath, "utf8"));
16294
+ raw = JSON.parse(await (0, import_promises20.readFile)(workspacePath, "utf8"));
15713
16295
  } catch (err) {
15714
16296
  throw new ProjectError(
15715
16297
  `Workspace catalog at ${workspacePath} is malformed:
@@ -15723,22 +16305,22 @@ async function saveWorkspace(workspacePath, ws) {
15723
16305
  version: 1,
15724
16306
  projects: [...ws.projects].sort((a, b) => a.id.localeCompare(b.id))
15725
16307
  };
15726
- await (0, import_promises19.mkdir)(import_node_path27.default.dirname(import_node_path27.default.resolve(workspacePath)), { recursive: true });
16308
+ await (0, import_promises20.mkdir)(import_node_path28.default.dirname(import_node_path28.default.resolve(workspacePath)), { recursive: true });
15727
16309
  const tmp = `${workspacePath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
15728
16310
  try {
15729
- await (0, import_promises19.writeFile)(tmp, JSON.stringify(sorted, null, 2) + "\n");
15730
- await (0, import_promises19.rename)(tmp, workspacePath);
16311
+ await (0, import_promises20.writeFile)(tmp, JSON.stringify(sorted, null, 2) + "\n");
16312
+ await (0, import_promises20.rename)(tmp, workspacePath);
15731
16313
  } finally {
15732
- await (0, import_promises19.rm)(tmp, { force: true });
16314
+ await (0, import_promises20.rm)(tmp, { force: true });
15733
16315
  }
15734
16316
  }
15735
16317
  function toPortablePath(dir, absolute) {
15736
- return import_node_path27.default.relative(dir, absolute).split(import_node_path27.default.sep).join("/");
16318
+ return import_node_path28.default.relative(dir, absolute).split(import_node_path28.default.sep).join("/");
15737
16319
  }
15738
16320
  function resolveProject(dir, project) {
15739
16321
  return {
15740
- manifestPath: import_node_path27.default.resolve(dir, project.manifest.split("/").join(import_node_path27.default.sep)),
15741
- lockPath: import_node_path27.default.resolve(dir, project.lock.split("/").join(import_node_path27.default.sep))
16322
+ manifestPath: import_node_path28.default.resolve(dir, project.manifest.split("/").join(import_node_path28.default.sep)),
16323
+ lockPath: import_node_path28.default.resolve(dir, project.lock.split("/").join(import_node_path28.default.sep))
15742
16324
  };
15743
16325
  }
15744
16326
  function validateWorkspace(ws, dir) {
@@ -15751,21 +16333,21 @@ function validateWorkspace(ws, dir) {
15751
16333
  const { manifestPath, lockPath } = resolveProject(dir, project);
15752
16334
  lockOwners.set(lockPath, [...lockOwners.get(lockPath) ?? [], project.id]);
15753
16335
  manifestOwners.set(manifestPath, [...manifestOwners.get(manifestPath) ?? [], project.id]);
15754
- if (import_node_path27.default.isAbsolute(project.manifest) || import_node_path27.default.isAbsolute(project.lock)) {
16336
+ if (import_node_path28.default.isAbsolute(project.manifest) || import_node_path28.default.isAbsolute(project.lock)) {
15755
16337
  diagnostics.push({
15756
16338
  id: "absolute-path",
15757
16339
  level: "warning",
15758
16340
  message: `project "${project.id}" stores an absolute path; the catalog will not resolve correctly if this tree is cloned or moved elsewhere`
15759
16341
  });
15760
16342
  }
15761
- if (!(0, import_node_fs24.existsSync)(manifestPath)) {
16343
+ if (!(0, import_node_fs25.existsSync)(manifestPath)) {
15762
16344
  diagnostics.push({
15763
16345
  id: "missing-manifest",
15764
16346
  level: "error",
15765
16347
  message: `project "${project.id}" manifest not found: ${manifestPath}`
15766
16348
  });
15767
16349
  }
15768
- if (!(0, import_node_fs24.existsSync)(lockPath)) {
16350
+ if (!(0, import_node_fs25.existsSync)(lockPath)) {
15769
16351
  diagnostics.push({
15770
16352
  id: "missing-lock",
15771
16353
  level: "error",
@@ -15812,16 +16394,16 @@ function validateWorkspace(ws, dir) {
15812
16394
  }
15813
16395
 
15814
16396
  // src/gallery/edit.ts
15815
- var import_promises25 = require("fs/promises");
15816
- var import_node_path33 = __toESM(require("path"), 1);
16397
+ var import_promises26 = require("fs/promises");
16398
+ var import_node_path34 = __toESM(require("path"), 1);
15817
16399
  var import_zod13 = require("zod");
15818
16400
 
15819
16401
  // src/manifest-edit.ts
15820
- var import_promises20 = require("fs/promises");
15821
- var import_node_path28 = __toESM(require("path"), 1);
16402
+ var import_promises21 = require("fs/promises");
16403
+ var import_node_path29 = __toESM(require("path"), 1);
15822
16404
  var import_zod8 = require("zod");
15823
16405
  var HexSha = import_zod8.z.string().regex(/^[0-9a-f]{64}$/);
15824
- 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");
16406
+ var ProjectFileSchema = import_zod8.z.string().min(1).refine((file) => !import_node_path29.default.isAbsolute(file) && !file.split(/[\\/]/).includes(".."), "must be a path inside the project");
15825
16407
  var AssetPatchSchema = import_zod8.z.object({
15826
16408
  prompt: import_zod8.z.string().optional(),
15827
16409
  /** Per-style override for the style being viewed; null removes it. */
@@ -16240,8 +16822,8 @@ function applyEdit(raw, edit) {
16240
16822
  }
16241
16823
  }
16242
16824
  async function withCandidate(manifestPath, edit, use, unchanged) {
16243
- const absolute = import_node_path28.default.resolve(manifestPath);
16244
- const original = await (0, import_promises20.readFile)(absolute, "utf8");
16825
+ const absolute = import_node_path29.default.resolve(manifestPath);
16826
+ const original = await (0, import_promises21.readFile)(absolute, "utf8");
16245
16827
  if (sha256(original) !== edit.expectedSha256) throw new ManifestDriftError(absolute);
16246
16828
  let raw;
16247
16829
  try {
@@ -16254,12 +16836,12 @@ async function withCandidate(manifestPath, edit, use, unchanged) {
16254
16836
  applyEdit(raw, edit);
16255
16837
  const next = serializeLike(original, raw);
16256
16838
  if (next === original) return unchanged(absolute, original);
16257
- const tmp = import_node_path28.default.join(
16258
- import_node_path28.default.dirname(absolute),
16259
- `.${import_node_path28.default.basename(absolute)}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`
16839
+ const tmp = import_node_path29.default.join(
16840
+ import_node_path29.default.dirname(absolute),
16841
+ `.${import_node_path29.default.basename(absolute)}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`
16260
16842
  );
16261
16843
  try {
16262
- await (0, import_promises20.writeFile)(tmp, next);
16844
+ await (0, import_promises21.writeFile)(tmp, next);
16263
16845
  let specs;
16264
16846
  try {
16265
16847
  specs = await resolveSpecs(await loadManifest(tmp));
@@ -16269,7 +16851,7 @@ async function withCandidate(manifestPath, edit, use, unchanged) {
16269
16851
  }
16270
16852
  return await use({ absolute, original, next, tmp, specs });
16271
16853
  } finally {
16272
- await (0, import_promises20.rm)(tmp, { force: true });
16854
+ await (0, import_promises21.rm)(tmp, { force: true });
16273
16855
  }
16274
16856
  }
16275
16857
  async function applyManifestEdit(manifestPath, edit) {
@@ -16277,7 +16859,7 @@ async function applyManifestEdit(manifestPath, edit) {
16277
16859
  manifestPath,
16278
16860
  edit,
16279
16861
  async ({ absolute, next, tmp }) => {
16280
- await (0, import_promises20.rename)(tmp, absolute);
16862
+ await (0, import_promises21.rename)(tmp, absolute);
16281
16863
  return { manifestPath: absolute, sha256: sha256(next), changed: true };
16282
16864
  },
16283
16865
  (absolute, original) => ({ manifestPath: absolute, sha256: sha256(original), changed: false })
@@ -16285,18 +16867,18 @@ async function applyManifestEdit(manifestPath, edit) {
16285
16867
  }
16286
16868
 
16287
16869
  // src/pipeline/hand-edit.ts
16288
- var import_node_fs25 = require("fs");
16289
- var import_promises21 = require("fs/promises");
16290
- var import_node_path29 = __toESM(require("path"), 1);
16870
+ var import_node_fs26 = require("fs");
16871
+ var import_promises22 = require("fs/promises");
16872
+ var import_node_path30 = __toESM(require("path"), 1);
16291
16873
  var import_zod9 = require("zod");
16292
16874
  var HAND_EDIT_DIR = "edits";
16293
16875
  function handEditPath(loaded, spec) {
16294
16876
  const style = loaded.manifest.styles[spec.styleId];
16295
- const outDir = import_node_path29.default.resolve(loaded.root, style?.outDir ?? ".");
16296
- const relative = import_node_path29.default.relative(outDir, spec.outFile);
16297
- const inside = relative && !relative.startsWith("..") && !import_node_path29.default.isAbsolute(relative);
16298
- const file = inside ? relative : import_node_path29.default.basename(spec.outFile);
16299
- return import_node_path29.default.join(outDir, HAND_EDIT_DIR, file.replace(/\.gif$/i, ".png"));
16877
+ const outDir = import_node_path30.default.resolve(loaded.root, style?.outDir ?? ".");
16878
+ const relative = import_node_path30.default.relative(outDir, spec.outFile);
16879
+ const inside = relative && !relative.startsWith("..") && !import_node_path30.default.isAbsolute(relative);
16880
+ const file = inside ? relative : import_node_path30.default.basename(spec.outFile);
16881
+ return import_node_path30.default.join(outDir, HAND_EDIT_DIR, file.replace(/\.gif$/i, ".png"));
16300
16882
  }
16301
16883
  function handEditBases(spec, lock) {
16302
16884
  const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
@@ -16309,10 +16891,10 @@ function handEditBases(spec, lock) {
16309
16891
  }));
16310
16892
  if (members.some((member) => member.mediaType && member.mediaType !== "image/png")) return null;
16311
16893
  if (entry.outputs.length === 1 && !primaryOutput(entry)) return null;
16312
- if (!members.every((member) => (0, import_node_fs25.existsSync)(member.path))) return null;
16894
+ if (!members.every((member) => (0, import_node_fs26.existsSync)(member.path))) return null;
16313
16895
  return members.map(({ role, path: file }) => ({ role, path: file }));
16314
16896
  }
16315
- return (0, import_node_fs25.existsSync)(spec.outFile) ? [{ role: null, path: spec.outFile }] : null;
16897
+ return (0, import_node_fs26.existsSync)(spec.outFile) ? [{ role: null, path: spec.outFile }] : null;
16316
16898
  }
16317
16899
  function handEditBase(spec, lock) {
16318
16900
  const bases = handEditBases(spec, lock);
@@ -16342,19 +16924,19 @@ async function startHandEdit(loaded, lock, spec, opts = {}) {
16342
16924
  `${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`
16343
16925
  );
16344
16926
  }
16345
- const existing = spec.source ? import_node_path29.default.resolve(loaded.root, spec.source) : null;
16927
+ const existing = spec.source ? import_node_path30.default.resolve(loaded.root, spec.source) : null;
16346
16928
  const editPath = existing ?? handEditPath(loaded, spec);
16347
16929
  const members = handEditMembers(loaded, spec, lock, editPath);
16348
16930
  let created = false;
16349
16931
  for (const member of members) {
16350
- if ((0, import_node_fs25.existsSync)(member.path)) continue;
16932
+ if ((0, import_node_fs26.existsSync)(member.path)) continue;
16351
16933
  if (!member.base) {
16352
16934
  throw new ManifestEditError(
16353
16935
  `${spec.styleId}/${spec.assetId} has no generated PNG on disk to start from; generate or restore it first`
16354
16936
  );
16355
16937
  }
16356
- await (0, import_promises21.mkdir)(import_node_path29.default.dirname(member.path), { recursive: true });
16357
- await (0, import_promises21.copyFile)(member.base, member.path);
16938
+ await (0, import_promises22.mkdir)(import_node_path30.default.dirname(member.path), { recursive: true });
16939
+ await (0, import_promises22.copyFile)(member.base, member.path);
16358
16940
  created = true;
16359
16941
  }
16360
16942
  const source = portableOutputPath(editPath, loaded.root);
@@ -16432,7 +17014,7 @@ function handEditProjectPath(editPath) {
16432
17014
  async function readHandEditCompanion(editPath) {
16433
17015
  let text;
16434
17016
  try {
16435
- text = await (0, import_promises21.readFile)(handEditCompanionPath(editPath), "utf8");
17017
+ text = await (0, import_promises22.readFile)(handEditCompanionPath(editPath), "utf8");
16436
17018
  } catch {
16437
17019
  return null;
16438
17020
  }
@@ -16475,7 +17057,7 @@ async function saveHandEdit(loaded, lock, spec, input) {
16475
17057
  }
16476
17058
  });
16477
17059
  const bases = handEditBases(spec, lock);
16478
- const expectedSize = bases?.[0] ? decodePng(await (0, import_promises21.readFile)(bases[0].path)) : { width: spec.width, height: spec.height };
17060
+ const expectedSize = bases?.[0] ? decodePng(await (0, import_promises22.readFile)(bases[0].path)) : { width: spec.width, height: spec.height };
16479
17061
  for (const [index, image] of decoded.entries()) {
16480
17062
  if (image.width !== expectedSize.width || image.height !== expectedSize.height) {
16481
17063
  const which = memberSet ? `member ${incoming[index].role ?? index}` : "the edit";
@@ -16491,7 +17073,7 @@ async function saveHandEdit(loaded, lock, spec, input) {
16491
17073
  editor: input.editor,
16492
17074
  protocol: input.protocol,
16493
17075
  savedAt: (input.now ?? /* @__PURE__ */ new Date()).toISOString(),
16494
- project: projectPath ? import_node_path29.default.basename(projectPath) : null,
17076
+ project: projectPath ? import_node_path30.default.basename(projectPath) : null,
16495
17077
  outputs: incoming.map((frame, index) => ({
16496
17078
  role: frame.role,
16497
17079
  basedOn: basedOnHash(entry, index),
@@ -16521,10 +17103,10 @@ function basedOnHash(entry, index) {
16521
17103
  async function replaceFile(file, bytes) {
16522
17104
  const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
16523
17105
  try {
16524
- await (0, import_promises21.writeFile)(tmp, bytes);
16525
- await (0, import_promises21.rename)(tmp, file);
17106
+ await (0, import_promises22.writeFile)(tmp, bytes);
17107
+ await (0, import_promises22.rename)(tmp, file);
16526
17108
  } finally {
16527
- await (0, import_promises21.rm)(tmp, { force: true });
17109
+ await (0, import_promises22.rm)(tmp, { force: true });
16528
17110
  }
16529
17111
  }
16530
17112
  function openInEditor(file, editor = process.env.PIXELKILN_EDITOR) {
@@ -16533,9 +17115,9 @@ function openInEditor(file, editor = process.env.PIXELKILN_EDITOR) {
16533
17115
  }
16534
17116
 
16535
17117
  // src/gallery/upload.ts
16536
- var import_node_fs26 = require("fs");
16537
- var import_promises22 = require("fs/promises");
16538
- var import_node_path30 = __toESM(require("path"), 1);
17118
+ var import_node_fs27 = require("fs");
17119
+ var import_promises23 = require("fs/promises");
17120
+ var import_node_path31 = __toESM(require("path"), 1);
16539
17121
  var import_zod10 = require("zod");
16540
17122
  var MAX_UPLOAD_BYTES = 8 * 1024 * 1024;
16541
17123
  var UploadImageSchema = import_zod10.z.object({
@@ -16548,12 +17130,12 @@ var UploadImageSchema = import_zod10.z.object({
16548
17130
  overwrite: import_zod10.z.boolean().optional()
16549
17131
  }).strict();
16550
17132
  async function saveProjectImage(root, request) {
16551
- const file = import_node_path30.default.resolve(root, request.path);
16552
- const relative = import_node_path30.default.relative(root, file);
16553
- if (relative.startsWith("..") || import_node_path30.default.isAbsolute(relative)) {
17133
+ const file = import_node_path31.default.resolve(root, request.path);
17134
+ const relative = import_node_path31.default.relative(root, file);
17135
+ if (relative.startsWith("..") || import_node_path31.default.isAbsolute(relative)) {
16554
17136
  throw new ManifestEditError("an upload must stay inside the project");
16555
17137
  }
16556
- const extension = import_node_path30.default.extname(file).toLowerCase();
17138
+ const extension = import_node_path31.default.extname(file).toLowerCase();
16557
17139
  const expected = extension === ".png" ? "png" : extension === ".jpg" || extension === ".jpeg" ? "jpeg" : null;
16558
17140
  if (!expected) throw new ManifestEditError("an upload must be a .png, .jpg, or .jpeg path");
16559
17141
  const bytes = Buffer.from(request.base64, "base64");
@@ -16565,18 +17147,18 @@ async function saveProjectImage(root, request) {
16565
17147
  if (metadata.format !== expected) {
16566
17148
  throw new ManifestEditError(`the upload is a ${metadata.format.toUpperCase()}; name it ${metadata.format === "png" ? ".png" : ".jpg"}`);
16567
17149
  }
16568
- if ((0, import_node_fs26.existsSync)(file) && !request.overwrite) {
16569
- throw new ManifestEditError(`${relative.split(import_node_path30.default.sep).join("/")} already exists; choose another name, or replace it`);
17150
+ if ((0, import_node_fs27.existsSync)(file) && !request.overwrite) {
17151
+ throw new ManifestEditError(`${relative.split(import_node_path31.default.sep).join("/")} already exists; choose another name, or replace it`);
16570
17152
  }
16571
- await (0, import_promises22.mkdir)(import_node_path30.default.dirname(file), { recursive: true });
16572
- await (0, import_promises22.writeFile)(file, bytes);
16573
- return relative.split(import_node_path30.default.sep).join("/");
17153
+ await (0, import_promises23.mkdir)(import_node_path31.default.dirname(file), { recursive: true });
17154
+ await (0, import_promises23.writeFile)(file, bytes);
17155
+ return relative.split(import_node_path31.default.sep).join("/");
16574
17156
  }
16575
17157
 
16576
17158
  // src/gallery/image-revision.ts
16577
- var import_node_fs27 = require("fs");
16578
- var import_promises23 = require("fs/promises");
16579
- var import_node_path31 = __toESM(require("path"), 1);
17159
+ var import_node_fs28 = require("fs");
17160
+ var import_promises24 = require("fs/promises");
17161
+ var import_node_path32 = __toESM(require("path"), 1);
16580
17162
  var import_zod11 = require("zod");
16581
17163
  var InpaintRevisionRequestSchema = import_zod11.z.object({
16582
17164
  action: import_zod11.z.literal("create-inpaint-revision"),
@@ -16595,15 +17177,15 @@ var InpaintRevisionRequestSchema = import_zod11.z.object({
16595
17177
  overwrite: import_zod11.z.boolean().optional()
16596
17178
  }).strict();
16597
17179
  async function createInpaintRevision(manifestPath, request) {
16598
- const root = import_node_path31.default.dirname(import_node_path31.default.resolve(manifestPath));
16599
- if (import_node_path31.default.extname(request.maskPath).toLowerCase() !== ".png") {
17180
+ const root = import_node_path32.default.dirname(import_node_path32.default.resolve(manifestPath));
17181
+ if (import_node_path32.default.extname(request.maskPath).toLowerCase() !== ".png") {
16600
17182
  throw new ManifestEditError("the mask must be a .png path inside the project");
16601
17183
  }
16602
17184
  const bytes = Buffer.from(request.maskBase64, "base64");
16603
17185
  const metadata = imageMetadata(bytes);
16604
17186
  if (!metadata || metadata.format !== "png") throw new ManifestEditError("the mask is not a readable PNG");
16605
- const file = import_node_path31.default.resolve(root, request.maskPath);
16606
- const previous = (0, import_node_fs27.existsSync)(file) ? await (0, import_promises23.readFile)(file) : null;
17187
+ const file = import_node_path32.default.resolve(root, request.maskPath);
17188
+ const previous = (0, import_node_fs28.existsSync)(file) ? await (0, import_promises24.readFile)(file) : null;
16607
17189
  const mask = await saveProjectImage(root, {
16608
17190
  action: "upload-image",
16609
17191
  path: request.maskPath,
@@ -16622,17 +17204,17 @@ async function createInpaintRevision(manifestPath, request) {
16622
17204
  }
16623
17205
  });
16624
17206
  } catch (error) {
16625
- if (previous) await (0, import_promises23.writeFile)(file, previous);
16626
- else await (0, import_promises23.rm)(file, { force: true });
17207
+ if (previous) await (0, import_promises24.writeFile)(file, previous);
17208
+ else await (0, import_promises24.rm)(file, { force: true });
16627
17209
  throw error;
16628
17210
  }
16629
17211
  return mask;
16630
17212
  }
16631
17213
 
16632
17214
  // src/gallery/skeleton.ts
16633
- var import_node_fs28 = require("fs");
16634
- var import_promises24 = require("fs/promises");
16635
- var import_node_path32 = __toESM(require("path"), 1);
17215
+ var import_node_fs29 = require("fs");
17216
+ var import_promises25 = require("fs/promises");
17217
+ var import_node_path33 = __toESM(require("path"), 1);
16636
17218
  var import_zod12 = require("zod");
16637
17219
  var SkeletonEstimateRequestSchema = import_zod12.z.object({
16638
17220
  project: import_zod12.z.string().min(1).optional(),
@@ -16670,21 +17252,21 @@ var SkeletonKeypointsUpdateSchema = import_zod12.z.object({
16670
17252
  set: SkeletonSetSchema
16671
17253
  }).strict();
16672
17254
  async function createSkeletonAnimation(manifestPath, request) {
16673
- const root = import_node_path32.default.dirname(import_node_path32.default.resolve(manifestPath));
16674
- const file = import_node_path32.default.resolve(root, request.keypointsFile);
16675
- const relative = import_node_path32.default.relative(root, file);
16676
- if (!request.keypointsFile.endsWith(".json") || relative.startsWith("..") || import_node_path32.default.isAbsolute(relative)) {
17255
+ const root = import_node_path33.default.dirname(import_node_path33.default.resolve(manifestPath));
17256
+ const file = import_node_path33.default.resolve(root, request.keypointsFile);
17257
+ const relative = import_node_path33.default.relative(root, file);
17258
+ if (!request.keypointsFile.endsWith(".json") || relative.startsWith("..") || import_node_path33.default.isAbsolute(relative)) {
16677
17259
  throw new ManifestEditError("the keypoints file must be a .json path inside the project");
16678
17260
  }
16679
- const existed = (0, import_node_fs28.existsSync)(file);
17261
+ const existed = (0, import_node_fs29.existsSync)(file);
16680
17262
  let previous = null;
16681
17263
  if (request.set) {
16682
17264
  if (existed && !request.overwrite) {
16683
17265
  throw new ManifestEditError(`${request.keypointsFile} already exists; choose another path, or replace it`);
16684
17266
  }
16685
- if (existed) previous = await (0, import_promises24.readFile)(file);
16686
- await (0, import_promises24.mkdir)(import_node_path32.default.dirname(file), { recursive: true });
16687
- await (0, import_promises24.writeFile)(file, JSON.stringify(request.set, null, 2) + "\n");
17267
+ if (existed) previous = await (0, import_promises25.readFile)(file);
17268
+ await (0, import_promises25.mkdir)(import_node_path33.default.dirname(file), { recursive: true });
17269
+ await (0, import_promises25.writeFile)(file, JSON.stringify(request.set, null, 2) + "\n");
16688
17270
  } else if (!existed) {
16689
17271
  throw new ManifestEditError(`${request.keypointsFile} does not exist; paste poses or estimate them from the sprite`);
16690
17272
  }
@@ -16699,7 +17281,7 @@ async function createSkeletonAnimation(manifestPath, request) {
16699
17281
  revision: {
16700
17282
  mode: "animate-skeleton",
16701
17283
  from: request.from,
16702
- keypointsFile: relative.split(import_node_path32.default.sep).join("/"),
17284
+ keypointsFile: relative.split(import_node_path33.default.sep).join("/"),
16703
17285
  direction: request.direction,
16704
17286
  ...request.description ? { description: request.description } : {},
16705
17287
  ...request.skeletonTemplate ? { skeletonTemplate: request.skeletonTemplate } : {}
@@ -16708,8 +17290,8 @@ async function createSkeletonAnimation(manifestPath, request) {
16708
17290
  });
16709
17291
  } catch (error) {
16710
17292
  if (request.set) {
16711
- if (previous) await (0, import_promises24.writeFile)(file, previous);
16712
- else await (0, import_promises24.rm)(file, { force: true });
17293
+ if (previous) await (0, import_promises25.writeFile)(file, previous);
17294
+ else await (0, import_promises25.rm)(file, { force: true });
16713
17295
  }
16714
17296
  throw error;
16715
17297
  }
@@ -16720,19 +17302,19 @@ async function updateSkeletonKeypoints(ctx, request) {
16720
17302
  if (!spec) throw new ManifestEditError(`"${key}" is not declared by the manifest`);
16721
17303
  const file = spec.revision?.mode === "animate-skeleton" ? spec.revision.keypointsFile : void 0;
16722
17304
  if (!file) throw new ManifestEditError(`${key} is not an animate-skeleton revision`);
16723
- const relative = import_node_path32.default.relative(ctx.loaded.root, file);
16724
- if (relative.startsWith("..") || import_node_path32.default.isAbsolute(relative)) {
17305
+ const relative = import_node_path33.default.relative(ctx.loaded.root, file);
17306
+ if (relative.startsWith("..") || import_node_path33.default.isAbsolute(relative)) {
16725
17307
  throw new ManifestEditError(`${key}'s keypoints file is outside the project; edit it by hand`);
16726
17308
  }
16727
- const current = (0, import_node_fs28.existsSync)(file) ? sha256(await (0, import_promises24.readFile)(file)) : null;
17309
+ const current = (0, import_node_fs29.existsSync)(file) ? sha256(await (0, import_promises25.readFile)(file)) : null;
16728
17310
  if (current !== request.expectedSha256) {
16729
17311
  throw Object.assign(
16730
- new Error(`${relative.split(import_node_path32.default.sep).join("/")} changed on disk since this page loaded it; press Refresh and edit again`),
17312
+ new Error(`${relative.split(import_node_path33.default.sep).join("/")} changed on disk since this page loaded it; press Refresh and edit again`),
16731
17313
  { status: 409 }
16732
17314
  );
16733
17315
  }
16734
- await (0, import_promises24.writeFile)(file, JSON.stringify(request.set, null, 2) + "\n");
16735
- return relative.split(import_node_path32.default.sep).join("/");
17316
+ await (0, import_promises25.writeFile)(file, JSON.stringify(request.set, null, 2) + "\n");
17317
+ return relative.split(import_node_path33.default.sep).join("/");
16736
17318
  }
16737
17319
 
16738
17320
  // src/gallery/edit.ts
@@ -16786,7 +17368,7 @@ function createGalleryEditHandler(opts) {
16786
17368
  const history = /* @__PURE__ */ new Map();
16787
17369
  const manifestOf = async (project) => {
16788
17370
  try {
16789
- return import_node_path33.default.resolve(await opts.manifestFor(typeof project === "string" ? project : void 0));
17371
+ return import_node_path34.default.resolve(await opts.manifestFor(typeof project === "string" ? project : void 0));
16790
17372
  } catch {
16791
17373
  return null;
16792
17374
  }
@@ -16796,7 +17378,7 @@ function createGalleryEditHandler(opts) {
16796
17378
  if (undo.success) {
16797
17379
  const manifestPath2 = await manifestOf(undo.data.project);
16798
17380
  if (!manifestPath2) throw new ManifestEditError("no manifest to undo an edit in");
16799
- const current = await (0, import_promises25.readFile)(manifestPath2, "utf8");
17381
+ const current = await (0, import_promises26.readFile)(manifestPath2, "utf8");
16800
17382
  if (sha256(current) !== undo.data.expectedSha256) throw new ManifestDriftError(manifestPath2);
16801
17383
  const stack = history.get(manifestPath2) ?? [];
16802
17384
  const last = stack[stack.length - 1];
@@ -16806,17 +17388,17 @@ function createGalleryEditHandler(opts) {
16806
17388
  throw Object.assign(new Error("the manifest changed outside the gallery since its last edit; undo would discard that change"), { status: 409 });
16807
17389
  }
16808
17390
  const tmp = manifestPath2 + ".pixelkiln-undo";
16809
- await (0, import_promises25.writeFile)(tmp, last.before);
16810
- await (0, import_promises25.rename)(tmp, manifestPath2);
17391
+ await (0, import_promises26.writeFile)(tmp, last.before);
17392
+ await (0, import_promises26.rename)(tmp, manifestPath2);
16811
17393
  stack.pop();
16812
- log(` manifest edit undone in ${import_node_path33.default.relative(process.cwd(), manifestPath2) || manifestPath2}`);
17394
+ log(` manifest edit undone in ${import_node_path34.default.relative(process.cwd(), manifestPath2) || manifestPath2}`);
16813
17395
  return opts.reload();
16814
17396
  }
16815
17397
  const manifestPath = await manifestOf(body?.project);
16816
- const before = manifestPath ? await (0, import_promises25.readFile)(manifestPath, "utf8").catch(() => null) : null;
17398
+ const before = manifestPath ? await (0, import_promises26.readFile)(manifestPath, "utf8").catch(() => null) : null;
16817
17399
  const build = await apply(body);
16818
17400
  if (manifestPath && before !== null) {
16819
- const after = await (0, import_promises25.readFile)(manifestPath, "utf8").catch(() => null);
17401
+ const after = await (0, import_promises26.readFile)(manifestPath, "utf8").catch(() => null);
16820
17402
  if (after !== null && after !== before) {
16821
17403
  const stack = history.get(manifestPath) ?? [];
16822
17404
  stack.push({ before, afterSha: sha256(after) });
@@ -16871,7 +17453,7 @@ function createEditApplier(opts, log) {
16871
17453
  } catch (error) {
16872
17454
  throw new ManifestEditError(error instanceof Error ? error.message : String(error));
16873
17455
  }
16874
- const written = await saveProjectImage(import_node_path33.default.dirname(import_node_path33.default.resolve(manifestPath2)), upload.data);
17456
+ const written = await saveProjectImage(import_node_path34.default.dirname(import_node_path34.default.resolve(manifestPath2)), upload.data);
16875
17457
  log(` image added: ${written}`);
16876
17458
  return opts.reload();
16877
17459
  }
@@ -16938,7 +17520,7 @@ function createEditApplier(opts, log) {
16938
17520
  }
16939
17521
  const result = await applyManifestEdit(manifestPath, parsed.data);
16940
17522
  if (result.changed) {
16941
- const relative = import_node_path33.default.relative(process.cwd(), result.manifestPath);
17523
+ const relative = import_node_path34.default.relative(process.cwd(), result.manifestPath);
16942
17524
  const shown = !relative || relative.startsWith("..") ? result.manifestPath : relative;
16943
17525
  log(` manifest edited: ${describeEdit(parsed.data)} in ${shown}`);
16944
17526
  }
@@ -16966,15 +17548,20 @@ function describeEdit(edit) {
16966
17548
  // src/gallery/snapshot.ts
16967
17549
  var PROJECT_FILE_TYPE = "application/octet-stream";
16968
17550
  function galleryMediaId(absolutePath) {
16969
- return (0, import_node_crypto7.createHash)("sha256").update(import_node_path34.default.resolve(absolutePath)).digest("hex").slice(0, 24);
17551
+ return (0, import_node_crypto7.createHash)("sha256").update(import_node_path35.default.resolve(absolutePath)).digest("hex").slice(0, 24);
16970
17552
  }
16971
17553
  function galleryMediaRoute(id) {
16972
17554
  return `/media/${id}`;
16973
17555
  }
17556
+ function renderedSize(entry) {
17557
+ const vfx = entry?.providerMetadata?.[entry.provider]?.vfx;
17558
+ return typeof vfx?.width === "number" && typeof vfx.height === "number" ? { width: vfx.width, height: vfx.height } : null;
17559
+ }
16974
17560
  function isDeclared(loaded, styleId, assetId) {
16975
17561
  const asset = loaded.manifest.assets[assetId];
16976
- if (!asset || !loaded.manifest.styles[styleId]) return false;
16977
- return asset.styles.length === 0 || asset.styles.includes(styleId);
17562
+ const style = loaded.manifest.styles[styleId];
17563
+ if (!asset || !style) return false;
17564
+ return assetInStyle(asset, styleId, style);
16978
17565
  }
16979
17566
  function matchesFilter(filter, styleId, assetId) {
16980
17567
  if (filter?.styles?.length && !filter.styles.includes(styleId)) return false;
@@ -16985,9 +17572,9 @@ function describeHistory(media, entry, cacheDir) {
16985
17572
  return (entry.history ?? []).map((generation, i) => {
16986
17573
  const outputs = generation.outputs.map((output) => {
16987
17574
  const mediaType = output.mediaType ?? MediaType.PNG;
16988
- const file = import_node_path34.default.join(cacheDir, cacheFileName(output.sha256, mediaType));
17575
+ const file = import_node_path35.default.join(cacheDir, cacheFileName(output.sha256, mediaType));
16989
17576
  let url = null;
16990
- if ((0, import_node_fs29.existsSync)(file)) {
17577
+ if ((0, import_node_fs30.existsSync)(file)) {
16991
17578
  const id = galleryMediaId(file);
16992
17579
  media.set(id, { path: file, contentType: mediaType });
16993
17580
  url = `${galleryMediaRoute(id)}?v=${output.sha256.slice(0, 24)}`;
@@ -17019,7 +17606,7 @@ function describeHistory(media, entry, cacheDir) {
17019
17606
  var MAX_COMPARED_BYTES = 8 * 1024 * 1024;
17020
17607
  async function samePixels(a, b) {
17021
17608
  try {
17022
- const [left, right] = await Promise.all([(0, import_promises26.readFile)(a), (0, import_promises26.readFile)(b)]);
17609
+ const [left, right] = await Promise.all([(0, import_promises27.readFile)(a), (0, import_promises27.readFile)(b)]);
17023
17610
  if (left.length > MAX_COMPARED_BYTES || right.length > MAX_COMPARED_BYTES) return false;
17024
17611
  const x = decodePng(left);
17025
17612
  const y = decodePng(right);
@@ -17085,7 +17672,7 @@ function describeCharacter(spec, entry) {
17085
17672
  }
17086
17673
  async function fileInfo(absolutePath) {
17087
17674
  try {
17088
- const info = await (0, import_promises26.stat)(absolutePath);
17675
+ const info = await (0, import_promises27.stat)(absolutePath);
17089
17676
  return info.isFile() ? { bytes: info.size, modifiedAt: info.mtime.toISOString() } : null;
17090
17677
  } catch {
17091
17678
  return null;
@@ -17117,10 +17704,10 @@ async function describeOutput(media, root, absolutePath, recorded = {}) {
17117
17704
  async function describeQuality(media, root, inspection) {
17118
17705
  const outputPaths = inspection.outputs ?? [inspection.output];
17119
17706
  const outputs = await Promise.all(
17120
- outputPaths.map((file) => describeOutput(media, root, import_node_path34.default.resolve(root, file)))
17707
+ outputPaths.map((file) => describeOutput(media, root, import_node_path35.default.resolve(root, file)))
17121
17708
  );
17122
- const recordPath = import_node_path34.default.resolve(root, inspection.record);
17123
- const recordExists = (0, import_node_fs29.existsSync)(recordPath);
17709
+ const recordPath = import_node_path35.default.resolve(root, inspection.record);
17710
+ const recordExists = (0, import_node_fs30.existsSync)(recordPath);
17124
17711
  let options = null;
17125
17712
  let check = null;
17126
17713
  if (recordExists) {
@@ -17139,7 +17726,7 @@ async function describeQuality(media, root, inspection) {
17139
17726
  return {
17140
17727
  state: inspection.state,
17141
17728
  reason: inspection.reason,
17142
- output: portableOutputPath(import_node_path34.default.resolve(root, inspection.output), root),
17729
+ output: portableOutputPath(import_node_path35.default.resolve(root, inspection.output), root),
17143
17730
  record: portableOutputPath(recordPath, root),
17144
17731
  recordExists,
17145
17732
  outputs,
@@ -17177,9 +17764,9 @@ async function buildGallerySnapshot(opts) {
17177
17764
  const media = /* @__PURE__ */ new Map();
17178
17765
  const plan = await buildPlan(specs, lock);
17179
17766
  const items = [];
17180
- const manifestText = await (0, import_promises26.readFile)(loaded.path, "utf8");
17767
+ const manifestText = await (0, import_promises27.readFile)(loaded.path, "utf8");
17181
17768
  const manifestSha256 = sha256(manifestText);
17182
- const cacheDir = import_node_path34.default.resolve(import_node_path34.default.dirname(opts.lockPath), ".pixelkiln", "cache");
17769
+ const cacheDir = import_node_path35.default.resolve(import_node_path35.default.dirname(opts.lockPath), ".pixelkiln", "cache");
17183
17770
  let rawStyles = {};
17184
17771
  try {
17185
17772
  const raw = JSON.parse(manifestText);
@@ -17203,12 +17790,12 @@ async function buildGallerySnapshot(opts) {
17203
17790
  )
17204
17791
  );
17205
17792
  if (spec.source) {
17206
- const editPath = import_node_path34.default.resolve(root, spec.source);
17793
+ const editPath = import_node_path35.default.resolve(root, spec.source);
17207
17794
  const memberCount = sourceIsStem(spec.source, entry, root) ? entry.outputs.length : 1;
17208
17795
  const editShas = await Promise.all(
17209
17796
  Array.from({ length: memberCount }, async (_, index) => {
17210
17797
  const file = sourceOutputPath(spec.source, entry, index, root);
17211
- return { file, sha256: (0, import_node_fs29.existsSync)(file) ? await sha256File(file) : null };
17798
+ return { file, sha256: (0, import_node_fs30.existsSync)(file) ? await sha256File(file) : null };
17212
17799
  })
17213
17800
  );
17214
17801
  edits = await Promise.all(editShas.map(({ file, sha256: editSha }) => describeOutput(media, root, file, { sha256: editSha })));
@@ -17216,7 +17803,7 @@ async function buildGallerySnapshot(opts) {
17216
17803
  const companion = await readHandEditCompanion(editPath);
17217
17804
  if (companion) {
17218
17805
  const projectPath = handEditProjectPath(editPath);
17219
- const project = companion.project && (0, import_node_fs29.existsSync)(projectPath) ? await fileInfo(projectPath) : null;
17806
+ const project = companion.project && (0, import_node_fs30.existsSync)(projectPath) ? await fileInfo(projectPath) : null;
17220
17807
  let projectUrl = null;
17221
17808
  if (project) {
17222
17809
  const id = galleryMediaId(projectPath);
@@ -17252,8 +17839,8 @@ async function buildGallerySnapshot(opts) {
17252
17839
  editStatus = edits.some((member) => !member.exists) ? "missing" : editChanged.every((changed) => !changed) ? "same" : regenerated ? "regenerated-since" : "edited";
17253
17840
  }
17254
17841
  } else if (spec.source) {
17255
- outputs = [await describeOutput(media, root, import_node_path34.default.resolve(root, spec.source))];
17256
- } else if (!entry && (0, import_node_fs29.existsSync)(spec.outFile)) {
17842
+ outputs = [await describeOutput(media, root, import_node_path35.default.resolve(root, spec.source))];
17843
+ } else if (!entry && (0, import_node_fs30.existsSync)(spec.outFile)) {
17257
17844
  outputs = [await describeOutput(media, root, spec.outFile)];
17258
17845
  } else {
17259
17846
  outputs = [];
@@ -17274,8 +17861,8 @@ async function buildGallerySnapshot(opts) {
17274
17861
  generator: entry?.generator ?? spec.generator,
17275
17862
  prompt: entry?.prompt ?? spec.prompt,
17276
17863
  currentPrompt: entry && entry.prompt !== spec.prompt ? spec.prompt : null,
17277
- width: entry?.width ?? spec.width,
17278
- height: entry?.height ?? spec.height,
17864
+ width: renderedSize(entry)?.width ?? entry?.width ?? spec.width,
17865
+ height: renderedSize(entry)?.height ?? entry?.height ?? spec.height,
17279
17866
  tileFeature: entry?.tileFeature ?? spec.tileFeature ?? null,
17280
17867
  fps: quality?.frameSet?.fps ?? metadataFps(entry) ?? spec.quality?.fps ?? null,
17281
17868
  cost: entry?.cost ?? 0,
@@ -17456,7 +18043,7 @@ async function buildGallerySnapshot(opts) {
17456
18043
  id: loaded.manifest.name,
17457
18044
  name: loaded.manifest.name,
17458
18045
  manifest: loaded.path,
17459
- lock: import_node_path34.default.resolve(opts.lockPath),
18046
+ lock: import_node_path35.default.resolve(opts.lockPath),
17460
18047
  root,
17461
18048
  provider: loaded.manifest.provider,
17462
18049
  account: null,
@@ -17482,7 +18069,7 @@ async function buildGallerySnapshot(opts) {
17482
18069
  return { snapshot, media };
17483
18070
  }
17484
18071
  async function buildWorkspaceGallerySnapshot(opts) {
17485
- const dir = import_node_path34.default.dirname(import_node_path34.default.resolve(opts.workspacePath));
18072
+ const dir = import_node_path35.default.dirname(import_node_path35.default.resolve(opts.workspacePath));
17486
18073
  const media = /* @__PURE__ */ new Map();
17487
18074
  const projects = [];
17488
18075
  const items = [];
@@ -17525,11 +18112,11 @@ async function buildWorkspaceGallerySnapshot(opts) {
17525
18112
  id: project.id,
17526
18113
  name: loaded.manifest.name,
17527
18114
  manifest: manifestPath,
17528
- lock: import_node_path34.default.resolve(lockPath),
18115
+ lock: import_node_path35.default.resolve(lockPath),
17529
18116
  root: loaded.root,
17530
18117
  provider: loaded.manifest.provider,
17531
18118
  account: project.account ?? null,
17532
- manifestSha256: manifestSha256 ?? sha256(await (0, import_promises26.readFile)(manifestPath, "utf8")),
18119
+ manifestSha256: manifestSha256 ?? sha256(await (0, import_promises27.readFile)(manifestPath, "utf8")),
17533
18120
  historyLimit: historyLimit(loaded.manifest),
17534
18121
  entries: Object.keys(lock.entries).length,
17535
18122
  items: projectItems.length,
@@ -17541,8 +18128,8 @@ async function buildWorkspaceGallerySnapshot(opts) {
17541
18128
  id: project.id,
17542
18129
  name: project.id,
17543
18130
  manifest: manifestPath,
17544
- lock: import_node_path34.default.resolve(lockPath),
17545
- root: import_node_path34.default.dirname(manifestPath),
18131
+ lock: import_node_path35.default.resolve(lockPath),
18132
+ root: import_node_path35.default.dirname(manifestPath),
17546
18133
  provider: project.provider,
17547
18134
  account: project.account ?? null,
17548
18135
  manifestSha256: null,
@@ -17569,7 +18156,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
17569
18156
  version: 1,
17570
18157
  generatedAt: (opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
17571
18158
  project: null,
17572
- workspace: { path: import_node_path34.default.resolve(opts.workspacePath), dir, projects },
18159
+ workspace: { path: import_node_path35.default.resolve(opts.workspacePath), dir, projects },
17573
18160
  filter,
17574
18161
  totals: {
17575
18162
  entries: projects.reduce((sum, project) => sum + project.entries, 0),
@@ -17586,7 +18173,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
17586
18173
  }
17587
18174
 
17588
18175
  // src/gallery/page.ts
17589
- var import_node_fs30 = require("fs");
18176
+ var import_node_fs31 = require("fs");
17590
18177
 
17591
18178
  // src/gallery/client-bundle.ts
17592
18179
  var import_node_module = require("module");
@@ -17626,8 +18213,8 @@ var client;
17626
18213
  function clientAssets() {
17627
18214
  const prebuilt = new URL("gallery.js", CLIENT_DIR);
17628
18215
  client ??= {
17629
- css: (0, import_node_fs30.readFileSync)(new URL("gallery.css", CLIENT_DIR), "utf8"),
17630
- js: (0, import_node_fs30.existsSync)(prebuilt) ? (0, import_node_fs30.readFileSync)(prebuilt, "utf8") : bundleGalleryClient()
18216
+ css: (0, import_node_fs31.readFileSync)(new URL("gallery.css", CLIENT_DIR), "utf8"),
18217
+ js: (0, import_node_fs31.existsSync)(prebuilt) ? (0, import_node_fs31.readFileSync)(prebuilt, "utf8") : bundleGalleryClient()
17631
18218
  };
17632
18219
  return client;
17633
18220
  }
@@ -17770,10 +18357,10 @@ var EDITOR_RELEASE_BASE = "https://github.com/gfargo/pixelkiln/releases/download
17770
18357
 
17771
18358
  // src/editor/install.ts
17772
18359
  var import_node_crypto8 = require("crypto");
17773
- var import_node_fs31 = require("fs");
17774
- var import_promises27 = require("fs/promises");
18360
+ var import_node_fs32 = require("fs");
18361
+ var import_promises28 = require("fs/promises");
17775
18362
  var import_node_os4 = __toESM(require("os"), 1);
17776
- var import_node_path35 = __toESM(require("path"), 1);
18363
+ var import_node_path36 = __toESM(require("path"), 1);
17777
18364
  var EditorInstallError = class extends Error {
17778
18365
  constructor(message6, file) {
17779
18366
  super(message6);
@@ -17784,15 +18371,15 @@ var EditorInstallError = class extends Error {
17784
18371
  };
17785
18372
  function editorToolsRoot() {
17786
18373
  const override = process.env.PIXELKILN_TOOLS_DIR?.trim();
17787
- if (override) return import_node_path35.default.resolve(override);
17788
- if (process.platform === "darwin") return import_node_path35.default.join(import_node_os4.default.homedir(), "Library", "Caches", "pixelkiln", "tools");
18374
+ if (override) return import_node_path36.default.resolve(override);
18375
+ if (process.platform === "darwin") return import_node_path36.default.join(import_node_os4.default.homedir(), "Library", "Caches", "pixelkiln", "tools");
17789
18376
  if (process.platform === "win32") {
17790
- return import_node_path35.default.join(process.env.LOCALAPPDATA ?? import_node_path35.default.join(import_node_os4.default.homedir(), "AppData", "Local"), "pixelkiln", "tools");
18377
+ return import_node_path36.default.join(process.env.LOCALAPPDATA ?? import_node_path36.default.join(import_node_os4.default.homedir(), "AppData", "Local"), "pixelkiln", "tools");
17791
18378
  }
17792
- return import_node_path35.default.join(process.env.XDG_CACHE_HOME ?? import_node_path35.default.join(import_node_os4.default.homedir(), ".cache"), "pixelkiln", "tools");
18379
+ return import_node_path36.default.join(process.env.XDG_CACHE_HOME ?? import_node_path36.default.join(import_node_os4.default.homedir(), ".cache"), "pixelkiln", "tools");
17793
18380
  }
17794
18381
  function editorDir(pin = EDITOR_PIN, root = editorToolsRoot()) {
17795
- return import_node_path35.default.join(root, "pixelorama", pin.release ?? "unpublished");
18382
+ return import_node_path36.default.join(root, "pixelorama", pin.release ?? "unpublished");
17796
18383
  }
17797
18384
  function editorBaseUrl(pin = EDITOR_PIN) {
17798
18385
  const override = process.env.PIXELKILN_EDITOR_URL?.trim();
@@ -17802,7 +18389,7 @@ function editorBaseUrl(pin = EDITOR_PIN) {
17802
18389
  }
17803
18390
  async function sha256Of(file) {
17804
18391
  try {
17805
- return (0, import_node_crypto8.createHash)("sha256").update(await (0, import_promises27.readFile)(file)).digest("hex");
18392
+ return (0, import_node_crypto8.createHash)("sha256").update(await (0, import_promises28.readFile)(file)).digest("hex");
17806
18393
  } catch {
17807
18394
  return null;
17808
18395
  }
@@ -17815,8 +18402,8 @@ async function editorStatus(opts = {}) {
17815
18402
  let totalBytes = 0;
17816
18403
  for (const [name, expected] of Object.entries(pin.files)) {
17817
18404
  totalBytes += expected.bytes;
17818
- const file = import_node_path35.default.join(dir, name);
17819
- const info = (0, import_node_fs31.existsSync)(file) ? await (0, import_promises27.stat)(file).catch(() => null) : null;
18405
+ const file = import_node_path36.default.join(dir, name);
18406
+ const info = (0, import_node_fs32.existsSync)(file) ? await (0, import_promises28.stat)(file).catch(() => null) : null;
17820
18407
  if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
17821
18408
  installedBytes += expected.bytes;
17822
18409
  } else {
@@ -17845,7 +18432,7 @@ async function installEditor(opts = {}) {
17845
18432
  );
17846
18433
  }
17847
18434
  if (!baseUrl) throw new EditorInstallError("no download location for the editor build");
17848
- await (0, import_promises27.mkdir)(dir, { recursive: true });
18435
+ await (0, import_promises28.mkdir)(dir, { recursive: true });
17849
18436
  const downloaded = [];
17850
18437
  const skipped = Object.keys(pin.files).filter((name) => !before.missing.includes(name));
17851
18438
  let fetchedBytes = before.installedBytes;
@@ -17873,13 +18460,13 @@ async function installEditor(opts = {}) {
17873
18460
  if (digest2 !== expected.sha256) {
17874
18461
  throw new EditorInstallError(`${name}: sha256 ${digest2.slice(0, 12)}\u2026 does not match the pinned ${expected.sha256.slice(0, 12)}\u2026`, name);
17875
18462
  }
17876
- const final = import_node_path35.default.join(dir, name);
18463
+ const final = import_node_path36.default.join(dir, name);
17877
18464
  const tmp = `${final}.${process.pid}.part`;
17878
18465
  try {
17879
- await (0, import_promises27.writeFile)(tmp, bytes);
17880
- await (0, import_promises27.rename)(tmp, final);
18466
+ await (0, import_promises28.writeFile)(tmp, bytes);
18467
+ await (0, import_promises28.rename)(tmp, final);
17881
18468
  } finally {
17882
- await (0, import_promises27.rm)(tmp, { force: true });
18469
+ await (0, import_promises28.rm)(tmp, { force: true });
17883
18470
  }
17884
18471
  fetchedBytes += bytes.length;
17885
18472
  downloaded.push(name);
@@ -17919,12 +18506,12 @@ var EDITOR_CONTENT_TYPES = {
17919
18506
  ".json": "application/json; charset=utf-8"
17920
18507
  };
17921
18508
  function editorContentType(name) {
17922
- return EDITOR_CONTENT_TYPES[import_node_path35.default.extname(name).toLowerCase()] ?? null;
18509
+ return EDITOR_CONTENT_TYPES[import_node_path36.default.extname(name).toLowerCase()] ?? null;
17923
18510
  }
17924
18511
 
17925
18512
  // src/gallery/editor.ts
17926
- var import_promises28 = require("fs/promises");
17927
- var import_node_path36 = __toESM(require("path"), 1);
18513
+ var import_promises29 = require("fs/promises");
18514
+ var import_node_path37 = __toESM(require("path"), 1);
17928
18515
  function editorRoute(release, name = "index.html") {
17929
18516
  return `/editor/${encodeURIComponent(release)}/${encodeURIComponent(name)}`;
17930
18517
  }
@@ -17997,7 +18584,7 @@ function createGalleryEditorHandlers(opts = {}) {
17997
18584
  if (!status.installed) return null;
17998
18585
  let bytes;
17999
18586
  try {
18000
- bytes = await (0, import_promises28.readFile)(import_node_path36.default.join(dir, name));
18587
+ bytes = await (0, import_promises29.readFile)(import_node_path37.default.join(dir, name));
18001
18588
  } catch {
18002
18589
  verified = null;
18003
18590
  return null;
@@ -18014,7 +18601,7 @@ function createGalleryEditorHandlers(opts = {}) {
18014
18601
  // src/gallery/server.ts
18015
18602
  var import_node_crypto9 = require("crypto");
18016
18603
  var import_node_http2 = require("http");
18017
- var import_promises29 = require("fs/promises");
18604
+ var import_promises30 = require("fs/promises");
18018
18605
  var MAX_EDIT_BYTES = 48 * 1024 * 1024;
18019
18606
  var EDITOR_CSP = [
18020
18607
  "default-src 'self'",
@@ -18195,7 +18782,7 @@ async function serveGallery(opts) {
18195
18782
  const asset = reviewAssets.get(jobId)?.get(url2.pathname);
18196
18783
  if (!asset) return fail(404, "no such review asset");
18197
18784
  try {
18198
- const bytes = await (0, import_promises29.readFile)(asset.path);
18785
+ const bytes = await (0, import_promises30.readFile)(asset.path);
18199
18786
  res.writeHead(200, {
18200
18787
  "Content-Type": asset.contentType,
18201
18788
  "Cache-Control": "no-store",
@@ -18278,7 +18865,7 @@ async function serveGallery(opts) {
18278
18865
  if (!asset) return fail(404, "no such gallery media");
18279
18866
  let bytes;
18280
18867
  try {
18281
- bytes = await (0, import_promises29.readFile)(asset.path);
18868
+ bytes = await (0, import_promises30.readFile)(asset.path);
18282
18869
  } catch {
18283
18870
  return fail(404, "gallery media is no longer on disk; refresh the page");
18284
18871
  }
@@ -18688,19 +19275,19 @@ function createGenerateHandlers(opts) {
18688
19275
  }
18689
19276
 
18690
19277
  // src/pipeline/salvage.ts
18691
- var import_promises30 = require("fs/promises");
18692
- var import_node_fs32 = require("fs");
18693
- var import_node_path37 = __toESM(require("path"), 1);
19278
+ var import_promises31 = require("fs/promises");
19279
+ var import_node_fs33 = require("fs");
19280
+ var import_node_path38 = __toESM(require("path"), 1);
18694
19281
  function providerClaimId(provider, objectId) {
18695
19282
  return `${encodeURIComponent(provider)}:${encodeURIComponent(objectId)}`;
18696
19283
  }
18697
19284
  async function loadClaims(lockPaths, opts = {}) {
18698
19285
  const claimed = /* @__PURE__ */ new Set();
18699
19286
  for (const p of lockPaths) {
18700
- if (!(0, import_node_fs32.existsSync)(p)) throw new Error(`Claim lockfile not found: ${p}`);
19287
+ if (!(0, import_node_fs33.existsSync)(p)) throw new Error(`Claim lockfile not found: ${p}`);
18701
19288
  let parsed;
18702
19289
  try {
18703
- parsed = parseLock(JSON.parse(await (0, import_promises30.readFile)(p, "utf8")));
19290
+ parsed = parseLock(JSON.parse(await (0, import_promises31.readFile)(p, "utf8")));
18704
19291
  } catch {
18705
19292
  throw new Error(`Claim lockfile is malformed: ${p}`);
18706
19293
  }
@@ -18804,20 +19391,20 @@ function matchOrphanStyle(prompt, manifest) {
18804
19391
  return matchStyleByPattern(prompt, manifest);
18805
19392
  }
18806
19393
  async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, claimPaths) {
18807
- const own = import_node_path37.default.resolve(ownManifestPath);
19394
+ const own = import_node_path38.default.resolve(ownManifestPath);
18808
19395
  const siblingManifestPaths = [
18809
19396
  .../* @__PURE__ */ new Set([
18810
19397
  ...workspaceManifestPaths,
18811
- ...claimPaths.map((c) => import_node_path37.default.join(import_node_path37.default.dirname(import_node_path37.default.resolve(c)), "pixelkiln.manifest.json"))
19398
+ ...claimPaths.map((c) => import_node_path38.default.join(import_node_path38.default.dirname(import_node_path38.default.resolve(c)), "pixelkiln.manifest.json"))
18812
19399
  ])
18813
19400
  ];
18814
19401
  const siblings = [];
18815
19402
  for (const siblingManifestPath of siblingManifestPaths) {
18816
- if (import_node_path37.default.resolve(siblingManifestPath) === own) continue;
18817
- if (!(0, import_node_fs32.existsSync)(siblingManifestPath)) continue;
19403
+ if (import_node_path38.default.resolve(siblingManifestPath) === own) continue;
19404
+ if (!(0, import_node_fs33.existsSync)(siblingManifestPath)) continue;
18818
19405
  try {
18819
19406
  const { manifest } = await loadManifest(siblingManifestPath);
18820
- siblings.push({ label: import_node_path37.default.basename(import_node_path37.default.dirname(siblingManifestPath)), manifest });
19407
+ siblings.push({ label: import_node_path38.default.basename(import_node_path38.default.dirname(siblingManifestPath)), manifest });
18821
19408
  } catch {
18822
19409
  }
18823
19410
  }
@@ -18937,8 +19524,8 @@ async function applyTags(provider, decisions, existing, opts = {}) {
18937
19524
  }
18938
19525
 
18939
19526
  // src/pick/salvage-server.ts
18940
- var import_promises31 = require("fs/promises");
18941
- var import_node_path38 = __toESM(require("path"), 1);
19527
+ var import_promises32 = require("fs/promises");
19528
+ var import_node_path39 = __toESM(require("path"), 1);
18942
19529
 
18943
19530
  // src/pick/salvage-sheet.ts
18944
19531
  var escapeHtml3 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
@@ -19104,7 +19691,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
19104
19691
  });
19105
19692
  const html = renderSalvageSheet(orphans, {
19106
19693
  styleId: ctx.styleId,
19107
- importDir: import_node_path38.default.relative(process.cwd(), ctx.importDir) || "."
19694
+ importDir: import_node_path39.default.relative(process.cwd(), ctx.importDir) || "."
19108
19695
  });
19109
19696
  const byId = new Map(orphans.map((o) => [o.id, o]));
19110
19697
  const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
@@ -19160,10 +19747,10 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
19160
19747
  if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
19161
19748
  decodePng(buf);
19162
19749
  const assetId = idFromPrompt(orphan.prompt, taken);
19163
- const rel = import_node_path38.default.join("_salvaged", `${assetId}.png`);
19164
- const outFile = import_node_path38.default.resolve(ctx.importDir, rel);
19165
- await (0, import_promises31.mkdir)(import_node_path38.default.dirname(outFile), { recursive: true });
19166
- await (0, import_promises31.writeFile)(outFile, buf);
19750
+ const rel = import_node_path39.default.join("_salvaged", `${assetId}.png`);
19751
+ const outFile = import_node_path39.default.resolve(ctx.importDir, rel);
19752
+ await (0, import_promises32.mkdir)(import_node_path39.default.dirname(outFile), { recursive: true });
19753
+ await (0, import_promises32.writeFile)(outFile, buf);
19167
19754
  ctx.manifest.assets[assetId] = {
19168
19755
  prompt: orphan.prompt,
19169
19756
  promptByStyle: {},
@@ -19195,7 +19782,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
19195
19782
  sourceUrl: durableSource,
19196
19783
  sourceUrls: durableSource ? [{ url: durableSource }] : [],
19197
19784
  outputs: [{
19198
- path: portableOutputPath(outFile, import_node_path38.default.dirname(ctx.manifestPath)),
19785
+ path: portableOutputPath(outFile, import_node_path39.default.dirname(ctx.manifestPath)),
19199
19786
  sha256: sha256(buf)
19200
19787
  }],
19201
19788
  submittedAt: orphan.createdAt,
@@ -19219,9 +19806,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
19219
19806
  }
19220
19807
  }
19221
19808
  await applyTags(provider, decisions, existingTags, { onProgress: log, characters: characterMembers });
19222
- const raw = JSON.parse(await (0, import_promises31.readFile)(ctx.manifestPath, "utf8"));
19809
+ const raw = JSON.parse(await (0, import_promises32.readFile)(ctx.manifestPath, "utf8"));
19223
19810
  for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
19224
- await (0, import_promises31.writeFile)(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
19811
+ await (0, import_promises32.writeFile)(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
19225
19812
  await saveLock(ctx.lockPath, ctx.lock);
19226
19813
  if (characterAssetIds.length) result.characterAssetIds = characterAssetIds;
19227
19814
  return result;
@@ -19332,6 +19919,7 @@ async function workspaceStatus(ws, dir) {
19332
19919
  0 && (module.exports = {
19333
19920
  AssetBatchSchema,
19334
19921
  AssetSchema,
19922
+ AssetVfxSchema,
19335
19923
  BudgetError,
19336
19924
  CHARACTER_DIRECTIONS_4,
19337
19925
  CHARACTER_DIRECTIONS_8,
@@ -19388,12 +19976,14 @@ async function workspaceStatus(ws, dir) {
19388
19976
  OverwriteRefusedError,
19389
19977
  PIXELATE_COST_GENERATIONS,
19390
19978
  PIXELATE_MAX_SIDE,
19979
+ PIXELLAB_MCP_URL,
19391
19980
  PIXEL_ART_FIXER_REVISION,
19392
19981
  PIXEL_ART_FIXER_URL,
19393
19982
  PRO_TIER_PRICES,
19394
19983
  PixelKilnError,
19395
19984
  PixelLabClient,
19396
19985
  PixelLabError,
19986
+ PixelLabMcpClient,
19397
19987
  PixelLabProvider,
19398
19988
  PostprocessSchema,
19399
19989
  ProjectError,
@@ -19416,12 +20006,15 @@ async function workspaceStatus(ws, dir) {
19416
20006
  UiPieceSchema,
19417
20007
  UnsupportedCapabilityError,
19418
20008
  UsageError,
20009
+ VFX_TOOL,
20010
+ VfxPartSchema,
19419
20011
  WorkspaceProjectSchema,
19420
20012
  WorkspaceSchema,
19421
20013
  adopt,
19422
20014
  applyManifestEdit,
19423
20015
  applyTags,
19424
20016
  approveQualityRecord,
20017
+ assetInStyle,
19425
20018
  auditStyle,
19426
20019
  availableProviders,
19427
20020
  backoffMs,
@@ -19448,6 +20041,7 @@ async function workspaceStatus(ws, dir) {
19448
20041
  defaultOpenCommand,
19449
20042
  defaultPixelateOut,
19450
20043
  defaultUnzoomOut,
20044
+ describeVfx,
19451
20045
  detachHandEdit,
19452
20046
  detectMediaType,
19453
20047
  doctor,
@@ -19468,6 +20062,7 @@ async function workspaceStatus(ws, dir) {
19468
20062
  fontOutputPaths,
19469
20063
  formatCost,
19470
20064
  frameSetFps,
20065
+ frameSetLoop,
19471
20066
  galleryContentSecurityPolicy,
19472
20067
  galleryMediaId,
19473
20068
  galleryMediaRoute,
@@ -19490,9 +20085,11 @@ async function workspaceStatus(ws, dir) {
19490
20085
  inspectRevisionReadiness,
19491
20086
  installEditor,
19492
20087
  installRecipe,
20088
+ isBusyReply,
19493
20089
  isFrameSetEntry,
19494
20090
  isMemberSetEntry,
19495
20091
  isSensitiveSourceUrl,
20092
+ isVfxReference,
19496
20093
  isometricTileCost,
19497
20094
  listBundledRecipes,
19498
20095
  loadClaims,
@@ -19501,7 +20098,9 @@ async function workspaceStatus(ws, dir) {
19501
20098
  loadSiblingManifests,
19502
20099
  loadWorkspace,
19503
20100
  lockKey,
20101
+ makeVfx,
19504
20102
  matchOrphanStyle,
20103
+ mcpClientFromEnv,
19505
20104
  measureBalanceChange,
19506
20105
  measureDecodedImageQuality,
19507
20106
  measureImageQuality,
@@ -19525,8 +20124,12 @@ async function workspaceStatus(ws, dir) {
19525
20124
  paletteDistance,
19526
20125
  parentAssetId,
19527
20126
  parseLock,
20127
+ parseMcpBody,
19528
20128
  parseProFlashQuote,
19529
20129
  parseTerrainDescriptions,
20130
+ parseVfxExport,
20131
+ parseVfxRecipe,
20132
+ parseVfxStep,
19530
20133
  parseWorkspace,
19531
20134
  pickHistory,
19532
20135
  pixelateFile,
@@ -19546,6 +20149,8 @@ async function workspaceStatus(ws, dir) {
19546
20149
  readArtifactBundleManifest,
19547
20150
  readHandEditCompanion,
19548
20151
  readQualityBaseline,
20152
+ readSavedVfxRecipe,
20153
+ readVfxRecipe,
19549
20154
  recipeDigest,
19550
20155
  referencedHashes,
19551
20156
  refineAsset,
@@ -19576,6 +20181,7 @@ async function workspaceStatus(ws, dir) {
19576
20181
  resolveSpecs,
19577
20182
  resolveStyleImages,
19578
20183
  resolveStyleOutputs,
20184
+ resolveVfxSprite,
19579
20185
  resumeActions,
19580
20186
  resumeCommandForStatus,
19581
20187
  retireGeneration,
@@ -19583,6 +20189,7 @@ async function workspaceStatus(ws, dir) {
19583
20189
  revertGeneration,
19584
20190
  runPicker,
19585
20191
  runSalvage,
20192
+ runVfxWorkbench,
19586
20193
  saveHandEdit,
19587
20194
  saveLock,
19588
20195
  saveWorkspace,
@@ -19593,6 +20200,7 @@ async function workspaceStatus(ws, dir) {
19593
20200
  sha256File,
19594
20201
  shouldPersistSourceUrl,
19595
20202
  shouldRetry,
20203
+ sliceVfxSheet,
19596
20204
  slugify,
19597
20205
  snapshotQualityBaseline,
19598
20206
  sourceIsStem,
@@ -19617,6 +20225,8 @@ async function workspaceStatus(ws, dir) {
19617
20225
  validateWorkspace,
19618
20226
  verifyArtifactBundle,
19619
20227
  verifyRecipe,
20228
+ vfxOutputPaths,
20229
+ vfxRecipeSha256,
19620
20230
  withArtifactManifest,
19621
20231
  workspaceClaims,
19622
20232
  workspaceStatus,