pixelkiln 0.86.3 → 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,
@@ -86,11 +87,16 @@ __export(index_exports, {
86
87
  MapSceneSchema: () => MapSceneSchema,
87
88
  MediaType: () => MediaType,
88
89
  OverwriteRefusedError: () => OverwriteRefusedError,
90
+ PIXELATE_COST_GENERATIONS: () => PIXELATE_COST_GENERATIONS,
91
+ PIXELATE_MAX_SIDE: () => PIXELATE_MAX_SIDE,
92
+ PIXELLAB_MCP_URL: () => PIXELLAB_MCP_URL,
89
93
  PIXEL_ART_FIXER_REVISION: () => PIXEL_ART_FIXER_REVISION,
90
94
  PIXEL_ART_FIXER_URL: () => PIXEL_ART_FIXER_URL,
95
+ PRO_TIER_PRICES: () => PRO_TIER_PRICES,
91
96
  PixelKilnError: () => PixelKilnError,
92
97
  PixelLabClient: () => PixelLabClient,
93
98
  PixelLabError: () => PixelLabError,
99
+ PixelLabMcpClient: () => PixelLabMcpClient,
94
100
  PixelLabProvider: () => PixelLabProvider,
95
101
  PostprocessSchema: () => PostprocessSchema,
96
102
  ProjectError: () => ProjectError,
@@ -113,12 +119,15 @@ __export(index_exports, {
113
119
  UiPieceSchema: () => UiPieceSchema,
114
120
  UnsupportedCapabilityError: () => UnsupportedCapabilityError,
115
121
  UsageError: () => UsageError,
122
+ VFX_TOOL: () => VFX_TOOL,
123
+ VfxPartSchema: () => VfxPartSchema,
116
124
  WorkspaceProjectSchema: () => WorkspaceProjectSchema,
117
125
  WorkspaceSchema: () => WorkspaceSchema,
118
126
  adopt: () => adopt,
119
127
  applyManifestEdit: () => applyManifestEdit,
120
128
  applyTags: () => applyTags,
121
129
  approveQualityRecord: () => approveQualityRecord,
130
+ assetInStyle: () => assetInStyle,
122
131
  auditStyle: () => auditStyle,
123
132
  availableProviders: () => availableProviders,
124
133
  backoffMs: () => backoffMs,
@@ -143,7 +152,9 @@ __export(index_exports, {
143
152
  currentEntryOutputPath: () => currentEntryOutputPath,
144
153
  currentOutputPath: () => currentOutputPath,
145
154
  defaultOpenCommand: () => defaultOpenCommand,
155
+ defaultPixelateOut: () => defaultPixelateOut,
146
156
  defaultUnzoomOut: () => defaultUnzoomOut,
157
+ describeVfx: () => describeVfx,
147
158
  detachHandEdit: () => detachHandEdit,
148
159
  detectMediaType: () => detectMediaType,
149
160
  doctor: () => doctor,
@@ -164,6 +175,7 @@ __export(index_exports, {
164
175
  fontOutputPaths: () => fontOutputPaths,
165
176
  formatCost: () => formatCost,
166
177
  frameSetFps: () => frameSetFps,
178
+ frameSetLoop: () => frameSetLoop,
167
179
  galleryContentSecurityPolicy: () => galleryContentSecurityPolicy,
168
180
  galleryMediaId: () => galleryMediaId,
169
181
  galleryMediaRoute: () => galleryMediaRoute,
@@ -186,9 +198,11 @@ __export(index_exports, {
186
198
  inspectRevisionReadiness: () => inspectRevisionReadiness,
187
199
  installEditor: () => installEditor,
188
200
  installRecipe: () => installRecipe,
201
+ isBusyReply: () => isBusyReply,
189
202
  isFrameSetEntry: () => isFrameSetEntry,
190
203
  isMemberSetEntry: () => isMemberSetEntry,
191
204
  isSensitiveSourceUrl: () => isSensitiveSourceUrl,
205
+ isVfxReference: () => isVfxReference,
192
206
  isometricTileCost: () => isometricTileCost,
193
207
  listBundledRecipes: () => listBundledRecipes,
194
208
  loadClaims: () => loadClaims,
@@ -197,7 +211,9 @@ __export(index_exports, {
197
211
  loadSiblingManifests: () => loadSiblingManifests,
198
212
  loadWorkspace: () => loadWorkspace,
199
213
  lockKey: () => lockKey,
214
+ makeVfx: () => makeVfx,
200
215
  matchOrphanStyle: () => matchOrphanStyle,
216
+ mcpClientFromEnv: () => mcpClientFromEnv,
201
217
  measureBalanceChange: () => measureBalanceChange,
202
218
  measureDecodedImageQuality: () => measureDecodedImageQuality,
203
219
  measureImageQuality: () => measureImageQuality,
@@ -221,15 +237,21 @@ __export(index_exports, {
221
237
  paletteDistance: () => paletteDistance,
222
238
  parentAssetId: () => parentAssetId,
223
239
  parseLock: () => parseLock,
240
+ parseMcpBody: () => parseMcpBody,
224
241
  parseProFlashQuote: () => parseProFlashQuote,
225
242
  parseTerrainDescriptions: () => parseTerrainDescriptions,
243
+ parseVfxExport: () => parseVfxExport,
244
+ parseVfxRecipe: () => parseVfxRecipe,
245
+ parseVfxStep: () => parseVfxStep,
226
246
  parseWorkspace: () => parseWorkspace,
227
247
  pickHistory: () => pickHistory,
248
+ pixelateFile: () => pixelateFile,
228
249
  pngSize: () => pngSize,
229
250
  poll: () => poll,
230
251
  portableOutputPath: () => portableOutputPath,
231
252
  prepareReview: () => prepareReview,
232
253
  primaryOutput: () => primaryOutput,
254
+ proTierCost: () => proTierCost,
233
255
  proportionsPayload: () => proportionsPayload,
234
256
  providerCredentialEnvs: () => providerCredentialEnvs,
235
257
  providerFactory: () => providerFactory,
@@ -240,6 +262,8 @@ __export(index_exports, {
240
262
  readArtifactBundleManifest: () => readArtifactBundleManifest,
241
263
  readHandEditCompanion: () => readHandEditCompanion,
242
264
  readQualityBaseline: () => readQualityBaseline,
265
+ readSavedVfxRecipe: () => readSavedVfxRecipe,
266
+ readVfxRecipe: () => readVfxRecipe,
243
267
  recipeDigest: () => recipeDigest,
244
268
  referencedHashes: () => referencedHashes,
245
269
  refineAsset: () => refineAsset,
@@ -270,6 +294,7 @@ __export(index_exports, {
270
294
  resolveSpecs: () => resolveSpecs,
271
295
  resolveStyleImages: () => resolveStyleImages,
272
296
  resolveStyleOutputs: () => resolveStyleOutputs,
297
+ resolveVfxSprite: () => resolveVfxSprite,
273
298
  resumeActions: () => resumeActions,
274
299
  resumeCommandForStatus: () => resumeCommandForStatus,
275
300
  retireGeneration: () => retireGeneration,
@@ -277,6 +302,7 @@ __export(index_exports, {
277
302
  revertGeneration: () => revertGeneration,
278
303
  runPicker: () => runPicker,
279
304
  runSalvage: () => runSalvage,
305
+ runVfxWorkbench: () => runVfxWorkbench,
280
306
  saveHandEdit: () => saveHandEdit,
281
307
  saveLock: () => saveLock,
282
308
  saveWorkspace: () => saveWorkspace,
@@ -287,6 +313,7 @@ __export(index_exports, {
287
313
  sha256File: () => sha256File,
288
314
  shouldPersistSourceUrl: () => shouldPersistSourceUrl,
289
315
  shouldRetry: () => shouldRetry,
316
+ sliceVfxSheet: () => sliceVfxSheet,
290
317
  slugify: () => slugify,
291
318
  snapshotQualityBaseline: () => snapshotQualityBaseline,
292
319
  sourceIsStem: () => sourceIsStem,
@@ -311,6 +338,8 @@ __export(index_exports, {
311
338
  validateWorkspace: () => validateWorkspace,
312
339
  verifyArtifactBundle: () => verifyArtifactBundle,
313
340
  verifyRecipe: () => verifyRecipe,
341
+ vfxOutputPaths: () => vfxOutputPaths,
342
+ vfxRecipeSha256: () => vfxRecipeSha256,
314
343
  withArtifactManifest: () => withArtifactManifest,
315
344
  workspaceClaims: () => workspaceClaims,
316
345
  workspaceStatus: () => workspaceStatus,
@@ -326,7 +355,7 @@ var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
326
355
  // src/types.ts
327
356
  var import_zod = require("zod");
328
357
  var MediaTypeSchema = import_zod.z.enum(["image/png", "image/gif"]);
329
- 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"]);
330
359
  var GridConfidenceSchema = import_zod.z.enum(["low", "medium", "high"]);
331
360
  var RevisionModeSchema = import_zod.z.enum([
332
361
  "image-to-image",
@@ -359,18 +388,26 @@ function candidateCount(size) {
359
388
  return 1;
360
389
  }
361
390
  function generationCost(width, height, generator = "map") {
391
+ if (generator === "vfx") return 0;
362
392
  if (generator === "map" || generator === "pixflux") return 1;
363
- if (generator === "imagePro") return 40;
393
+ if (generator === "imagePro") return proTierCost(width, height);
364
394
  const px = width * height;
365
- if (px <= 1024) return 20;
366
- if (px <= 2048) return 25;
367
- return 40;
395
+ if (px <= 1024) return PRO_TIER_PRICES[0];
396
+ if (px <= 2048) return PRO_TIER_PRICES[1];
397
+ return PRO_TIER_PRICES[2];
398
+ }
399
+ var PRO_TIER_PRICES = [10, 15, 25];
400
+ function proTierCost(width, height) {
401
+ const px = width * height;
402
+ if (px <= 65536) return PRO_TIER_PRICES[0];
403
+ if (px <= 102400) return PRO_TIER_PRICES[1];
404
+ return PRO_TIER_PRICES[2];
368
405
  }
369
406
  function tilesCost(tileSize, variations) {
370
407
  const px = tileSize * tileSize * Math.max(1, variations);
371
- if (px <= 1024) return 20;
372
- if (px <= 2048) return 25;
373
- return 40;
408
+ if (px <= 1024) return PRO_TIER_PRICES[0];
409
+ if (px <= 2048) return PRO_TIER_PRICES[1];
410
+ return PRO_TIER_PRICES[2];
374
411
  }
375
412
  function isometricTileCost() {
376
413
  return 1;
@@ -536,6 +573,29 @@ var MapSceneSchema = import_zod.z.object({
536
573
  image: import_zod.z.string().min(1),
537
574
  placement: MapScenePlacementSchema.default({ oval: 0.3 })
538
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
+ });
539
599
  var CharacterOutfitSchema = import_zod.z.object({
540
600
  /** The loop (a character animation asset) to re-clothe, in the same style. */
541
601
  of: import_zod.z.string().min(1),
@@ -886,7 +946,7 @@ var MEMBER_SET_REVISION_MODES = ["reduce-colors", "correct-pixelart", "edit-anim
886
946
  var StyleObjectSchema = import_zod.z.object({
887
947
  /** Generation backend for this style. Omit to inherit the manifest default. */
888
948
  provider: import_zod.z.string().min(1).optional(),
889
- // `map` is the default because it is 20-40x cheaper and correct for any
949
+ // `map` is the default because it is 10-25x cheaper and correct for any
890
950
  // asset that is not going to be rotated or animated.
891
951
  generator: GeneratorSchema.default("map"),
892
952
  /** Square edge length. The active provider owns its exact limit. */
@@ -1169,7 +1229,7 @@ var StyleSchema = StyleObjectSchema.refine((s) => !(s.tileFeature && s.styleImag
1169
1229
  message: "tileFeature and styleImages cannot be combined; a connectable set derives its own tile geometry, so remove one or the other",
1170
1230
  path: ["tileFeature"]
1171
1231
  }).superRefine((style, ctx) => {
1172
- 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")) {
1173
1233
  ctx.addIssue({
1174
1234
  code: import_zod.z.ZodIssueCode.custom,
1175
1235
  message: "quality profiles currently support single-image generators only",
@@ -1299,6 +1359,8 @@ var AssetSchema = import_zod.z.object({
1299
1359
  outfit: CharacterOutfitSchema.optional(),
1300
1360
  /** `map` styles only: draw this object in the style of a scene image. See `MapSceneSchema`. */
1301
1361
  scene: MapSceneSchema.optional(),
1362
+ /** `vfx` styles only: the effect to render. See `AssetVfxSchema`. */
1363
+ vfx: AssetVfxSchema.optional(),
1302
1364
  /** `character` styles, `standard` humanoid bases: this character's proportions, over the style's. */
1303
1365
  proportions: CharacterProportionsSchema.optional(),
1304
1366
  /**
@@ -1375,7 +1437,7 @@ var AssetSchema = import_zod.z.object({
1375
1437
  path: ["revision"]
1376
1438
  });
1377
1439
  }
1378
- 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);
1379
1441
  if (shapes.length > 1) {
1380
1442
  context.addIssue({
1381
1443
  code: import_zod.z.ZodIssueCode.custom,
@@ -1390,7 +1452,7 @@ var AssetSchema = import_zod.z.object({
1390
1452
  path: ["mirror"]
1391
1453
  });
1392
1454
  }
1393
- 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) {
1394
1456
  context.addIssue({
1395
1457
  code: import_zod.z.ZodIssueCode.custom,
1396
1458
  message: "Required",
@@ -2150,9 +2212,9 @@ var PixelLabClient = class {
2150
2212
  apiKey;
2151
2213
  http;
2152
2214
  /** Retries transport failures, 429, and 5xx; see http.ts for the policy. */
2153
- async request(path38, init) {
2215
+ async request(path39, init) {
2154
2216
  const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
2155
- const res = await this.http(`${BASE}${path38}`, {
2217
+ const res = await this.http(`${BASE}${path39}`, {
2156
2218
  ...init,
2157
2219
  headers: {
2158
2220
  Authorization: auth,
@@ -2162,13 +2224,13 @@ var PixelLabClient = class {
2162
2224
  });
2163
2225
  const text = await res.text();
2164
2226
  if (!res.ok) {
2165
- 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);
2166
2228
  }
2167
2229
  if (!text) return {};
2168
2230
  try {
2169
2231
  return JSON.parse(text);
2170
2232
  } catch {
2171
- throw new Error(`${init?.method ?? "GET"} ${path38} returned invalid JSON`);
2233
+ throw new Error(`${init?.method ?? "GET"} ${path39} returned invalid JSON`);
2172
2234
  }
2173
2235
  }
2174
2236
  async balance() {
@@ -2573,10 +2635,10 @@ var PixelLabClient = class {
2573
2635
  const palette = args.paletteSwatchBase64 ? { color_image: { type: "base64", base64: args.paletteSwatchBase64, format: "png" }, force_colors: true } : {};
2574
2636
  const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
2575
2637
  const south = args.reference?.south;
2576
- let path38;
2638
+ let path39;
2577
2639
  let body;
2578
2640
  if (args.mode === "v3") {
2579
- path38 = "/create-character-v3";
2641
+ path39 = "/create-character-v3";
2580
2642
  body = {
2581
2643
  description: args.description,
2582
2644
  image_size: imageSize,
@@ -2590,7 +2652,7 @@ var PixelLabClient = class {
2590
2652
  ...args.enhancePrompt ? { enhance_prompt: true } : {}
2591
2653
  };
2592
2654
  } else if (args.mode === "pro-flash") {
2593
- path38 = "/create-character-pro-flash";
2655
+ path39 = "/create-character-pro-flash";
2594
2656
  const traits = args.styleTraits;
2595
2657
  body = {
2596
2658
  description: args.description,
@@ -2614,7 +2676,7 @@ var PixelLabClient = class {
2614
2676
  } : {}
2615
2677
  };
2616
2678
  } else if (args.mode === "pro") {
2617
- path38 = "/create-character-pro";
2679
+ path39 = "/create-character-pro";
2618
2680
  body = {
2619
2681
  description: args.description,
2620
2682
  image_size: imageSize,
@@ -2628,7 +2690,7 @@ var PixelLabClient = class {
2628
2690
  ...!south && args.styleCharacterId ? { style_character_id: args.styleCharacterId } : {}
2629
2691
  };
2630
2692
  } else {
2631
- 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";
2632
2694
  body = {
2633
2695
  description: args.description,
2634
2696
  image_size: imageSize,
@@ -2645,8 +2707,8 @@ var PixelLabClient = class {
2645
2707
  ...palette
2646
2708
  };
2647
2709
  }
2648
- const raw = await this.request(path38, { method: "POST", body: JSON.stringify(body) });
2649
- 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));
2650
2712
  }
2651
2713
  /** A text edit of an existing character, applied to every direction and stored as a sibling. */
2652
2714
  async createCharacterState(args) {
@@ -2709,7 +2771,8 @@ var PixelLabClient = class {
2709
2771
  * A full-body sprite in, a bust portrait out (or the reverse, unused
2710
2772
  * today). Job-based like a state or `1dir`, not the couple-generations
2711
2773
  * "conversion" the name suggests: the smallest `resultSize` (16px) billed
2712
- * 20 generations live (docs/ENDPOINTS.md, "Characters, measured").
2774
+ * 20 generations live (docs/ENDPOINTS.md, "Characters, measured"), the
2775
+ * Pro floor before PixelLab's October 2026 cut put it at 10.
2713
2776
  */
2714
2777
  async createPortraitCharacterPro(args) {
2715
2778
  const raw = await this.request("/portrait-character-pro", {
@@ -2748,7 +2811,8 @@ var PixelLabClient = class {
2748
2811
  * everything else here, but the completed job returns the result frames
2749
2812
  * inline as base64 (`last_response.quantized_images`), not as storage
2750
2813
  * URLs — the one PixelLab character tool that works this way. A live
2751
- * 2-frame, 92x92 job billed 20 generations (docs/ENDPOINTS.md).
2814
+ * 2-frame, 92x92 job billed 20 generations (docs/ENDPOINTS.md), before
2815
+ * PixelLab's October 2026 Pro cut halved that floor to 10.
2752
2816
  */
2753
2817
  async transferOutfitV2(args) {
2754
2818
  const image = (img) => ({
@@ -2798,7 +2862,9 @@ var PixelLabClient = class {
2798
2862
  * far higher than this tiering assumes; 352x352 (123904px²), 384x384
2799
2863
  * (147456px²), and 512x512 (262144px², the tiering ceiling) all billed
2800
2864
  * 40. Both breakpoints are now tightly bracketed: 20->25 in
2801
- * (65536px², 82944px²], 25->40 in (102400px², 123904px²].
2865
+ * (65536px², 82944px²], 25->40 in (102400px², 123904px²]. Those prices
2866
+ * predate PixelLab's October 2026 Pro cut to 10/15/25; `proTierCost`
2867
+ * keeps the breakpoints with the new prices.
2802
2868
  * `editImagesV2` below returns the same fields under `images`, plural
2803
2869
  * and array-wrapped, not `image` — do not assume the two endpoints share
2804
2870
  * one response shape. See docs/ENDPOINTS.md and docs/REVISIONS.md for
@@ -2851,8 +2917,9 @@ var PixelLabClient = class {
2851
2917
  );
2852
2918
  }
2853
2919
  /**
2854
- * PixelLab's Pro image tier, `/generate-image-v2`: a flat 40 generations
2855
- * (docs/ENDPOINTS.md, "Single-image generators, measured") for real style
2920
+ * PixelLab's Pro image tier, `/generate-image-v2`: 10 to 25 generations by
2921
+ * canvas since PixelLab's October 2026 Pro cut, a flat 40 before
2922
+ * (docs/ENDPOINTS.md, "Single-image generators, measured"), for real style
2856
2923
  * transfer and non-square canvases up to 792x688, where `pixflux` is
2857
2924
  * limited to 400x400 and no style reference. Like a revision, this hands
2858
2925
  * back a plain background job with no resource of its own; unlike a
@@ -2938,6 +3005,24 @@ var PixelLabClient = class {
2938
3005
  "inpaint-image-pro-flash"
2939
3006
  );
2940
3007
  }
3008
+ /**
3009
+ * `/image-to-pixelart-pro-flash`: a photo, painting, or render in, pixel
3010
+ * art out, with no size to pass: PixelLab detects the native pixel scale
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
3013
+ * in October 2026. Sources over 2048px on the longer side are scaled down
3014
+ * upstream. The finished job carries the image at `last_response.images[0]`.
3015
+ */
3016
+ async imageToPixelartProFlash(args) {
3017
+ const body = { image: args.image };
3018
+ if (args.description) body.description = args.description;
3019
+ if (args.seed != null) body.seed = args.seed;
3020
+ return validateResponse(
3021
+ ProFlashImageSubmitSchema,
3022
+ await this.request("/image-to-pixelart-pro-flash", { method: "POST", body: JSON.stringify(body) }),
3023
+ "image-to-pixelart-pro-flash"
3024
+ );
3025
+ }
2941
3026
  /**
2942
3027
  * `/generate-ui-v2` ("Generate UI (Pro)"): one UI element (a button, a
2943
3028
  * health bar, an inventory slot, a dialogue box) from a description,
@@ -3848,10 +3933,10 @@ function imageMetadata(buf) {
3848
3933
  }
3849
3934
 
3850
3935
  // src/providers/pixellab.ts
3851
- var import_node_fs2 = require("fs");
3936
+ var import_node_fs3 = require("fs");
3852
3937
  var import_node_crypto2 = require("crypto");
3853
3938
  var import_node_os = __toESM(require("os"), 1);
3854
- var import_node_path = __toESM(require("path"), 1);
3939
+ var import_node_path2 = __toESM(require("path"), 1);
3855
3940
 
3856
3941
  // src/hash.ts
3857
3942
  var import_node_crypto = require("crypto");
@@ -3859,16 +3944,17 @@ var import_node_fs = require("fs");
3859
3944
  function sha256(data) {
3860
3945
  return (0, import_node_crypto.createHash)("sha256").update(data).digest("hex");
3861
3946
  }
3862
- async function sha256File(path38) {
3947
+ async function sha256File(path39) {
3863
3948
  return new Promise((resolve, reject) => {
3864
3949
  const hash = (0, import_node_crypto.createHash)("sha256");
3865
- const stream = (0, import_node_fs.createReadStream)(path38);
3950
+ const stream = (0, import_node_fs.createReadStream)(path39);
3866
3951
  stream.on("data", (chunk2) => hash.update(chunk2));
3867
3952
  stream.on("error", reject);
3868
3953
  stream.on("end", () => resolve(hash.digest("hex")));
3869
3954
  });
3870
3955
  }
3871
3956
  function specHash(spec, styleImageHashes, providerOptionIdentity = spec.providerOptions, providerInputIdentity = spec.providerInputs ?? {}) {
3957
+ const effect = Boolean(spec.vfx);
3872
3958
  return sha256(
3873
3959
  JSON.stringify({
3874
3960
  // Preserve every existing PixelLab hash while making a provider switch
@@ -3877,15 +3963,15 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
3877
3963
  providerOptions: providerOptionIdentity && (typeof providerOptionIdentity !== "object" || Object.keys(providerOptionIdentity).length > 0) ? providerOptionIdentity : void 0,
3878
3964
  providerInputs: providerInputIdentity && (typeof providerInputIdentity !== "object" || Object.keys(providerInputIdentity).length > 0) ? providerInputIdentity : void 0,
3879
3965
  generator: spec.generator,
3880
- prompt: spec.prompt,
3881
- width: spec.width,
3882
- height: spec.height,
3883
- view: spec.view,
3884
- outline: spec.outline ?? null,
3885
- shading: spec.shading ?? null,
3886
- detail: spec.detail ?? null,
3887
- seed: spec.seed ?? null,
3888
- 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,
3889
3975
  // `noBackground` only reaches the wire for pixflux and uiAsset; the
3890
3976
  // tile fields are undefined for every other generator. `tileSize` is
3891
3977
  // intentionally absent; width/height are derived from it, so it is
@@ -3960,6 +4046,15 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
3960
4046
  styleTraits: spec.objectPro.styleTraits,
3961
4047
  reference: spec.objectPro.reference?.sha256
3962
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,
3963
4058
  // A mirror's bytes come from its source's recorded outputs; the plan
3964
4059
  // compares those directly, so only the choice of source is identity.
3965
4060
  mirror: spec.mirror ? { of: spec.mirror.sourceAssetId } : void 0,
@@ -3997,6 +4092,337 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
3997
4092
  );
3998
4093
  }
3999
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
+
4000
4426
  // src/providers/pixellab.ts
4001
4427
  function pixelLabObjectUrl(generator, objectId) {
4002
4428
  if (!objectId) return null;
@@ -4011,6 +4437,7 @@ function pixelLabObjectUrl(generator, objectId) {
4011
4437
  const characterId = objectId.split("#")[0];
4012
4438
  return characterId ? `https://www.pixellab.ai/create-character/${encodeURIComponent(characterId)}` : null;
4013
4439
  }
4440
+ if (generator === "vfx") return `https://pixellab.ai/create-vfx?effect=${encodeURIComponent(objectId)}`;
4014
4441
  return null;
4015
4442
  }
4016
4443
  function billedFromUsage(usage) {
@@ -4034,7 +4461,7 @@ function characterCost(spec) {
4034
4461
  }
4035
4462
  if (character.kind === "state" || character.kind === "portrait") return generationCost(spec.width, spec.height, "1dir");
4036
4463
  if (character.kind === "outfit") {
4037
- return 20;
4464
+ return PRO_TIER_PRICES[0];
4038
4465
  }
4039
4466
  if (character.mode === "standard") return 1;
4040
4467
  if (character.mode === "v3") {
@@ -4127,13 +4554,20 @@ function decodeObjectAnimationJob(jobId) {
4127
4554
  return null;
4128
4555
  }
4129
4556
  var PixelLabProvider = class _PixelLabProvider {
4130
- 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) {
4131
4563
  this.client = client2;
4564
+ this.mcp = mcp;
4132
4565
  }
4133
4566
  client;
4567
+ mcp;
4134
4568
  id = "pixellab";
4135
4569
  static fromEnv() {
4136
- return new _PixelLabProvider(clientFromEnv());
4570
+ return new _PixelLabProvider(clientFromEnv(), mcpClientFromEnv());
4137
4571
  }
4138
4572
  /** Public storage URLs and the local content cache do not require API auth. */
4139
4573
  static forDownloads() {
@@ -4144,7 +4578,7 @@ var PixelLabProvider = class _PixelLabProvider {
4144
4578
  return new _PixelLabProvider(new PixelLabClient("download-only"));
4145
4579
  }
4146
4580
  supports(generator) {
4147
- 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";
4148
4582
  }
4149
4583
  /** `/map-objects` style matching: `background_image` plus `inpainting`. */
4150
4584
  supportsMapScene = true;
@@ -4186,11 +4620,12 @@ var PixelLabProvider = class _PixelLabProvider {
4186
4620
  * is a file read.
4187
4621
  */
4188
4622
  static cacheDir(namespace = "pixflux") {
4189
- const dir = import_node_path.default.join(import_node_os.default.tmpdir(), `pixelkiln-${namespace}`);
4190
- (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 });
4191
4625
  return dir;
4192
4626
  }
4193
4627
  estimate(spec) {
4628
+ if (spec.vfx) return { unit: "generations", amount: 0, candidates: 1 };
4194
4629
  if (spec.batch?.role === "member") {
4195
4630
  return { unit: "generations", amount: 0, candidates: 1 };
4196
4631
  }
@@ -4227,7 +4662,7 @@ var PixelLabProvider = class _PixelLabProvider {
4227
4662
  if (spec.revision) {
4228
4663
  const width = spec.revision.sourceWidth ?? spec.width;
4229
4664
  const height = spec.revision.sourceHeight ?? spec.height;
4230
- return { unit: "generations", amount: generationCost(width, height, "1dir"), candidates: 1 };
4665
+ return { unit: "generations", amount: proTierCost(width, height), candidates: 1 };
4231
4666
  }
4232
4667
  if (spec.generator === "tiles" || spec.generator === "terrain" || spec.generator === "isometricTile") {
4233
4668
  return { unit: "generations", amount: spec.cost, candidates: spec.candidates };
@@ -4248,6 +4683,9 @@ var PixelLabProvider = class _PixelLabProvider {
4248
4683
  };
4249
4684
  }
4250
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
+ }
4251
4689
  const members = spec.revision?.sourceMembers;
4252
4690
  if (spec.revision && members && !MEMBER_SET_REVISION_MODES.includes(spec.revision.mode)) {
4253
4691
  throw new Error(
@@ -4532,7 +4970,7 @@ var PixelLabProvider = class _PixelLabProvider {
4532
4970
  }
4533
4971
  }
4534
4972
  if (endFrame) {
4535
- 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);
4536
4974
  if (start && (start.width !== endFrame.width || start.height !== endFrame.height)) {
4537
4975
  throw new Error(
4538
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`
@@ -4690,7 +5128,7 @@ var PixelLabProvider = class _PixelLabProvider {
4690
5128
  }
4691
5129
  }
4692
5130
  if (endFrame) {
4693
- 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);
4694
5132
  if (start && (start.width !== endFrame.width || start.height !== endFrame.height)) {
4695
5133
  throw new Error(
4696
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`
@@ -4745,13 +5183,13 @@ var PixelLabProvider = class _PixelLabProvider {
4745
5183
  );
4746
5184
  }
4747
5185
  const frames = parentOutputs.slice(0, 16).map((output) => {
4748
- const bytes = (0, import_node_fs2.readFileSync)(output.path);
5186
+ const bytes = (0, import_node_fs3.readFileSync)(output.path);
4749
5187
  const dims = imageMetadata(bytes);
4750
5188
  if (!dims) throw new Error(`${spec.styleId}/${spec.assetId}: outfit source frame is not a readable PNG: ${output.path}`);
4751
5189
  return { base64: bytes.toString("base64"), format: "png", width: dims.width, height: dims.height };
4752
5190
  });
4753
5191
  const outfit = character.outfit;
4754
- const referenceBytes = (0, import_node_fs2.readFileSync)(outfit.reference.path);
5192
+ const referenceBytes = (0, import_node_fs3.readFileSync)(outfit.reference.path);
4755
5193
  if (sha256(referenceBytes) !== outfit.reference.sha256) {
4756
5194
  throw new Error(`${spec.styleId}/${spec.assetId}: outfit reference changed after the manifest was resolved: ${outfit.reference.path}`);
4757
5195
  }
@@ -4794,7 +5232,7 @@ var PixelLabProvider = class _PixelLabProvider {
4794
5232
  }
4795
5233
  if (character.kind === "portrait") {
4796
5234
  const parentFile = character.parentFile;
4797
- const bytes = (0, import_node_fs2.readFileSync)(parentFile);
5235
+ const bytes = (0, import_node_fs3.readFileSync)(parentFile);
4798
5236
  if (character.parentSha256 && sha256(bytes) !== character.parentSha256) {
4799
5237
  throw new Error(`${spec.styleId}/${spec.assetId}: portrait source changed after the manifest was resolved: ${parentFile}`);
4800
5238
  }
@@ -5000,8 +5438,8 @@ var PixelLabProvider = class _PixelLabProvider {
5000
5438
  const sources = images.map((image, index) => {
5001
5439
  if (!image.base64) throw new Error(`outfit transfer frame ${index} has no image data`);
5002
5440
  const role = meta?.frameRoles?.[index] ?? void 0;
5003
- const file = import_node_path.default.join(dir, `${jobId}-${index}.png`);
5004
- (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"));
5005
5443
  return { url: `file://${file}`, ...role ? { role } : {} };
5006
5444
  });
5007
5445
  return {
@@ -5152,6 +5590,7 @@ var PixelLabProvider = class _PixelLabProvider {
5152
5590
  async submit(spec, styleImages, context) {
5153
5591
  this.validate(spec, styleImages);
5154
5592
  if (spec.revision) return this.submitRevision(spec);
5593
+ if (spec.vfx) return this.submitVfx(spec, context);
5155
5594
  if (spec.generator === "character") return this.submitCharacter(spec, styleImages, context);
5156
5595
  if (spec.generator === "objectPro") return this.submitObjectPro(spec, styleImages, context);
5157
5596
  if (spec.generator === "uiAsset") {
@@ -5205,7 +5644,7 @@ var PixelLabProvider = class _PixelLabProvider {
5205
5644
  seed: spec.seed
5206
5645
  });
5207
5646
  const jobId = (0, import_node_crypto2.randomUUID)();
5208
- (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);
5209
5648
  return { jobId };
5210
5649
  }
5211
5650
  if (spec.generator === "tiles") {
@@ -5325,7 +5764,7 @@ var PixelLabProvider = class _PixelLabProvider {
5325
5764
  }
5326
5765
  const sourceFiles = revision.sourceMembers ?? [{ file: revision.sourceFile, sha256: revision.sourceSha256, format: revision.sourceFormat }];
5327
5766
  const images = sourceFiles.map((member) => {
5328
- const bytes = (0, import_node_fs2.readFileSync)(member.file);
5767
+ const bytes = (0, import_node_fs3.readFileSync)(member.file);
5329
5768
  if (sha256(bytes) !== member.sha256) {
5330
5769
  throw new Error(`${spec.styleId}/${spec.assetId}: revision source changed after the manifest was resolved`);
5331
5770
  }
@@ -5340,7 +5779,7 @@ var PixelLabProvider = class _PixelLabProvider {
5340
5779
  if (!revision.maskFile || !revision.maskSha256 || !revision.maskFormat) {
5341
5780
  throw new Error(`${spec.styleId}/${spec.assetId}: revision mask is not ready`);
5342
5781
  }
5343
- const maskBytes = (0, import_node_fs2.readFileSync)(revision.maskFile);
5782
+ const maskBytes = (0, import_node_fs3.readFileSync)(revision.maskFile);
5344
5783
  if (sha256(maskBytes) !== revision.maskSha256) {
5345
5784
  throw new Error(`${spec.styleId}/${spec.assetId}: revision mask changed after the manifest was resolved`);
5346
5785
  }
@@ -5365,7 +5804,7 @@ var PixelLabProvider = class _PixelLabProvider {
5365
5804
  if (!revision.maskFile || !revision.maskSha256 || !revision.maskFormat) {
5366
5805
  throw new Error(`${spec.styleId}/${spec.assetId}: revision mask is not ready`);
5367
5806
  }
5368
- const maskBytes = (0, import_node_fs2.readFileSync)(revision.maskFile);
5807
+ const maskBytes = (0, import_node_fs3.readFileSync)(revision.maskFile);
5369
5808
  if (sha256(maskBytes) !== revision.maskSha256) {
5370
5809
  throw new Error(`${spec.styleId}/${spec.assetId}: revision mask changed after the manifest was resolved`);
5371
5810
  }
@@ -5386,7 +5825,7 @@ var PixelLabProvider = class _PixelLabProvider {
5386
5825
  if (!revision.paletteImageSha256 || !revision.paletteImageFormat) {
5387
5826
  throw new Error(`${spec.styleId}/${spec.assetId}: revision palette image is not ready`);
5388
5827
  }
5389
- const paletteBytes = (0, import_node_fs2.readFileSync)(revision.paletteImageFile);
5828
+ const paletteBytes = (0, import_node_fs3.readFileSync)(revision.paletteImageFile);
5390
5829
  if (sha256(paletteBytes) !== revision.paletteImageSha256) {
5391
5830
  throw new Error(`${spec.styleId}/${spec.assetId}: revision palette image changed after the manifest was resolved`);
5392
5831
  }
@@ -5477,7 +5916,7 @@ var PixelLabProvider = class _PixelLabProvider {
5477
5916
  if (!revision.keypointsFile || !revision.keypointsSha256 || !revision.skeleton) {
5478
5917
  throw new Error(`${spec.styleId}/${spec.assetId}: revision keypoints are not ready`);
5479
5918
  }
5480
- const keypointsBytes = (0, import_node_fs2.readFileSync)(revision.keypointsFile);
5919
+ const keypointsBytes = (0, import_node_fs3.readFileSync)(revision.keypointsFile);
5481
5920
  if (sha256(keypointsBytes) !== revision.keypointsSha256) {
5482
5921
  throw new Error(`${spec.styleId}/${spec.assetId}: revision keypoints changed after the manifest was resolved`);
5483
5922
  }
@@ -5519,7 +5958,7 @@ var PixelLabProvider = class _PixelLabProvider {
5519
5958
  if (!revision.lastFrameSha256 || !revision.lastFrameFormat) {
5520
5959
  throw new Error(`${spec.styleId}/${spec.assetId}: revision last frame is not ready`);
5521
5960
  }
5522
- const bytes = (0, import_node_fs2.readFileSync)(revision.lastFrameFile);
5961
+ const bytes = (0, import_node_fs3.readFileSync)(revision.lastFrameFile);
5523
5962
  if (sha256(bytes) !== revision.lastFrameSha256) {
5524
5963
  throw new Error(`${spec.styleId}/${spec.assetId}: revision last frame changed after the manifest was resolved`);
5525
5964
  }
@@ -5534,11 +5973,11 @@ var PixelLabProvider = class _PixelLabProvider {
5534
5973
  cacheCleanupResult(pngs, usage, roles, fps) {
5535
5974
  const jobId = (0, import_node_crypto2.randomUUID)();
5536
5975
  if (!roles) {
5537
- (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]);
5538
5977
  return { jobId, metadata: { revisionUsage: usage } };
5539
5978
  }
5540
5979
  for (const [index, png] of pngs.entries()) {
5541
- (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);
5542
5981
  }
5543
5982
  return { jobId, metadata: { revisionUsage: usage, revisionMembers: roles, ...fps != null ? { revisionFps: fps } : {} } };
5544
5983
  }
@@ -5566,10 +6005,11 @@ var PixelLabProvider = class _PixelLabProvider {
5566
6005
  return this.pollAnimateRevision(jobId, context);
5567
6006
  }
5568
6007
  if (context?.spec?.revision) return this.pollRevision(jobId);
6008
+ if (generator === "vfx") return this.pollVfx(jobId, context);
5569
6009
  if (generator === "imageProFlash") return this.pollRevision(jobId);
5570
6010
  if (generator === "pixflux") {
5571
- const file = import_node_path.default.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
5572
- 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)) {
5573
6013
  const sourceUrl = `file://${file}`;
5574
6014
  return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }] };
5575
6015
  }
@@ -5612,6 +6052,120 @@ var PixelLabProvider = class _PixelLabProvider {
5612
6052
  * background job to ask afterwards the way `billedForJob` does for async
5613
6053
  * work.
5614
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
+ }
5615
6169
  async pollCachedRevision(jobId, context) {
5616
6170
  const usage = context?.metadata?.revisionUsage;
5617
6171
  const roles = context?.metadata?.revisionMembers;
@@ -5621,10 +6175,10 @@ var PixelLabProvider = class _PixelLabProvider {
5621
6175
  };
5622
6176
  if (Array.isArray(roles) && roles.length) {
5623
6177
  const sources = roles.map((role, index) => ({
5624
- 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`)}`,
5625
6179
  role: String(role)
5626
6180
  }));
5627
- 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;
5628
6182
  const fps = context?.metadata?.revisionFps;
5629
6183
  const frames = sources.every((source) => source.role.startsWith("frame-"));
5630
6184
  return {
@@ -5636,8 +6190,8 @@ var PixelLabProvider = class _PixelLabProvider {
5636
6190
  billed: billedFromUsage(usage)
5637
6191
  };
5638
6192
  }
5639
- const file = import_node_path.default.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
5640
- 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;
5641
6195
  const sourceUrl = `file://${file}`;
5642
6196
  return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }], billed: billedFromUsage(usage) };
5643
6197
  }
@@ -5693,8 +6247,8 @@ var PixelLabProvider = class _PixelLabProvider {
5693
6247
  }
5694
6248
  const base64 = extractBase64(item);
5695
6249
  if (base64) {
5696
- const file = import_node_path.default.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`);
5697
- (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"));
5698
6252
  urls.push(`file://${file}`);
5699
6253
  continue;
5700
6254
  }
@@ -5744,8 +6298,8 @@ var PixelLabProvider = class _PixelLabProvider {
5744
6298
  const candidate = done[key];
5745
6299
  const base64 = extractBase64(candidate);
5746
6300
  if (base64) {
5747
- const file = import_node_path.default.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
5748
- (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"));
5749
6303
  const sourceUrl = `file://${file}`;
5750
6304
  return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }], billed };
5751
6305
  }
@@ -5753,8 +6307,8 @@ var PixelLabProvider = class _PixelLabProvider {
5753
6307
  if (Array.isArray(done.images) && done.images.length) {
5754
6308
  const base64 = extractBase64(done.images[0]);
5755
6309
  if (base64) {
5756
- const file = import_node_path.default.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
5757
- (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"));
5758
6312
  const sourceUrl = `file://${file}`;
5759
6313
  return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }], billed };
5760
6314
  }
@@ -5787,8 +6341,8 @@ var PixelLabProvider = class _PixelLabProvider {
5787
6341
  for (const [index, candidate] of done.images.entries()) {
5788
6342
  const base64 = extractBase64(candidate);
5789
6343
  if (!base64) continue;
5790
- const file = import_node_path.default.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`);
5791
- (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"));
5792
6346
  urls.push(`file://${file}`);
5793
6347
  }
5794
6348
  if (urls.length) return { status: "review", candidateUrls: urls, billed };
@@ -5817,8 +6371,8 @@ var PixelLabProvider = class _PixelLabProvider {
5817
6371
  for (const [index, candidate] of images.entries()) {
5818
6372
  const base64 = extractBase64(candidate);
5819
6373
  if (!base64) continue;
5820
- const file = import_node_path.default.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`);
5821
- (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"));
5822
6376
  urls.push(`file://${file}`);
5823
6377
  }
5824
6378
  if (urls.length === 1) return { status: "ready", objectId: jobId, sourceUrl: urls[0], sources: [{ url: urls[0] }], billed };
@@ -5915,8 +6469,8 @@ var PixelLabProvider = class _PixelLabProvider {
5915
6469
  for (const [index, tile] of tileset.tiles.entries()) {
5916
6470
  const slug2 = tile.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "tile";
5917
6471
  const role = `tile-${String(index).padStart(2, "0")}-${slug2}`;
5918
- const file = import_node_path.default.join(_PixelLabProvider.cacheDir(), `${tilesetId}-${index}.png`);
5919
- (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"));
5920
6474
  sources.push({ url: `file://${file}`, role });
5921
6475
  terrainTiles.push({ role, corners: tile.corners, pattern4x4: tile.pattern_4x4 });
5922
6476
  }
@@ -5945,8 +6499,8 @@ var PixelLabProvider = class _PixelLabProvider {
5945
6499
  const { image, usage } = await this.client.getIsometricTile(tileId);
5946
6500
  const backgroundJobId = context?.metadata?.backgroundJobId;
5947
6501
  const billed = billedFromUsage(usage) ?? await this.billedForJob(backgroundJobId);
5948
- const file = import_node_path.default.join(_PixelLabProvider.cacheDir(), `${tileId}.png`);
5949
- (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"));
5950
6504
  return {
5951
6505
  status: "ready",
5952
6506
  objectId: tileId,
@@ -5989,8 +6543,8 @@ var PixelLabProvider = class _PixelLabProvider {
5989
6543
  return { objectId: `${jobId}#${index}`, sourceUrl: url };
5990
6544
  }
5991
6545
  if (generator === "imagePro" || generator === "uiElement") {
5992
- const file = import_node_path.default.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`);
5993
- 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}`);
5994
6548
  return { objectId: `${jobId}#${index}`, sourceUrl: `file://${file}` };
5995
6549
  }
5996
6550
  const promoted = await this.client.selectFrames(jobId, [index], commonTag);
@@ -6002,7 +6556,7 @@ var PixelLabProvider = class _PixelLabProvider {
6002
6556
  return { objectId, sourceUrl: firstUrl(obj?.rotation_urls) ?? obj?.preview_url ?? null };
6003
6557
  }
6004
6558
  async download(url) {
6005
- 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));
6006
6560
  return this.client.download(url);
6007
6561
  }
6008
6562
  async balance() {
@@ -6124,7 +6678,7 @@ function extractBase64(value) {
6124
6678
  function readReference(spec, reference) {
6125
6679
  const images = {};
6126
6680
  for (const [direction, image] of Object.entries(reference)) {
6127
- const bytes = (0, import_node_fs2.readFileSync)(image.path);
6681
+ const bytes = (0, import_node_fs3.readFileSync)(image.path);
6128
6682
  if (sha256(bytes) !== image.sha256) {
6129
6683
  throw new Error(`${spec.styleId}/${spec.assetId}: reference (${direction}) changed after the manifest was resolved: ${image.path}`);
6130
6684
  }
@@ -6141,7 +6695,7 @@ function requireStyleObjectId(spec, context) {
6141
6695
  return context.styleObjectId;
6142
6696
  }
6143
6697
  function readPose(spec, what, image) {
6144
- const bytes = (0, import_node_fs2.readFileSync)(image.path);
6698
+ const bytes = (0, import_node_fs3.readFileSync)(image.path);
6145
6699
  if (sha256(bytes) !== image.sha256) {
6146
6700
  throw new Error(`${spec.styleId}/${spec.assetId}: ${what} changed after the manifest was resolved: ${image.path}`);
6147
6701
  }
@@ -6206,7 +6760,7 @@ function findObjectAnimation(object, name, direction, animationId) {
6206
6760
  function mapScenePayload(spec) {
6207
6761
  const scene = spec.scene;
6208
6762
  const read = (file, hash, label) => {
6209
- const bytes = (0, import_node_fs2.readFileSync)(file);
6763
+ const bytes = (0, import_node_fs3.readFileSync)(file);
6210
6764
  if (!hash || sha256(bytes) !== hash) {
6211
6765
  throw new Error(`${spec.styleId}/${spec.assetId}: ${label} changed after the manifest was resolved`);
6212
6766
  }
@@ -6267,9 +6821,9 @@ var RetroDiffusionClient = class {
6267
6821
  if (!response.ok) throw new ProviderError("retrodiffusion", `Retro Diffusion download failed (${response.status})`, { status: response.status });
6268
6822
  return Buffer.from(await response.arrayBuffer());
6269
6823
  }
6270
- async call(path38, init = {}) {
6824
+ async call(path39, init = {}) {
6271
6825
  if (!this.token) throw new Error("RD_API_KEY is not set");
6272
- const response = await this.request(`${this.baseUrl}${path38}`, {
6826
+ const response = await this.request(`${this.baseUrl}${path39}`, {
6273
6827
  ...init,
6274
6828
  headers: {
6275
6829
  "Content-Type": "application/json",
@@ -6579,9 +7133,9 @@ function retroError(value) {
6579
7133
 
6580
7134
  // src/providers/scenario.ts
6581
7135
  var import_node_crypto3 = require("crypto");
6582
- var import_node_fs3 = require("fs");
7136
+ var import_node_fs4 = require("fs");
6583
7137
  var import_node_os2 = __toESM(require("os"), 1);
6584
- var import_node_path2 = __toESM(require("path"), 1);
7138
+ var import_node_path3 = __toESM(require("path"), 1);
6585
7139
  var DEFAULT_BASE_URL2 = "https://api.cloud.scenario.com/v1";
6586
7140
  var SOURCE_PROTOCOL2 = "scenario:";
6587
7141
  var SUBMIT_RETRIES = 8;
@@ -6964,7 +7518,7 @@ var ScenarioProvider = class _ScenarioProvider {
6964
7518
  }
6965
7519
  };
6966
7520
  function uploadCachePath() {
6967
- 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");
6968
7522
  }
6969
7523
  function uploadCacheKey(baseUrl, projectId, digest2) {
6970
7524
  return (0, import_node_crypto3.createHash)("sha256").update(`${baseUrl}
@@ -6973,7 +7527,7 @@ ${digest2}`).digest("hex");
6973
7527
  }
6974
7528
  function readUploadCache() {
6975
7529
  try {
6976
- const value = JSON.parse((0, import_node_fs3.readFileSync)(uploadCachePath(), "utf8"));
7530
+ const value = JSON.parse((0, import_node_fs4.readFileSync)(uploadCachePath(), "utf8"));
6977
7531
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
6978
7532
  } catch {
6979
7533
  return {};
@@ -6982,10 +7536,10 @@ function readUploadCache() {
6982
7536
  function writeUploadCache(key, assetId) {
6983
7537
  try {
6984
7538
  const file = uploadCachePath();
6985
- (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 });
6986
7540
  const tmp = `${file}.${process.pid}.tmp`;
6987
- (0, import_node_fs3.writeFileSync)(tmp, JSON.stringify({ ...readUploadCache(), [key]: assetId }));
6988
- (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);
6989
7543
  } catch {
6990
7544
  }
6991
7545
  }
@@ -7012,7 +7566,7 @@ function readRevisionSource(spec) {
7012
7566
  if (!revision.sourceFile || !revision.sourceSha256 || !revision.sourceFormat) {
7013
7567
  throw new Error(`${spec.styleId}/${spec.assetId}: revision source is not ready`);
7014
7568
  }
7015
- const bytes = (0, import_node_fs3.readFileSync)(revision.sourceFile);
7569
+ const bytes = (0, import_node_fs4.readFileSync)(revision.sourceFile);
7016
7570
  if (sha256(bytes) !== revision.sourceSha256) {
7017
7571
  throw new Error(`${spec.styleId}/${spec.assetId}: revision source changed after the manifest was resolved`);
7018
7572
  }
@@ -7182,8 +7736,8 @@ function redactScenarioError(value, secrets) {
7182
7736
 
7183
7737
  // src/providers/comfyui.ts
7184
7738
  var import_node_crypto4 = require("crypto");
7185
- var import_promises = require("fs/promises");
7186
- var import_node_path3 = __toESM(require("path"), 1);
7739
+ var import_promises2 = require("fs/promises");
7740
+ var import_node_path4 = __toESM(require("path"), 1);
7187
7741
  var DEFAULT_BASE_URL3 = "http://127.0.0.1:8188";
7188
7742
  var BUILTIN_BINDINGS = /* @__PURE__ */ new Set([
7189
7743
  "prompt",
@@ -7320,10 +7874,10 @@ var ComfyUIProvider = class _ComfyUIProvider {
7320
7874
  }
7321
7875
  async resolveOptions(value, context) {
7322
7876
  const options = parseOptions(value);
7323
- const workflowPath = import_node_path3.default.resolve(context.root, options.workflowFile);
7877
+ const workflowPath = import_node_path4.default.resolve(context.root, options.workflowFile);
7324
7878
  let raw;
7325
7879
  try {
7326
- raw = await (0, import_promises.readFile)(workflowPath, "utf8");
7880
+ raw = await (0, import_promises2.readFile)(workflowPath, "utf8");
7327
7881
  } catch (err) {
7328
7882
  throw new Error(
7329
7883
  `ComfyUI workflow for style "${context.styleId}" could not be read at ${workflowPath}: ${err instanceof Error ? err.message : String(err)}`
@@ -7375,10 +7929,10 @@ var ComfyUIProvider = class _ComfyUIProvider {
7375
7929
  if (typeof item !== "string") {
7376
7930
  throw new Error(`ComfyUI ${label} must be a manifest-relative PNG or JPEG path`);
7377
7931
  }
7378
- const file = import_node_path3.default.resolve(context.root, item);
7932
+ const file = import_node_path4.default.resolve(context.root, item);
7379
7933
  let bytes;
7380
7934
  try {
7381
- bytes = await (0, import_promises.readFile)(file);
7935
+ bytes = await (0, import_promises2.readFile)(file);
7382
7936
  } catch (error) {
7383
7937
  throw new Error(
7384
7938
  `ComfyUI ${label} could not be read at ${file}: ${error instanceof Error ? error.message : String(error)}`
@@ -7535,7 +8089,7 @@ var ComfyUIProvider = class _ComfyUIProvider {
7535
8089
  setBinding(workflow2, binding, input);
7536
8090
  return;
7537
8091
  }
7538
- const bytes = await (0, import_promises.readFile)(input.path);
8092
+ const bytes = await (0, import_promises2.readFile)(input.path);
7539
8093
  if (sha256(bytes) !== input.sha256) {
7540
8094
  throw new Error(`ComfyUI provider input "${name}" changed before upload`);
7541
8095
  }
@@ -7602,7 +8156,7 @@ var ComfyUIProvider = class _ComfyUIProvider {
7602
8156
  if (!spec.revision.sourceSha256 || !spec.revision.sourceFormat) {
7603
8157
  throw new Error("ComfyUI revision source is not ready");
7604
8158
  }
7605
- const sourceBytes = await (0, import_promises.readFile)(spec.revision.sourceFile);
8159
+ const sourceBytes = await (0, import_promises2.readFile)(spec.revision.sourceFile);
7606
8160
  if (sha256(sourceBytes) !== spec.revision.sourceSha256) {
7607
8161
  throw new Error("ComfyUI revision source changed before upload");
7608
8162
  }
@@ -7616,7 +8170,7 @@ var ComfyUIProvider = class _ComfyUIProvider {
7616
8170
  if (!spec.revision.maskSha256 || !spec.revision.maskFormat) {
7617
8171
  throw new Error("ComfyUI revision mask is not ready");
7618
8172
  }
7619
- const maskBytes = await (0, import_promises.readFile)(spec.revision.maskFile);
8173
+ const maskBytes = await (0, import_promises2.readFile)(spec.revision.maskFile);
7620
8174
  if (sha256(maskBytes) !== spec.revision.maskSha256) {
7621
8175
  throw new Error("ComfyUI revision mask changed before upload");
7622
8176
  }
@@ -8336,9 +8890,9 @@ registerProvider({
8336
8890
  });
8337
8891
 
8338
8892
  // src/manifest.ts
8339
- var import_promises3 = require("fs/promises");
8340
- var import_node_fs6 = require("fs");
8341
- 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);
8342
8896
 
8343
8897
  // src/loop-directions.ts
8344
8898
  var SINGLE_OUTPUT_FIELDS = ["file", "cell", "remoteId", "source", "sourceByStyle", "outputRole"];
@@ -8426,17 +8980,17 @@ function isObject2(value) {
8426
8980
  }
8427
8981
 
8428
8982
  // src/outputs.ts
8429
- var import_node_fs5 = require("fs");
8430
- var import_node_path5 = __toESM(require("path"), 1);
8983
+ var import_node_fs6 = require("fs");
8984
+ var import_node_path6 = __toESM(require("path"), 1);
8431
8985
 
8432
8986
  // src/lock.ts
8433
- var import_promises2 = require("fs/promises");
8434
- var import_node_fs4 = require("fs");
8435
- 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);
8436
8990
  async function loadLock(lockPath) {
8437
- if (!(0, import_node_fs4.existsSync)(lockPath)) return { version: 2, entries: {} };
8991
+ if (!(0, import_node_fs5.existsSync)(lockPath)) return { version: 2, entries: {} };
8438
8992
  try {
8439
- return parseLock(JSON.parse(await (0, import_promises2.readFile)(lockPath, "utf8")));
8993
+ return parseLock(JSON.parse(await (0, import_promises3.readFile)(lockPath, "utf8")));
8440
8994
  } catch (err) {
8441
8995
  throw new ProjectError(
8442
8996
  `Lockfile at ${lockPath} is malformed:
@@ -8449,7 +9003,7 @@ var saveQueues = /* @__PURE__ */ new Map();
8449
9003
  var dirtyPatches = /* @__PURE__ */ new WeakMap();
8450
9004
  var dirtyDeletes = /* @__PURE__ */ new WeakMap();
8451
9005
  function saveLock(lockPath, lock) {
8452
- const queueKey = import_node_path4.default.resolve(lockPath);
9006
+ const queueKey = import_node_path5.default.resolve(lockPath);
8453
9007
  const previous = saveQueues.get(queueKey) ?? Promise.resolve();
8454
9008
  const next = previous.catch(() => {
8455
9009
  }).then(() => writeLockNow(lockPath, lock));
@@ -8461,7 +9015,7 @@ function saveLock(lockPath, lock) {
8461
9015
  return next;
8462
9016
  }
8463
9017
  async function writeLockNow(lockPath, lock) {
8464
- 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 });
8465
9019
  const release = await acquireFileLock(lockPath);
8466
9020
  try {
8467
9021
  await writeLockWhileHeld(lockPath, lock);
@@ -8471,9 +9025,9 @@ async function writeLockNow(lockPath, lock) {
8471
9025
  }
8472
9026
  async function writeLockWhileHeld(file, lock) {
8473
9027
  let disk = { version: 2, entries: {} };
8474
- if ((0, import_node_fs4.existsSync)(file)) {
9028
+ if ((0, import_node_fs5.existsSync)(file)) {
8475
9029
  try {
8476
- disk = parseLock(JSON.parse(await (0, import_promises2.readFile)(file, "utf8")));
9030
+ disk = parseLock(JSON.parse(await (0, import_promises3.readFile)(file, "utf8")));
8477
9031
  } catch (err) {
8478
9032
  throw new ProjectError(
8479
9033
  `Refusing to overwrite malformed lockfile at ${file}: ${err instanceof Error ? err.message : String(err)}`,
@@ -8502,10 +9056,10 @@ async function writeLockWhileHeld(file, lock) {
8502
9056
  }
8503
9057
  const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
8504
9058
  try {
8505
- await (0, import_promises2.writeFile)(tmp, JSON.stringify({ version: 2, entries: sorted }, null, 2) + "\n");
8506
- 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);
8507
9061
  } finally {
8508
- await (0, import_promises2.rm)(tmp, { force: true });
9062
+ await (0, import_promises3.rm)(tmp, { force: true });
8509
9063
  }
8510
9064
  const currentDirty = dirtyPatches.get(lock);
8511
9065
  if (currentDirty) {
@@ -8568,18 +9122,18 @@ async function acquireFileLock(file) {
8568
9122
  const deadline = Date.now() + FILE_LOCK_TIMEOUT_MS;
8569
9123
  for (; ; ) {
8570
9124
  try {
8571
- await (0, import_promises2.mkdir)(lockDir);
9125
+ await (0, import_promises3.mkdir)(lockDir);
8572
9126
  return async () => {
8573
- await (0, import_promises2.rmdir)(lockDir).catch(() => {
9127
+ await (0, import_promises3.rmdir)(lockDir).catch(() => {
8574
9128
  });
8575
9129
  };
8576
9130
  } catch (err) {
8577
9131
  const code = err.code;
8578
9132
  if (code !== "EEXIST") throw err;
8579
9133
  try {
8580
- const age = Date.now() - (await (0, import_promises2.stat)(lockDir)).mtimeMs;
9134
+ const age = Date.now() - (await (0, import_promises3.stat)(lockDir)).mtimeMs;
8581
9135
  if (age > STALE_FILE_LOCK_MS) {
8582
- await (0, import_promises2.rmdir)(lockDir);
9136
+ await (0, import_promises3.rmdir)(lockDir);
8583
9137
  continue;
8584
9138
  }
8585
9139
  } catch (lockErr) {
@@ -8606,22 +9160,22 @@ function totalSpend(lock, unit = "generations") {
8606
9160
 
8607
9161
  // src/outputs.ts
8608
9162
  function portableOutputPath(file, manifestDir) {
8609
- const root = import_node_path5.default.resolve(manifestDir);
8610
- const absolute = import_node_path5.default.isAbsolute(file) ? import_node_path5.default.normalize(file) : import_node_path5.default.resolve(root, file);
8611
- const relative = import_node_path5.default.relative(root, absolute);
8612
- const value = import_node_path5.default.isAbsolute(relative) ? absolute : relative;
8613
- 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("/");
8614
9168
  }
8615
9169
  function resolveOutputPath(recordedPath, manifestDir) {
8616
- if (import_node_path5.default.isAbsolute(recordedPath)) return import_node_path5.default.normalize(recordedPath);
8617
- if (import_node_path5.default.win32.isAbsolute(recordedPath)) return recordedPath;
8618
- 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));
8619
9173
  }
8620
9174
  function expectedOutputPath(spec, role, index, total, mediaType) {
8621
9175
  return memberPath(spec.outFile, role, index, total, mediaType);
8622
9176
  }
8623
9177
  function memberPath(file, role, index, total, mediaType) {
8624
- const originalExt = import_node_path5.default.extname(file);
9178
+ const originalExt = import_node_path6.default.extname(file);
8625
9179
  const ext = mediaType ? mediaExtension(mediaType) : originalExt || ".png";
8626
9180
  const stem = originalExt ? file.slice(0, -originalExt.length) : file;
8627
9181
  if (total === 1) return `${stem}${ext}`;
@@ -8635,10 +9189,10 @@ function isFrameSetEntry(entry) {
8635
9189
  return entry.generator === "frames" && isMemberSetEntry(entry);
8636
9190
  }
8637
9191
  function sourceIsStem(source, entry, manifestDir) {
8638
- 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));
8639
9193
  }
8640
9194
  function sourceOutputPath(source, entry, index, manifestDir) {
8641
- const absolute = import_node_path5.default.resolve(manifestDir, source);
9195
+ const absolute = import_node_path6.default.resolve(manifestDir, source);
8642
9196
  if (!sourceIsStem(source, entry, manifestDir)) return absolute;
8643
9197
  const output = entry.outputs[index];
8644
9198
  return memberPath(absolute, output.role, index, entry.outputs.length, output.mediaType);
@@ -8725,7 +9279,7 @@ function resolveSpecOutputs(spec, lock, manifestDir) {
8725
9279
  id: spec.assetId,
8726
9280
  index: 0,
8727
9281
  path: spec.outFile,
8728
- absolutePath: import_node_path5.default.resolve(spec.outFile),
9282
+ absolutePath: import_node_path6.default.resolve(spec.outFile),
8729
9283
  sha256: ""
8730
9284
  }];
8731
9285
  }
@@ -8738,12 +9292,17 @@ function frameSetFps(entry) {
8738
9292
  }
8739
9293
  return null;
8740
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
+ }
8741
9300
 
8742
9301
  // src/manifest.ts
8743
9302
  async function loadManifest(manifestPath) {
8744
- const abs = import_node_path6.default.resolve(manifestPath);
8745
- if (!(0, import_node_fs6.existsSync)(abs)) throw new ProjectError(`No manifest at ${abs}`);
8746
- 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")));
8747
9306
  if (expansion.issues.length) {
8748
9307
  throw new ProjectError(`Manifest at ${abs} is invalid:
8749
9308
  ${expansion.issues.map((i) => ` ${i}`).join("\n")}`);
@@ -8798,7 +9357,7 @@ ${issues}`);
8798
9357
  if (fromStyle) {
8799
9358
  if (!styleIds.has(fromStyle)) {
8800
9359
  unknownReferences.push(`assets.${assetId}.revision.fromStyle: unknown style "${fromStyle}"`);
8801
- } else if (parent && parent.styles.length && !parent.styles.includes(fromStyle)) {
9360
+ } else if (parent && !assetInStyle(parent, fromStyle, parsed.data.styles[fromStyle])) {
8802
9361
  unknownReferences.push(
8803
9362
  `assets.${assetId}.revision.fromStyle: "${asset.revision.from}" is not in style "${fromStyle}"`
8804
9363
  );
@@ -8845,7 +9404,11 @@ ${issues}`);
8845
9404
  ${unknownReferences.map((i) => ` ${i}`).join("\n")}`);
8846
9405
  }
8847
9406
  const loopFamilies = Object.keys(expansion.families).length ? { loopFamilies: expansion.families } : {};
8848
- 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);
8849
9412
  }
8850
9413
  function parentAssetId(asset) {
8851
9414
  return asset?.revision?.from ?? asset?.state?.of ?? asset?.animation?.of ?? asset?.portrait?.of ?? asset?.outfit?.of;
@@ -8949,8 +9512,8 @@ async function resolveSpecs(loaded, filter) {
8949
9512
  const needed = /* @__PURE__ */ new Set();
8950
9513
  for (const assetId of resolutionAssetIds) {
8951
9514
  const asset = manifest.assets[assetId];
8952
- if (asset?.styles.length && !asset.styles.includes(styleId)) continue;
8953
- const fromStyle = asset?.revision?.fromStyle;
9515
+ if (!asset || !assetInStyle(asset, styleId, manifest.styles[styleId])) continue;
9516
+ const fromStyle = asset.revision?.fromStyle;
8954
9517
  if (fromStyle && fromStyle !== styleId) needed.add(fromStyle);
8955
9518
  }
8956
9519
  styleDependencies.set(styleId, needed);
@@ -8979,11 +9542,11 @@ async function resolveSpecs(loaded, filter) {
8979
9542
  const styleFinalizers = /* @__PURE__ */ new Map();
8980
9543
  const styleImageCache = /* @__PURE__ */ new Map();
8981
9544
  async function loadStyleImage(rel, what = "Style image") {
8982
- const abs = import_node_path6.default.resolve(root, rel);
9545
+ const abs = import_node_path7.default.resolve(root, rel);
8983
9546
  let hit = styleImageCache.get(abs);
8984
9547
  if (!hit) {
8985
- if (!(0, import_node_fs6.existsSync)(abs)) throw new Error(`${what} not found: ${abs}`);
8986
- 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);
8987
9550
  const metadata = imageMetadata(buf);
8988
9551
  if (!metadata) throw new Error(`${what} is not a readable PNG or JPEG: ${abs}`);
8989
9552
  if (metadata.width < 1 || metadata.height < 1) {
@@ -9009,14 +9572,14 @@ async function resolveSpecs(loaded, filter) {
9009
9572
  styleImageDimensions.push(loadedImage);
9010
9573
  }
9011
9574
  const resolvedImages = style.styleImages.map((image) => {
9012
- 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));
9013
9576
  return { base64: hit.base64, width: hit.width, height: hit.height, format: hit.format };
9014
9577
  });
9015
9578
  const styleSpecs = /* @__PURE__ */ new Map();
9016
9579
  const providerInputIdentities = /* @__PURE__ */ new Map();
9017
9580
  for (const [assetId, asset] of Object.entries(manifest.assets)) {
9018
9581
  if (!resolutionAssetIds.has(assetId)) continue;
9019
- if (asset.styles.length && !asset.styles.includes(styleId)) continue;
9582
+ if (!assetInStyle(asset, styleId, style)) continue;
9020
9583
  const generator = style.generator;
9021
9584
  if (!activeProvider.supports(generator)) {
9022
9585
  throw new Error(`Provider "${activeProvider.id}" does not support generator "${generator}"`);
@@ -9036,6 +9599,15 @@ async function resolveSpecs(loaded, filter) {
9036
9599
  const providerInputs = inputResolution.inputs;
9037
9600
  const providerInputIdentity = inputResolution.identity ?? providerInputs;
9038
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;
9039
9611
  let width;
9040
9612
  let height;
9041
9613
  let size;
@@ -9063,6 +9635,11 @@ async function resolveSpecs(loaded, filter) {
9063
9635
  size = asset.size ?? style.size ?? 64;
9064
9636
  width = size;
9065
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);
9066
9643
  } else if (generator === "uiAsset" || generator === "uiElement") {
9067
9644
  width = asset.width ?? style.size ?? 256;
9068
9645
  height = asset.height ?? style.size ?? 256;
@@ -9105,14 +9682,14 @@ async function resolveSpecs(loaded, filter) {
9105
9682
  terrainUpperDescription = wrap(parsed.upper);
9106
9683
  terrainTransitionDescription = parsed.transition ? wrap(parsed.transition) : void 0;
9107
9684
  prompt = subject.trim();
9108
- } else if ((generator === "character" || generator === "objectPro") && characterKind !== "base") {
9685
+ } else if ((generator === "character" || generator === "objectPro") && characterKind !== "base" || vfx) {
9109
9686
  prompt = subject.trim();
9110
9687
  } else {
9111
9688
  prompt = [style.promptPrefix, subject, style.promptSuffix].map((p) => p.trim()).filter(Boolean).join(", ");
9112
9689
  }
9113
- const relFile = asset.file ?? import_node_path6.default.join(asset.category ?? "", `${assetId}.png`);
9114
- const outFile = import_node_path6.default.resolve(root, style.outDir, relFile);
9115
- 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;
9116
9693
  const tileSize = generator === "tiles" ? size : style.tileSize ?? 32;
9117
9694
  const tileVariations = tileFeatureOutputCount(generator === "tiles" ? style.tileFeature : void 0) ?? tileVariationCount(countNumberedDescriptions(prompt));
9118
9695
  const terrainTiles = generator === "terrain" ? terrainTileCount(style.terrainTransitionSize) : 0;
@@ -9183,6 +9760,7 @@ async function resolveSpecs(loaded, filter) {
9183
9760
  // A portrait needs a character style (refused above), so an
9184
9761
  // objectPro asset is only ever a base, state, or animation.
9185
9762
  ...generator === "objectPro" && !asset.revision && characterKind !== "portrait" ? { objectPro: await resolveObjectProShape(assetId, asset, style, characterKind, { root, load: loadStyleImage }) } : {},
9763
+ ...vfx ? { vfx } : {},
9186
9764
  cost: generator === "tiles" ? tilesCost(tileSize, tileVariations) : generator === "terrain" ? tilesCost(size, terrainTiles) : generator === "isometricTile" ? isometricTileCost() : generationCost(width, height, generator),
9187
9765
  costUnit: "generations",
9188
9766
  candidates: generator === "tiles" ? tileVariations : generator === "terrain" ? terrainTiles : generator === "1dir" || generator === "imagePro" ? candidateCount(size) : 1
@@ -9207,7 +9785,7 @@ async function resolveSpecs(loaded, filter) {
9207
9785
  minGridConfidence: style.quality.minGridConfidence,
9208
9786
  ...style.quality.minTransparency == null ? {} : { minTransparency: style.quality.minTransparency },
9209
9787
  ...style.quality.fixerRevision ? { fixerRevision: style.quality.fixerRevision } : {},
9210
- ...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) } : {},
9211
9789
  ...style.quality.fps == null ? {} : { fps: style.quality.fps }
9212
9790
  }
9213
9791
  } : {},
@@ -9290,7 +9868,7 @@ async function resolveSpecs(loaded, filter) {
9290
9868
  const file = memberPath(anchorSpec.outFile, "south", 0, anchorSpec.character.directions, MediaType.PNG);
9291
9869
  resolved.character = {
9292
9870
  ...resolved.character,
9293
- 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 }
9294
9872
  };
9295
9873
  }
9296
9874
  const characterParent = asset.state?.of ?? asset.animation?.of ?? asset.portrait?.of;
@@ -9303,7 +9881,7 @@ async function resolveSpecs(loaded, filter) {
9303
9881
  parentAssetId: characterParent,
9304
9882
  parentSpec,
9305
9883
  parentFile,
9306
- parentSha256: (0, import_node_fs6.existsSync)(parentFile) ? await sha256File(parentFile) : null,
9884
+ parentSha256: (0, import_node_fs7.existsSync)(parentFile) ? await sha256File(parentFile) : null,
9307
9885
  // A state and its animations keep the engine and rotations of the base.
9308
9886
  mode: resolved.character.kind === "base" ? resolved.character.mode : parentSpec.character?.mode ?? resolved.character.mode,
9309
9887
  directions: resolved.character.kind === "base" ? resolved.character.directions : parentSpec.character?.directions ?? resolved.character.directions
@@ -9321,7 +9899,7 @@ async function resolveSpecs(loaded, filter) {
9321
9899
  parentAssetId: characterParent,
9322
9900
  parentSpec,
9323
9901
  parentFile,
9324
- parentSha256: (0, import_node_fs6.existsSync)(parentFile) ? await sha256File(parentFile) : null,
9902
+ parentSha256: (0, import_node_fs7.existsSync)(parentFile) ? await sha256File(parentFile) : null,
9325
9903
  // A state and its animations keep the base's own rotation count.
9326
9904
  directions: resolved.objectPro.kind === "base" ? resolved.objectPro.directions : parentSpec.objectPro.directions
9327
9905
  };
@@ -9343,7 +9921,7 @@ async function resolveSpecs(loaded, filter) {
9343
9921
  animation: sourceSpec.character.animation,
9344
9922
  outfit: {
9345
9923
  reference: {
9346
- path: import_node_path6.default.resolve(root, asset.outfit.reference),
9924
+ path: import_node_path7.default.resolve(root, asset.outfit.reference),
9347
9925
  sha256: referenceImage.hash,
9348
9926
  width: referenceImage.width,
9349
9927
  height: referenceImage.height,
@@ -9366,23 +9944,23 @@ async function resolveSpecs(loaded, filter) {
9366
9944
  }
9367
9945
  const sourceStyleId = asset.revision.fromStyle && asset.revision.fromStyle !== styleId ? asset.revision.fromStyle : void 0;
9368
9946
  const sourceSpec = sourceStyleId ? await styleFinalizers.get(sourceStyleId)(asset.revision.from) : await finalize(asset.revision.from);
9369
- 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);
9370
9948
  const sourceMembers = await revisionSourceMembers(sourceStem, `revision source ${asset.revision.from}`);
9371
9949
  const sourceFile = sourceMembers?.[0]?.file ?? sourceStem;
9372
9950
  const sourceImage = sourceMembers ? { ...sourceMembers[0], hash: sha256(JSON.stringify(sourceMembers.map((member) => member.sha256))) } : await optionalRevisionImage(sourceFile, "revision source");
9373
9951
  const sourceFps = sourceSpec.character?.animation?.fps ?? sourceSpec.objectPro?.animation?.fps ?? sourceSpec.revision?.fps ?? sourceSpec.revision?.sourceFps;
9374
- 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;
9375
9953
  const maskImage = maskFile ? await optionalRevisionImage(maskFile, "revision mask", "png") : null;
9376
9954
  if (sourceImage && maskImage && (sourceImage.width !== maskImage.width || sourceImage.height !== maskImage.height)) {
9377
9955
  throw new Error(
9378
9956
  `Revision mask for ${styleId}/${assetId} is ${maskImage.width}x${maskImage.height}; source ${asset.revision.from} is ${sourceImage.width}x${sourceImage.height}`
9379
9957
  );
9380
9958
  }
9381
- 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;
9382
9960
  const paletteImage = paletteImageFile ? await optionalRevisionImage(paletteImageFile, "revision palette image", "png") : null;
9383
- 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;
9384
9962
  const lastFrame = lastFrameFile ? await optionalRevisionImage(lastFrameFile, "revision last frame") : null;
9385
- 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;
9386
9964
  const keypoints = keypointsFile ? await optionalSkeletonSet(keypointsFile) : null;
9387
9965
  resolved.revision = {
9388
9966
  mode: asset.revision.mode,
@@ -9552,7 +10130,7 @@ async function resolveSpecs(loaded, filter) {
9552
10130
  return specs;
9553
10131
  }
9554
10132
  function pngPath(file) {
9555
- const extension = import_node_path6.default.extname(file);
10133
+ const extension = import_node_path7.default.extname(file);
9556
10134
  return extension ? `${file.slice(0, -extension.length)}.png` : `${file}.png`;
9557
10135
  }
9558
10136
  async function resolveMapScene(root, styleId, assetId, generator, scene, provider) {
@@ -9562,12 +10140,12 @@ async function resolveMapScene(root, styleId, assetId, generator, scene, provide
9562
10140
  if (!provider.supportsMapScene) {
9563
10141
  throw new Error(`assets.${assetId}.scene: provider "${provider.id}" cannot draw into a scene; PixelLab's /map-objects can`);
9564
10142
  }
9565
- const file = import_node_path6.default.resolve(root, scene.image);
10143
+ const file = import_node_path7.default.resolve(root, scene.image);
9566
10144
  const image = await optionalRevisionImage(file, "scene image", "png");
9567
10145
  const base = { file, sha256: image?.hash ?? null, width: image?.width ?? null, height: image?.height ?? null };
9568
10146
  const placement = scene.placement;
9569
10147
  if ("mask" in placement) {
9570
- const maskFile = import_node_path6.default.resolve(root, placement.mask);
10148
+ const maskFile = import_node_path7.default.resolve(root, placement.mask);
9571
10149
  const mask = await optionalRevisionImage(maskFile, "scene mask", "png");
9572
10150
  if (image && mask && (image.width !== mask.width || image.height !== mask.height)) {
9573
10151
  throw new Error(
@@ -9581,9 +10159,38 @@ async function resolveMapScene(root, styleId, assetId, generator, scene, provide
9581
10159
  }
9582
10160
  return "oval" in placement ? { ...base, placement: { type: "oval", fraction: placement.oval } } : { ...base, placement: { type: "rectangle", fraction: placement.rectangle } };
9583
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
+ }
9584
10191
  async function optionalRevisionImage(file, label, requiredFormat) {
9585
- if (!(0, import_node_fs6.existsSync)(file)) return null;
9586
- 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);
9587
10194
  const metadata = imageMetadata(bytes);
9588
10195
  if (!metadata) throw new Error(`${label} is not a readable PNG or JPEG: ${file}`);
9589
10196
  if (requiredFormat && metadata.format !== requiredFormat) {
@@ -9592,8 +10199,8 @@ async function optionalRevisionImage(file, label, requiredFormat) {
9592
10199
  return { hash: sha256(bytes), ...metadata };
9593
10200
  }
9594
10201
  async function optionalSkeletonSet(file) {
9595
- if (!(0, import_node_fs6.existsSync)(file)) return null;
9596
- 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);
9597
10204
  let json;
9598
10205
  try {
9599
10206
  json = JSON.parse(bytes.toString("utf8"));
@@ -9603,17 +10210,17 @@ async function optionalSkeletonSet(file) {
9603
10210
  return { hash: sha256(bytes), skeleton: parseSkeletonSet(json, file) };
9604
10211
  }
9605
10212
  async function revisionSourceMembers(stem, label) {
9606
- if ((0, import_node_fs6.existsSync)(stem)) return null;
10213
+ if ((0, import_node_fs7.existsSync)(stem)) return null;
9607
10214
  const at = (role) => memberPath(stem, role, 0, 2, MediaType.PNG);
9608
10215
  let roles = [];
9609
10216
  for (const set of [CHARACTER_DIRECTIONS_8, CHARACTER_DIRECTIONS_4]) {
9610
- if (set.every((direction) => (0, import_node_fs6.existsSync)(at(direction)))) {
10217
+ if (set.every((direction) => (0, import_node_fs7.existsSync)(at(direction)))) {
9611
10218
  roles = [...set];
9612
10219
  break;
9613
10220
  }
9614
10221
  }
9615
10222
  if (!roles.length) {
9616
- 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++) {
9617
10224
  roles.push(`frame-${String(index).padStart(2, "0")}`);
9618
10225
  }
9619
10226
  }
@@ -9639,8 +10246,8 @@ async function resolveStyleImages(loaded, styleId) {
9639
10246
  if (!style) throw new Error(`Unknown style "${styleId}"`);
9640
10247
  const out = [];
9641
10248
  for (const img of style.styleImages) {
9642
- const file = import_node_path6.default.resolve(loaded.root, img.path);
9643
- 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);
9644
10251
  const metadata = imageMetadata(buf);
9645
10252
  if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${file}`);
9646
10253
  if (metadata.width < 1 || metadata.height < 1) {
@@ -9670,7 +10277,7 @@ async function resolveCharacterShape(asset, style, kind, files) {
9670
10277
  };
9671
10278
  if (asset.concept) {
9672
10279
  const image = await files.load(asset.concept, "Concept image");
9673
- 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 };
9674
10281
  }
9675
10282
  if (asset.reference) {
9676
10283
  const byDirection = typeof asset.reference === "string" ? { south: asset.reference } : asset.reference;
@@ -9678,7 +10285,7 @@ async function resolveCharacterShape(asset, style, kind, files) {
9678
10285
  for (const [direction, rel] of Object.entries(byDirection)) {
9679
10286
  const image = await files.load(rel, `Reference sprite (${direction})`);
9680
10287
  shape.reference[direction] = {
9681
- path: import_node_path6.default.resolve(files.root, rel),
10288
+ path: import_node_path7.default.resolve(files.root, rel),
9682
10289
  sha256: image.hash,
9683
10290
  width: image.width,
9684
10291
  height: image.height,
@@ -9699,7 +10306,7 @@ async function resolveCharacterShape(asset, style, kind, files) {
9699
10306
  const animationMode = asset.animation.mode ?? (asset.animation.template ? "template" : "v3");
9700
10307
  const pose = async (rel, what) => {
9701
10308
  const image = await files.load(rel, what);
9702
- 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 };
9703
10310
  };
9704
10311
  shape.animation = {
9705
10312
  mode: animationMode,
@@ -9730,7 +10337,7 @@ async function resolveObjectProShape(assetId, asset, style, kind, files) {
9730
10337
  const south = typeof asset.reference === "string" ? asset.reference : asset.reference.south;
9731
10338
  if (south) {
9732
10339
  const image = await files.load(south, "Reference sprite");
9733
- 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 };
9734
10341
  }
9735
10342
  }
9736
10343
  if (asset.state) {
@@ -9758,7 +10365,7 @@ async function resolveObjectProShape(assetId, asset, style, kind, files) {
9758
10365
  }
9759
10366
  const pose = async (rel, what) => {
9760
10367
  const image = await files.load(rel, what);
9761
- 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 };
9762
10369
  };
9763
10370
  shape.animation = {
9764
10371
  mode: asset.animation.mode ?? "v3",
@@ -9778,11 +10385,11 @@ function characterDirections(directions) {
9778
10385
  }
9779
10386
 
9780
10387
  // src/project.ts
9781
- var import_node_path14 = __toESM(require("path"), 1);
10388
+ var import_node_path15 = __toESM(require("path"), 1);
9782
10389
 
9783
10390
  // src/env.ts
9784
- var import_node_fs7 = require("fs");
9785
- var import_node_path7 = __toESM(require("path"), 1);
10391
+ var import_node_fs8 = require("fs");
10392
+ var import_node_path8 = __toESM(require("path"), 1);
9786
10393
  function loadEnvFiles(dir) {
9787
10394
  const loaded = [];
9788
10395
  for (const [file, vars] of envFiles(dir)) {
@@ -9796,10 +10403,10 @@ function loadEnvFiles(dir) {
9796
10403
  function envFiles(dir) {
9797
10404
  const out = [];
9798
10405
  for (const name of [".env.local", ".env"]) {
9799
- const file = import_node_path7.default.join(dir, name);
9800
- 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;
9801
10408
  try {
9802
- out.push([file, parseEnv((0, import_node_fs7.readFileSync)(file, "utf8"))]);
10409
+ out.push([file, parseEnv((0, import_node_fs8.readFileSync)(file, "utf8"))]);
9803
10410
  } catch {
9804
10411
  }
9805
10412
  }
@@ -9825,12 +10432,12 @@ function parseEnv(contents) {
9825
10432
  }
9826
10433
 
9827
10434
  // src/pipeline/plan.ts
9828
- var import_node_fs10 = require("fs");
9829
- var import_node_path13 = __toESM(require("path"), 1);
10435
+ var import_node_fs11 = require("fs");
10436
+ var import_node_path14 = __toESM(require("path"), 1);
9830
10437
 
9831
10438
  // src/pipeline/mirror.ts
9832
- var import_promises4 = require("fs/promises");
9833
- var import_node_path8 = __toESM(require("path"), 1);
10439
+ var import_promises5 = require("fs/promises");
10440
+ var import_node_path9 = __toESM(require("path"), 1);
9834
10441
 
9835
10442
  // src/palette.ts
9836
10443
  function normalizePalette(values) {
@@ -9967,12 +10574,12 @@ async function deriveMirror(spec, lock, lockPath, opts = {}) {
9967
10574
  const outputs = [];
9968
10575
  const written = [];
9969
10576
  for (const [index, member] of members.entries()) {
9970
- const bytes = flipHorizontal(await (0, import_promises4.readFile)(member.file));
10577
+ const bytes = flipHorizontal(await (0, import_promises5.readFile)(member.file));
9971
10578
  const target = expectedOutputPath(spec, member.role, index, members.length, MediaType.PNG);
9972
- 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 });
9973
10580
  const tmp = `${target}.pixelkiln.tmp`;
9974
- await (0, import_promises4.writeFile)(tmp, bytes);
9975
- await (0, import_promises4.rename)(tmp, target);
10581
+ await (0, import_promises5.writeFile)(tmp, bytes);
10582
+ await (0, import_promises5.rename)(tmp, target);
9976
10583
  written.push(target);
9977
10584
  outputs.push({
9978
10585
  path: portableOutputPath(target, spec.root),
@@ -10031,20 +10638,20 @@ async function deriveMirror(spec, lock, lockPath, opts = {}) {
10031
10638
  }
10032
10639
 
10033
10640
  // src/pipeline/quality-profile.ts
10034
- var import_node_fs8 = require("fs");
10035
- var import_node_path11 = __toESM(require("path"), 1);
10641
+ var import_node_fs9 = require("fs");
10642
+ var import_node_path12 = __toESM(require("path"), 1);
10036
10643
 
10037
10644
  // src/pipeline/refine.ts
10038
- var import_node_path10 = __toESM(require("path"), 1);
10645
+ var import_node_path11 = __toESM(require("path"), 1);
10039
10646
  var import_node_child_process2 = require("child_process");
10040
- var import_promises6 = require("fs/promises");
10647
+ var import_promises7 = require("fs/promises");
10041
10648
  var import_node_os3 = require("os");
10042
10649
  var import_node_util = require("util");
10043
10650
 
10044
10651
  // src/artifacts.ts
10045
- var import_node_path9 = __toESM(require("path"), 1);
10652
+ var import_node_path10 = __toESM(require("path"), 1);
10046
10653
  var import_node_crypto6 = require("crypto");
10047
- var import_promises5 = require("fs/promises");
10654
+ var import_promises6 = require("fs/promises");
10048
10655
  var activeTransactions = /* @__PURE__ */ new Set();
10049
10656
  function message(error) {
10050
10657
  return error instanceof Error ? error.message : String(error);
@@ -10053,7 +10660,7 @@ function digest(data) {
10053
10660
  return (0, import_node_crypto6.createHash)("sha256").update(data).digest("hex");
10054
10661
  }
10055
10662
  function portableRelative(from, to) {
10056
- 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("/") || ".";
10057
10664
  }
10058
10665
  function canonical2(value) {
10059
10666
  if (value === null || typeof value === "string" || typeof value === "boolean") return value;
@@ -10103,12 +10710,12 @@ function parseArtifactManifest(absolute, data) {
10103
10710
  return raw;
10104
10711
  }
10105
10712
  async function readArtifactBundleManifest(manifestPath) {
10106
- const absolute = import_node_path9.default.resolve(manifestPath);
10107
- 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"));
10108
10715
  }
10109
10716
  async function readOptional(file) {
10110
10717
  try {
10111
- return await (0, import_promises5.readFile)(file);
10718
+ return await (0, import_promises6.readFile)(file);
10112
10719
  } catch (error) {
10113
10720
  if (isCode(error, "ENOENT")) return null;
10114
10721
  throw error;
@@ -10127,8 +10734,8 @@ function processIsAlive(pid) {
10127
10734
  }
10128
10735
  }
10129
10736
  function validTemporaryPath(candidate, destination, type) {
10130
- return import_node_path9.default.dirname(candidate) === import_node_path9.default.dirname(destination) && import_node_path9.default.basename(candidate).startsWith(
10131
- `.${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}-`
10132
10739
  );
10133
10740
  }
10134
10741
  function parseTransaction(journal, data) {
@@ -10144,14 +10751,14 @@ function parseTransaction(journal, data) {
10144
10751
  return raw;
10145
10752
  }
10146
10753
  async function removeTransactionFiles(journal) {
10147
- await (0, import_promises5.rm)(journal, { force: true });
10148
- 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 });
10149
10756
  }
10150
10757
  async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
10151
- const journal = import_node_path9.default.resolve(recoveryFile);
10758
+ const journal = import_node_path10.default.resolve(recoveryFile);
10152
10759
  const bytes = await readOptional(journal);
10153
10760
  if (!bytes) {
10154
- await (0, import_promises5.rm)(transactionMarker(journal), { force: true });
10761
+ await (0, import_promises6.rm)(transactionMarker(journal), { force: true });
10155
10762
  return;
10156
10763
  }
10157
10764
  if (activeTransactions.has(journal)) {
@@ -10164,7 +10771,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
10164
10771
  );
10165
10772
  }
10166
10773
  for (const entry of transaction.entries) {
10167
- 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")) {
10168
10775
  throw new Error(
10169
10776
  `Refusing unsafe artifact recovery from ${journal}; its destinations or temporary paths do not match the current bundle.`
10170
10777
  );
@@ -10190,7 +10797,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
10190
10797
  }
10191
10798
  await remove2(entry.destination, errors);
10192
10799
  try {
10193
- await (0, import_promises5.rename)(entry.backup, entry.destination);
10800
+ await (0, import_promises6.rename)(entry.backup, entry.destination);
10194
10801
  } catch (error) {
10195
10802
  errors.push(
10196
10803
  `could not restore ${entry.destination}; previous file remains at ${entry.backup}: ${message(error)}`
@@ -10210,7 +10817,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
10210
10817
  await removeTransactionFiles(journal);
10211
10818
  }
10212
10819
  function createArtifactBundleManifest(manifestPath, outputs, provenance) {
10213
- 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));
10214
10821
  const sources = provenance.sources.map((source) => ({
10215
10822
  ...source,
10216
10823
  path: portableRelative(root, source.path)
@@ -10240,11 +10847,11 @@ function withArtifactManifest(manifestPath, outputs, provenance) {
10240
10847
  ];
10241
10848
  }
10242
10849
  async function writeManagedArtifactBundle(manifestPath, outputs, provenance, options = {}) {
10243
- const absoluteManifest = import_node_path9.default.resolve(manifestPath);
10850
+ const absoluteManifest = import_node_path10.default.resolve(manifestPath);
10244
10851
  const recoveryFile = `${absoluteManifest}.transaction`;
10245
10852
  const allowedDestinations = /* @__PURE__ */ new Set([
10246
10853
  absoluteManifest,
10247
- ...outputs.map((output) => import_node_path9.default.resolve(output.path))
10854
+ ...outputs.map((output) => import_node_path10.default.resolve(output.path))
10248
10855
  ]);
10249
10856
  await recoverArtifactTransaction(recoveryFile, allowedDestinations);
10250
10857
  const existingManifestBytes = await readOptional(absoluteManifest);
@@ -10262,13 +10869,13 @@ async function writeManagedArtifactBundle(manifestPath, outputs, provenance, opt
10262
10869
  }
10263
10870
  }
10264
10871
  if (!options.force) {
10265
- const root = import_node_path9.default.dirname(absoluteManifest);
10872
+ const root = import_node_path10.default.dirname(absoluteManifest);
10266
10873
  const recorded = new Map(
10267
- 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]) ?? []
10268
10875
  );
10269
10876
  const conflicts = [];
10270
10877
  for (const output of outputs) {
10271
- const destination = import_node_path9.default.resolve(output.path);
10878
+ const destination = import_node_path10.default.resolve(output.path);
10272
10879
  const current = await readOptional(destination);
10273
10880
  if (!current || digest(current) === digest(output.data)) continue;
10274
10881
  const expected = recorded.get(destination);
@@ -10290,15 +10897,15 @@ Pass { force: true } (CLI: --force) to take ownership and replace the complete b
10290
10897
  );
10291
10898
  }
10292
10899
  async function verifyArtifactBundle(manifestPath) {
10293
- const absolute = import_node_path9.default.resolve(manifestPath);
10900
+ const absolute = import_node_path10.default.resolve(manifestPath);
10294
10901
  const raw = await readArtifactBundleManifest(absolute);
10295
- const root = import_node_path9.default.dirname(absolute);
10902
+ const root = import_node_path10.default.dirname(absolute);
10296
10903
  const changedSources = [];
10297
10904
  const changedOutputs = [];
10298
10905
  for (const source of raw.sources) {
10299
10906
  let actual = null;
10300
10907
  try {
10301
- 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)));
10302
10909
  } catch (error) {
10303
10910
  if (!isCode(error, "ENOENT")) throw error;
10304
10911
  }
@@ -10306,7 +10913,7 @@ async function verifyArtifactBundle(manifestPath) {
10306
10913
  }
10307
10914
  for (const output of raw.outputs) {
10308
10915
  try {
10309
- 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) {
10310
10917
  changedOutputs.push(output.path);
10311
10918
  }
10312
10919
  } catch (error) {
@@ -10332,7 +10939,7 @@ function isCode(error, code) {
10332
10939
  }
10333
10940
  async function remove2(file, errors) {
10334
10941
  try {
10335
- await (0, import_promises5.rm)(file, { force: true });
10942
+ await (0, import_promises6.rm)(file, { force: true });
10336
10943
  } catch (error) {
10337
10944
  errors?.push(`could not remove ${file}: ${message(error)}`);
10338
10945
  }
@@ -10345,7 +10952,7 @@ async function rollback(prepared) {
10345
10952
  for (const artifact of [...prepared].reverse()) {
10346
10953
  if (!artifact.backup || !artifact.backedUp) continue;
10347
10954
  try {
10348
- await (0, import_promises5.rename)(artifact.backup, artifact.destination);
10955
+ await (0, import_promises6.rename)(artifact.backup, artifact.destination);
10349
10956
  artifact.backedUp = false;
10350
10957
  } catch (error) {
10351
10958
  errors.push(
@@ -10362,7 +10969,7 @@ async function writeArtifactBundle(files, options = {}) {
10362
10969
  if (!files.length) throw new Error("An artifact bundle must contain at least one file.");
10363
10970
  const normalized = files.map((file) => {
10364
10971
  if (!file.path.trim()) throw new Error("Artifact paths cannot be empty.");
10365
- 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) };
10366
10973
  });
10367
10974
  const destinations = /* @__PURE__ */ new Set();
10368
10975
  for (const artifact of normalized) {
@@ -10378,7 +10985,7 @@ async function writeArtifactBundle(files, options = {}) {
10378
10985
  const unchanged = [];
10379
10986
  for (const artifact of normalized) {
10380
10987
  try {
10381
- const current = await (0, import_promises5.readFile)(artifact.destination);
10988
+ const current = await (0, import_promises6.readFile)(artifact.destination);
10382
10989
  if (current.equals(artifact.data)) {
10383
10990
  unchanged.push(artifact.destination);
10384
10991
  } else {
@@ -10392,21 +10999,21 @@ async function writeArtifactBundle(files, options = {}) {
10392
10999
  if (!changed.length) return { changed: [], unchanged };
10393
11000
  const token = (0, import_node_crypto6.randomUUID)();
10394
11001
  for (const [index, artifact] of changed.entries()) {
10395
- const basename = import_node_path9.default.basename(artifact.destination);
10396
- artifact.stage = import_node_path9.default.join(
10397
- 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),
10398
11005
  `.${basename}.pixelkiln-stage-${token}-${index}`
10399
11006
  );
10400
11007
  if (artifact.existed) {
10401
- artifact.backup = import_node_path9.default.join(
10402
- import_node_path9.default.dirname(artifact.destination),
11008
+ artifact.backup = import_node_path10.default.join(
11009
+ import_node_path10.default.dirname(artifact.destination),
10403
11010
  `.${basename}.pixelkiln-backup-${token}-${index}`
10404
11011
  );
10405
11012
  }
10406
11013
  }
10407
- 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;
10408
11015
  if (journal) {
10409
- 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 });
10410
11017
  const transaction = {
10411
11018
  format: "pixelkiln-artifact-transaction",
10412
11019
  version: 1,
@@ -10420,7 +11027,7 @@ async function writeArtifactBundle(files, options = {}) {
10420
11027
  }))
10421
11028
  };
10422
11029
  try {
10423
- 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" });
10424
11031
  activeTransactions.add(journal);
10425
11032
  } catch (error) {
10426
11033
  throw new Error(
@@ -10431,9 +11038,9 @@ async function writeArtifactBundle(files, options = {}) {
10431
11038
  }
10432
11039
  try {
10433
11040
  for (const [index, artifact] of changed.entries()) {
10434
- 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 });
10435
11042
  await options.beforeStage?.(artifact.destination, index);
10436
- await (0, import_promises5.writeFile)(artifact.stage, artifact.data, { flag: "wx" });
11043
+ await (0, import_promises6.writeFile)(artifact.stage, artifact.data, { flag: "wx" });
10437
11044
  }
10438
11045
  } catch (error) {
10439
11046
  const cleanupErrors2 = await rollback(changed);
@@ -10446,17 +11053,17 @@ async function writeArtifactBundle(files, options = {}) {
10446
11053
  try {
10447
11054
  for (const artifact of changed) {
10448
11055
  if (!artifact.existed) continue;
10449
- await (0, import_promises5.rename)(artifact.destination, artifact.backup);
11056
+ await (0, import_promises6.rename)(artifact.destination, artifact.backup);
10450
11057
  artifact.backedUp = true;
10451
11058
  }
10452
11059
  for (const [index, artifact] of changed.entries()) {
10453
11060
  await options.beforePromote?.(artifact.destination, index);
10454
- await (0, import_promises5.rename)(artifact.stage, artifact.destination);
11061
+ await (0, import_promises6.rename)(artifact.stage, artifact.destination);
10455
11062
  artifact.stage = void 0;
10456
11063
  artifact.promoted = true;
10457
11064
  }
10458
11065
  if (journal) {
10459
- await (0, import_promises5.writeFile)(transactionMarker(journal), `${token}
11066
+ await (0, import_promises6.writeFile)(transactionMarker(journal), `${token}
10460
11067
  `, { flag: "wx" });
10461
11068
  durableCommit = true;
10462
11069
  await options.afterCommit?.();
@@ -10697,16 +11304,16 @@ function sameFrameGrid(first, next) {
10697
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;
10698
11305
  }
10699
11306
  async function refineAsset(options) {
10700
- const source = import_node_path10.default.resolve(options.source);
10701
- 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);
10702
11309
  if (source === output) throw new Error("Refinement source and output must be different files.");
10703
- const record = import_node_path10.default.resolve(qualityRecordPath(output));
11310
+ const record = import_node_path11.default.resolve(qualityRecordPath(output));
10704
11311
  const palette = normalizePalette(options.palette).map((color) => color.hex);
10705
11312
  const minGridConfidence = requireConfidence(options.minGridConfidence ?? "high");
10706
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.");
10707
11314
  let sourcePng;
10708
11315
  try {
10709
- sourcePng = decodePng(await (0, import_promises6.readFile)(source));
11316
+ sourcePng = decodePng(await (0, import_promises7.readFile)(source));
10710
11317
  } catch (error) {
10711
11318
  throw new Error(`Cannot read refinement source ${source}: ${message2(error)}`, { cause: error });
10712
11319
  }
@@ -10723,8 +11330,8 @@ async function refineAsset(options) {
10723
11330
  );
10724
11331
  }
10725
11332
  }
10726
- const temp = await (0, import_promises6.mkdtemp)(import_node_path10.default.join((0, import_node_os3.tmpdir)(), "pixelkiln-refine-"));
10727
- 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");
10728
11335
  const fixerCommand = compatibilityCommand ?? options.fixerPython ?? process.env.PIXELKILN_PIXEL_FIXER_PYTHON ?? "python3";
10729
11336
  const args = compatibilityCommand ? [...options.fixerArgsPrefix ?? [], source, "--extract", recovered] : ["-c", PYTHON_FIXER_BRIDGE, source, recovered];
10730
11337
  try {
@@ -10745,7 +11352,7 @@ async function refineAsset(options) {
10745
11352
  );
10746
11353
  }
10747
11354
  const detection = parseDetection(firstJsonObject(stdout));
10748
- const native = decodePng(await (0, import_promises6.readFile)(recovered));
11355
+ const native = decodePng(await (0, import_promises7.readFile)(recovered));
10749
11356
  const quantized = quantizeToPalette(native, palette);
10750
11357
  const png = encodeRgbaPng(quantized.width, quantized.height, quantized.pixels);
10751
11358
  const audit = auditRefinement(
@@ -10795,7 +11402,7 @@ async function refineAsset(options) {
10795
11402
  );
10796
11403
  return { source, output, record, detection, palette, audit };
10797
11404
  } finally {
10798
- await (0, import_promises6.rm)(temp, { recursive: true, force: true });
11405
+ await (0, import_promises7.rm)(temp, { recursive: true, force: true });
10799
11406
  }
10800
11407
  }
10801
11408
  async function refineFrameSet(options) {
@@ -10809,18 +11416,18 @@ async function refineFrameSet(options) {
10809
11416
  if (roles.some((role) => !role.trim()) || new Set(roles).size !== roles.length) {
10810
11417
  throw new Error("Frame-set roles must be non-empty and unique.");
10811
11418
  }
10812
- const output = import_node_path10.default.resolve(options.output);
10813
- 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));
10814
11421
  const palette = normalizeRefinementPalette(options.palette);
10815
11422
  const finalOutputs = options.sources.map((source, index) => qualityFrameOutputPath(output, source.role, index));
10816
11423
  if (new Set(finalOutputs).size !== finalOutputs.length) {
10817
11424
  throw new Error("Frame-set roles resolve to duplicate output filenames.");
10818
11425
  }
10819
- 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)));
10820
11427
  if (finalOutputs.some((file) => sourceSet.has(file))) {
10821
11428
  throw new Error("Frame-set refinement sources and outputs must be different files.");
10822
11429
  }
10823
- 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-"));
10824
11431
  try {
10825
11432
  const frames = [];
10826
11433
  const outputs = [];
@@ -10828,8 +11435,8 @@ async function refineFrameSet(options) {
10828
11435
  const sourcePaths = [];
10829
11436
  let firstOptions = null;
10830
11437
  for (const [index, sourceEntry] of options.sources.entries()) {
10831
- const source = import_node_path10.default.resolve(sourceEntry.path);
10832
- 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`);
10833
11440
  const refined = await refineAsset({
10834
11441
  source,
10835
11442
  output: temporaryOutput,
@@ -10861,7 +11468,7 @@ async function refineFrameSet(options) {
10861
11468
  });
10862
11469
  outputs.push({
10863
11470
  path: finalOutputs[index],
10864
- data: await (0, import_promises6.readFile)(temporaryOutput)
11471
+ data: await (0, import_promises7.readFile)(temporaryOutput)
10865
11472
  });
10866
11473
  }
10867
11474
  const first = frames[0];
@@ -10904,23 +11511,23 @@ async function refineFrameSet(options) {
10904
11511
  );
10905
11512
  return {
10906
11513
  sources: sourcePaths,
10907
- outputs: outputs.map((item) => import_node_path10.default.resolve(item.path)),
11514
+ outputs: outputs.map((item) => import_node_path11.default.resolve(item.path)),
10908
11515
  record,
10909
11516
  frames,
10910
11517
  palette,
10911
11518
  audit
10912
11519
  };
10913
11520
  } finally {
10914
- await (0, import_promises6.rm)(temp, { recursive: true, force: true });
11521
+ await (0, import_promises7.rm)(temp, { recursive: true, force: true });
10915
11522
  }
10916
11523
  }
10917
11524
  async function checkQualityRecord(recordPath) {
10918
- const record = import_node_path10.default.resolve(recordPath);
11525
+ const record = import_node_path11.default.resolve(recordPath);
10919
11526
  const manifest = await readArtifactBundleManifest(record);
10920
11527
  const options = parseRecordOptions(manifest, record);
10921
11528
  const verification = await verifyArtifactBundle(record);
10922
- const sources = manifest.sources.map((source2) => import_node_path10.default.resolve(import_node_path10.default.dirname(record), source2.path));
10923
- 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));
10924
11531
  const source = sources[0] ?? "";
10925
11532
  const output = outputs[0] ?? "";
10926
11533
  const expectedCount = options.frameSet?.count ?? 1;
@@ -10953,7 +11560,7 @@ async function checkQualityRecord(recordPath) {
10953
11560
  async function approveQualityRecord(recordPath, approval) {
10954
11561
  const reviewer = approval.reviewer.trim();
10955
11562
  if (!reviewer) throw new Error("A human reviewer name is required.");
10956
- const record = import_node_path10.default.resolve(recordPath);
11563
+ const record = import_node_path11.default.resolve(recordPath);
10957
11564
  const manifest = await readArtifactBundleManifest(record);
10958
11565
  const options = parseRecordOptions(manifest, record);
10959
11566
  const verification = await verifyArtifactBundle(record);
@@ -10974,14 +11581,14 @@ async function approveQualityRecord(recordPath, approval) {
10974
11581
  `A refinement record must contain exactly ${expectedCount} source(s) and output(s).`
10975
11582
  );
10976
11583
  }
10977
- const root = import_node_path10.default.dirname(record);
11584
+ const root = import_node_path11.default.dirname(record);
10978
11585
  const outputs = await Promise.all(manifest.outputs.map(async (entry) => {
10979
- const outputPath = import_node_path10.default.resolve(root, entry.path);
10980
- 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) };
10981
11588
  }));
10982
11589
  const sources = manifest.sources.map((source) => ({
10983
11590
  ...source,
10984
- path: import_node_path10.default.resolve(root, source.path)
11591
+ path: import_node_path11.default.resolve(root, source.path)
10985
11592
  }));
10986
11593
  const approved = {
10987
11594
  ...options,
@@ -11013,8 +11620,8 @@ async function qualitySource(spec, lock) {
11013
11620
  if (spec.generator === "frames") {
11014
11621
  return { items: null, reason: "frame-set quality requires ordered downloaded provider outputs" };
11015
11622
  }
11016
- const source = import_node_path11.default.resolve(spec.root, spec.source);
11017
- 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}` };
11018
11625
  }
11019
11626
  const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
11020
11627
  if (!entry) return { items: null, reason: "raw provider output is not in the lockfile" };
@@ -11043,7 +11650,7 @@ async function qualitySource(spec, lock) {
11043
11650
  }
11044
11651
  if (role) roles.add(role);
11045
11652
  const source = currentEntryOutputPath(entry, spec, index);
11046
- 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}` };
11047
11654
  if (await sha256File(source) !== output.sha256) {
11048
11655
  return { items: null, reason: "raw provider output was modified after download" };
11049
11656
  }
@@ -11062,7 +11669,7 @@ async function inspectQualityProfile(spec, lock) {
11062
11669
  }
11063
11670
  const sourcePaths = source.items.map((item) => item.path);
11064
11671
  const expectedOutputs = spec.generator === "frames" ? source.items.map((item, index) => qualityFrameOutputPath(output, item.role, index)) : [output];
11065
- if (!(0, import_node_fs8.existsSync)(record)) {
11672
+ if (!(0, import_node_fs9.existsSync)(record)) {
11066
11673
  return {
11067
11674
  key,
11068
11675
  state: "needs-refinement",
@@ -11080,7 +11687,7 @@ async function inspectQualityProfile(spec, lock) {
11080
11687
  const expectedRevision = spec.quality.fixerRevision ?? PIXEL_ART_FIXER_REVISION;
11081
11688
  const expectedTransparency = spec.quality.minTransparency ?? null;
11082
11689
  const expectedFrameSet = spec.generator === "frames" ? { fps: spec.quality.fps ?? 12, count: source.items.length, roles: source.items.map((item) => item.role) } : null;
11083
- 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);
11084
11691
  if (contractChanged) {
11085
11692
  return {
11086
11693
  key,
@@ -11243,8 +11850,8 @@ async function requireApprovedQualitySources(specs, lock, assetIds, outputRoles)
11243
11850
  }
11244
11851
 
11245
11852
  // src/pipeline/revision.ts
11246
- var import_node_fs9 = require("fs");
11247
- var import_node_path12 = __toESM(require("path"), 1);
11853
+ var import_node_fs10 = require("fs");
11854
+ var import_node_path13 = __toESM(require("path"), 1);
11248
11855
  async function inspectRevisionReadiness(spec, lock) {
11249
11856
  if (spec.mirror) return inspectMirror(spec, lock, /* @__PURE__ */ new Set());
11250
11857
  if (spec.character?.kind === "outfit") return inspectOutfit(spec, lock, /* @__PURE__ */ new Set());
@@ -11252,6 +11859,7 @@ async function inspectRevisionReadiness(spec, lock) {
11252
11859
  if (spec.character?.styleAnchor) return inspectStyleAnchor(spec, lock, /* @__PURE__ */ new Set());
11253
11860
  if (spec.objectPro?.parentSpec) return inspectObjectProParent(spec, lock, /* @__PURE__ */ new Set());
11254
11861
  if (spec.scene) return inspectScene(spec);
11862
+ if (spec.vfx?.sprite && "file" in spec.vfx.sprite) return inspectVfxSprite(spec);
11255
11863
  if (!spec.revision) return null;
11256
11864
  return inspectRevision(spec, lock, /* @__PURE__ */ new Set());
11257
11865
  }
@@ -11260,7 +11868,7 @@ async function inspectStyleAnchor(spec, lock, seen) {
11260
11868
  const label = `style anchor ${spec.styleId}/${anchor.assetId}`;
11261
11869
  const dependency = await inspectParent(anchor.spec, lock, seen);
11262
11870
  if (!dependency.ready) return { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
11263
- 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` };
11264
11872
  if (await sha256File(anchor.file) !== anchor.sha256) {
11265
11873
  return { ready: false, reason: `${label} changed after the manifest was resolved` };
11266
11874
  }
@@ -11274,11 +11882,17 @@ async function inspectScene(spec) {
11274
11882
  const files = [{ label: "scene image", file: scene.file, sha256: scene.sha256 }];
11275
11883
  if (scene.placement.type === "mask") files.push({ label: "scene mask", file: scene.placement.file, sha256: scene.placement.sha256 });
11276
11884
  for (const { label, file, sha256: sha2562 } of files) {
11277
- 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}` };
11278
11886
  if (await sha256File(file) !== sha2562) return { ready: false, reason: `${label} changed after the manifest was resolved` };
11279
11887
  }
11280
11888
  return { ready: true, reason: "scene is current" };
11281
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
+ }
11282
11896
  async function inspectMirror(spec, lock, seen) {
11283
11897
  const source = spec.mirror.sourceSpec;
11284
11898
  const label = `mirror source ${spec.styleId}/${spec.mirror.sourceAssetId}`;
@@ -11291,7 +11905,7 @@ async function inspectOutfit(spec, lock, seen) {
11291
11905
  const label = `outfit source ${spec.styleId}/${character.parentAssetId}`;
11292
11906
  const dependency = await inspectParent(character.parentSpec, lock, seen);
11293
11907
  if (!dependency.ready) return { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
11294
- if (!(0, import_node_fs9.existsSync)(outfit.reference.path)) {
11908
+ if (!(0, import_node_fs10.existsSync)(outfit.reference.path)) {
11295
11909
  return { ready: false, reason: `outfit reference is missing: ${outfit.reference.path}` };
11296
11910
  }
11297
11911
  if (await sha256File(outfit.reference.path) !== outfit.reference.sha256) {
@@ -11305,7 +11919,7 @@ async function inspectCharacterParent(spec, lock, seen) {
11305
11919
  const label = `${character.kind} parent ${spec.styleId}/${character.parentAssetId}`;
11306
11920
  const dependency = await inspectParent(parentSpec, lock, seen);
11307
11921
  if (!dependency.ready) return { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
11308
- 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)) {
11309
11923
  return { ready: false, reason: `${label} has no generated south-facing file yet` };
11310
11924
  }
11311
11925
  if (await sha256File(character.parentFile) !== character.parentSha256) {
@@ -11323,7 +11937,7 @@ async function inspectObjectProParent(spec, lock, seen) {
11323
11937
  const label = `${object.kind} parent ${spec.styleId}/${object.parentAssetId}`;
11324
11938
  const dependency = await inspectParent(parentSpec, lock, seen);
11325
11939
  if (!dependency.ready) return { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
11326
- 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)) {
11327
11941
  return { ready: false, reason: `${label} has no generated south-facing file yet` };
11328
11942
  }
11329
11943
  if (await sha256File(object.parentFile) !== object.parentSha256) {
@@ -11349,7 +11963,7 @@ async function inspectRevision(spec, lock, seen) {
11349
11963
  const members = await inspectSourceMembers(spec, lock);
11350
11964
  if (!members.ready) return members;
11351
11965
  } else {
11352
- if (!revision.sourceSha256 || !(0, import_node_fs9.existsSync)(revision.sourceFile)) {
11966
+ if (!revision.sourceSha256 || !(0, import_node_fs10.existsSync)(revision.sourceFile)) {
11353
11967
  return { ready: false, reason: `revision source is missing: ${revision.sourceFile}` };
11354
11968
  }
11355
11969
  if (await sha256File(revision.sourceFile) !== revision.sourceSha256) {
@@ -11357,7 +11971,7 @@ async function inspectRevision(spec, lock, seen) {
11357
11971
  }
11358
11972
  }
11359
11973
  if (revision.maskFile) {
11360
- if (!revision.maskSha256 || !(0, import_node_fs9.existsSync)(revision.maskFile)) {
11974
+ if (!revision.maskSha256 || !(0, import_node_fs10.existsSync)(revision.maskFile)) {
11361
11975
  return { ready: false, reason: `revision mask is missing: ${revision.maskFile}` };
11362
11976
  }
11363
11977
  if (await sha256File(revision.maskFile) !== revision.maskSha256) {
@@ -11365,7 +11979,7 @@ async function inspectRevision(spec, lock, seen) {
11365
11979
  }
11366
11980
  }
11367
11981
  if (revision.lastFrameFile) {
11368
- if (!revision.lastFrameSha256 || !(0, import_node_fs9.existsSync)(revision.lastFrameFile)) {
11982
+ if (!revision.lastFrameSha256 || !(0, import_node_fs10.existsSync)(revision.lastFrameFile)) {
11369
11983
  return { ready: false, reason: `revision last frame is missing: ${revision.lastFrameFile}` };
11370
11984
  }
11371
11985
  if (await sha256File(revision.lastFrameFile) !== revision.lastFrameSha256) {
@@ -11378,7 +11992,7 @@ async function inspectSourceMembers(spec, lock) {
11378
11992
  const revision = spec.revision;
11379
11993
  const members = revision.sourceMembers;
11380
11994
  for (const member of members) {
11381
- 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}` };
11382
11996
  if (await sha256File(member.file) !== member.sha256) {
11383
11997
  return { ready: false, reason: `revision source member changed after the manifest was resolved: ${member.file}` };
11384
11998
  }
@@ -11387,8 +12001,8 @@ async function inspectSourceMembers(spec, lock) {
11387
12001
  if (parent.source) return { ready: true, reason: "declared source set is present" };
11388
12002
  const entry = lock.entries[lockKey(parent.styleId, parent.assetId)];
11389
12003
  if (!entry) return { ready: false, reason: "revision source is not in the lockfile" };
11390
- const recorded = entry.outputs.map((_, index) => import_node_path12.default.resolve(currentEntryOutputPath(entry, parent, index))).sort();
11391
- 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();
11392
12006
  if (recorded.length !== found.length || recorded.some((file, index) => file !== found[index])) {
11393
12007
  return {
11394
12008
  ready: false,
@@ -11437,8 +12051,8 @@ async function inspectParent(spec, lock, seen) {
11437
12051
  };
11438
12052
  }
11439
12053
  if (spec.source) {
11440
- const source = import_node_path12.default.resolve(spec.root, spec.source);
11441
- 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}` };
11442
12056
  }
11443
12057
  const entry = lock.entries[key];
11444
12058
  if (!entry) return { ready: false, reason: "parent is not in the lockfile" };
@@ -11452,7 +12066,7 @@ async function inspectParent(spec, lock, seen) {
11452
12066
  for (let index = 0; index < entry.outputs.length; index++) {
11453
12067
  const output = entry.outputs[index];
11454
12068
  const file = currentEntryOutputPath(entry, spec, index);
11455
- 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}` };
11456
12070
  if (await sha256File(file) !== output.sha256) {
11457
12071
  return { ready: false, reason: `parent output was modified: ${file}` };
11458
12072
  }
@@ -11515,8 +12129,8 @@ async function buildPlan(specs, lock, opts = {}) {
11515
12129
  } else if (opts.force) {
11516
12130
  state = "missing";
11517
12131
  reason = "--force";
11518
- } else if (!entry && spec.source && spec.generator !== "frames") {
11519
- 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))) {
11520
12134
  state = "ok";
11521
12135
  reason = `placed from ${spec.source}; not generated`;
11522
12136
  } else {
@@ -11524,7 +12138,7 @@ async function buildPlan(specs, lock, opts = {}) {
11524
12138
  reason = `declared source is missing: ${spec.source}`;
11525
12139
  }
11526
12140
  } else if (!entry) {
11527
- if ((0, import_node_fs10.existsSync)(spec.outFile)) {
12141
+ if ((0, import_node_fs11.existsSync)(spec.outFile)) {
11528
12142
  state = "untracked";
11529
12143
  reason = "file on disk, no provenance recorded";
11530
12144
  } else {
@@ -11537,7 +12151,7 @@ async function buildPlan(specs, lock, opts = {}) {
11537
12151
  } else if (spec.mirror && !mirrorCurrent(entry, lock.entries[lockKey(spec.styleId, spec.mirror.sourceAssetId)])) {
11538
12152
  state = "stale";
11539
12153
  reason = `mirror source ${spec.mirror.sourceAssetId} changed; flip it again (no generation cost)`;
11540
- } 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)))) {
11541
12155
  state = "stale";
11542
12156
  reason = "mirror output missing on disk; flip it again (no generation cost)";
11543
12157
  } else if (spec.character?.kind === "outfit" && entry.outfit?.sourceSha256 !== mirrorSourceHash(lock.entries[lockKey(spec.styleId, spec.character.parentAssetId)] ?? { outputs: [] })) {
@@ -11565,9 +12179,9 @@ async function buildPlan(specs, lock, opts = {}) {
11565
12179
  } else if (entry.status !== "downloaded") {
11566
12180
  state = "in-flight";
11567
12181
  reason = `awaiting ${entry.status}`;
11568
- } 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)))) {
11569
12183
  state = "orphaned";
11570
- 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(", ")}`;
11571
12185
  } else if (await anyOutputModified(entry, spec)) {
11572
12186
  state = "orphaned";
11573
12187
  reason = "output modified since download";
@@ -11658,13 +12272,13 @@ function summarize(plan) {
11658
12272
 
11659
12273
  // src/project.ts
11660
12274
  async function openProject(manifestPath = "pixelkiln.manifest.json", opts = {}) {
11661
- const absolute = import_node_path14.default.resolve(manifestPath);
11662
- 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);
11663
12277
  if (opts.env !== false) {
11664
12278
  loadEnvFiles(root);
11665
- 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());
11666
12280
  }
11667
- 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"));
11668
12282
  const loaded = await loadManifest(absolute);
11669
12283
  const specs = await resolveSpecs(loaded, { styles: opts.styles ?? [], assets: opts.assets ?? [] });
11670
12284
  const lock = await loadLock(lockPath);
@@ -11731,9 +12345,9 @@ function shouldPersistSourceUrl(value) {
11731
12345
  }
11732
12346
 
11733
12347
  // src/recipes.ts
11734
- var import_node_path15 = __toESM(require("path"), 1);
11735
- var import_node_fs11 = require("fs");
11736
- 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");
11737
12351
  var import_node_url = require("url");
11738
12352
  var import_zod4 = require("zod");
11739
12353
  var SHA256_RE = /^[0-9a-f]{64}$/;
@@ -11742,7 +12356,7 @@ var VERSION_RE = /^\d+\.\d+\.\d+$/;
11742
12356
  var RECIPE_FILE = "pixelkiln.recipe.json";
11743
12357
  var RECIPE_DIR_TOKEN = "{{recipeDir}}";
11744
12358
  var PortablePathSchema = import_zod4.z.string().min(1).refine((value) => {
11745
- 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;
11746
12360
  const parts = value.split("/");
11747
12361
  return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
11748
12362
  }, "expected a portable relative path without '.', '..', or backslashes");
@@ -11918,10 +12532,10 @@ function message3(error) {
11918
12532
  return error instanceof Error ? error.message : String(error);
11919
12533
  }
11920
12534
  async function readRecipeFile(recipePath, bundled) {
11921
- const absolute = import_node_path15.default.resolve(recipePath);
12535
+ const absolute = import_node_path16.default.resolve(recipePath);
11922
12536
  let raw;
11923
12537
  try {
11924
- raw = JSON.parse(await (0, import_promises7.readFile)(absolute, "utf8"));
12538
+ raw = JSON.parse(await (0, import_promises8.readFile)(absolute, "utf8"));
11925
12539
  } catch (error) {
11926
12540
  throw new Error(`Could not read recipe ${absolute}: ${message3(error)}`, { cause: error });
11927
12541
  }
@@ -11932,13 +12546,13 @@ async function readRecipeFile(recipePath, bundled) {
11932
12546
  ` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
11933
12547
  );
11934
12548
  }
11935
- 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 };
11936
12550
  }
11937
12551
  async function walkRecipeFiles(root) {
11938
- if (!(0, import_node_fs11.existsSync)(root)) return [];
12552
+ if (!(0, import_node_fs12.existsSync)(root)) return [];
11939
12553
  const out = [];
11940
- for (const entry of await (0, import_promises7.readdir)(root, { withFileTypes: true })) {
11941
- 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);
11942
12556
  if (entry.isDirectory()) out.push(...await walkRecipeFiles(resolved));
11943
12557
  else if (entry.isFile() && entry.name === RECIPE_FILE) out.push(resolved);
11944
12558
  }
@@ -11949,7 +12563,7 @@ function bundledRecipeRoot() {
11949
12563
  }
11950
12564
  async function listBundledRecipes(root = bundledRecipeRoot()) {
11951
12565
  const recipes = await Promise.all(
11952
- (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))
11953
12567
  );
11954
12568
  return recipes.sort(
11955
12569
  (a, b) => a.recipe.id.localeCompare(b.recipe.id) || compareVersions(b.recipe.version, a.recipe.version)
@@ -11972,9 +12586,9 @@ function parseSelector(selector) {
11972
12586
  return { id: match[1], version: match[2] };
11973
12587
  }
11974
12588
  async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
11975
- const local = import_node_path15.default.resolve(target);
11976
- if ((0, import_node_fs11.existsSync)(local)) {
11977
- 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);
11978
12592
  return readRecipeFile(recipePath, false);
11979
12593
  }
11980
12594
  if (target.startsWith(".") || target.startsWith("/") || target.includes("\\")) {
@@ -11988,8 +12602,8 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
11988
12602
  return matches[0];
11989
12603
  }
11990
12604
  async function verifyFile(root, file) {
11991
- const absolute = import_node_path15.default.resolve(root, ...file.path.split("/"));
11992
- 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)) {
11993
12607
  return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
11994
12608
  }
11995
12609
  const actualSha256 = await sha256File(absolute);
@@ -12004,7 +12618,7 @@ async function verifyRecipe(target, options = {}) {
12004
12618
  const loaded = await resolveRecipe(target, options.bundledRoot);
12005
12619
  const actualIntegrity = recipeDigest(loaded.authored);
12006
12620
  const files = await Promise.all(loaded.recipe.files.map((file) => verifyFile(loaded.dir, file)));
12007
- 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;
12008
12622
  const models = modelRoot ? await Promise.all(loaded.recipe.models.map(async (model) => ({
12009
12623
  ...await verifyFile(modelRoot, model),
12010
12624
  source: model.source,
@@ -12038,7 +12652,7 @@ async function verifyRecipe(target, options = {}) {
12038
12652
  };
12039
12653
  }
12040
12654
  function renderStyle(style, destination, cwd) {
12041
- 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("/") || ".";
12042
12656
  const replace = (value) => {
12043
12657
  if (typeof value === "string") return value.replaceAll(RECIPE_DIR_TOKEN, portableDir);
12044
12658
  if (Array.isArray(value)) return value.map(replace);
@@ -12050,25 +12664,25 @@ function renderStyle(style, destination, cwd) {
12050
12664
  return replace(style);
12051
12665
  }
12052
12666
  async function installRecipe(target, options = {}) {
12053
- const cwd = import_node_path15.default.resolve(options.cwd ?? process.cwd());
12667
+ const cwd = import_node_path16.default.resolve(options.cwd ?? process.cwd());
12054
12668
  const loaded = await resolveRecipe(target, options.bundledRoot);
12055
12669
  const verification = await verifyRecipe(loaded.path);
12056
12670
  if (!verification.ok) {
12057
12671
  throw new Error(`Recipe ${loaded.recipe.id}@${loaded.recipe.version} failed integrity verification.`);
12058
12672
  }
12059
- 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);
12060
12674
  const sources = [
12061
12675
  { path: RECIPE_FILE, source: loaded.path },
12062
- ...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("/")) }))
12063
12677
  ];
12064
12678
  const files = await Promise.all(sources.map(async (file) => ({
12065
- path: import_node_path15.default.join(destination, ...file.path.split("/")),
12066
- 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)
12067
12681
  })));
12068
12682
  if (!options.force) {
12069
12683
  for (const file of files) {
12070
- if (!(0, import_node_fs11.existsSync)(file.path)) continue;
12071
- 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);
12072
12686
  if (!current.equals(file.data)) {
12073
12687
  throw new OverwriteRefusedError(
12074
12688
  `Recipe destination has local changes: ${file.path}. Choose another --out or pass --force to replace declared recipe files.`,
@@ -12092,9 +12706,9 @@ async function installRecipe(target, options = {}) {
12092
12706
  }
12093
12707
 
12094
12708
  // src/pipeline/audit.ts
12095
- var import_promises8 = require("fs/promises");
12096
- var import_node_fs12 = require("fs");
12097
- 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);
12098
12712
  function paletteDistance(asset, reference) {
12099
12713
  if (!asset.length || !reference.length) return 0;
12100
12714
  let total = 0;
@@ -12130,12 +12744,12 @@ async function auditStyle(loaded, specs, styleId, lock) {
12130
12744
  const unreadable = [];
12131
12745
  for (const spec of mine) {
12132
12746
  for (const output of resolveSpecOutputs(spec, lock, loaded.root)) {
12133
- if (!(0, import_node_fs12.existsSync)(output.absolutePath)) {
12747
+ if (!(0, import_node_fs13.existsSync)(output.absolutePath)) {
12134
12748
  missing.push(output.id);
12135
12749
  continue;
12136
12750
  }
12137
12751
  try {
12138
- const png = decodePng(await (0, import_promises8.readFile)(output.absolutePath));
12752
+ const png = decodePng(await (0, import_promises9.readFile)(output.absolutePath));
12139
12753
  const palette = extractPalette(png, 12);
12140
12754
  assets.push({
12141
12755
  assetId: spec.assetId,
@@ -12159,10 +12773,10 @@ async function auditStyle(loaded, specs, styleId, lock) {
12159
12773
  let referenceFromStyleImages = false;
12160
12774
  const refPalettes = [];
12161
12775
  for (const img of style.styleImages) {
12162
- const abs = import_node_path16.default.resolve(loaded.root, img.path);
12163
- 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;
12164
12778
  try {
12165
- refPalettes.push(extractPalette(decodePng(await (0, import_promises8.readFile)(abs)), 12));
12779
+ refPalettes.push(extractPalette(decodePng(await (0, import_promises9.readFile)(abs)), 12));
12166
12780
  } catch {
12167
12781
  }
12168
12782
  }
@@ -12238,20 +12852,20 @@ function hex(c) {
12238
12852
  }
12239
12853
 
12240
12854
  // src/pixellab-utils.ts
12241
- var import_node_fs13 = require("fs");
12242
- var import_promises9 = require("fs/promises");
12243
- 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);
12244
12858
  var UNZOOM_MIN_SIDE = 256;
12245
12859
  var UNZOOM_MAX_AREA = 2048 * 2048;
12246
12860
  function defaultUnzoomOut(input) {
12247
- const ext = import_node_path17.default.extname(input);
12861
+ const ext = import_node_path18.default.extname(input);
12248
12862
  return `${ext ? input.slice(0, -ext.length) : input}.unzoomed.png`;
12249
12863
  }
12250
12864
  async function unzoomFile(client2, input, opts = {}) {
12251
- const out = import_node_path17.default.resolve(opts.out ?? defaultUnzoomOut(input));
12252
- if (import_node_path17.default.resolve(input) === out) throw new Error("unzoom will not overwrite its own input; pass a different --out");
12253
- if ((0, import_node_fs13.existsSync)(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
12254
- 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);
12255
12869
  const metadata = imageMetadata(bytes);
12256
12870
  if (!metadata) throw new Error(`${input} is not a readable PNG or JPEG`);
12257
12871
  const { width, height, format } = metadata;
@@ -12264,22 +12878,22 @@ async function unzoomFile(client2, input, opts = {}) {
12264
12878
  throw new Error(`${input} is ${width}x${height}; PixelLab's unzoom takes at most 2048x2048 worth of pixels`);
12265
12879
  }
12266
12880
  const res = await client2.unzoom({ image: { base64: bytes.toString("base64"), format }, quantize: opts.quantize });
12267
- await (0, import_promises9.mkdir)(import_node_path17.default.dirname(out), { recursive: true });
12268
- 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);
12269
12883
  return { out, original: res.originalSize, unzoomed: res.unzoomedSize, zoomFactor: res.zoomFactor, usage: res.usage };
12270
12884
  }
12271
12885
  var FONT_WEIGHTS = ["Bold", "Regular"];
12272
12886
  var FONT_GLYPH_SIZES = [8, 16, 32, 64];
12273
- var FONT_COST_GENERATIONS = 25;
12887
+ var FONT_COST_GENERATIONS = 15;
12274
12888
  function fontOutputPaths(out) {
12275
- 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, ""));
12276
12890
  return { ttf: `${base}.ttf`, atlas: `${base}.png` };
12277
12891
  }
12278
12892
  async function generateFont(client2, opts) {
12279
12893
  const { ttf, atlas } = fontOutputPaths(opts.out);
12280
12894
  if (!opts.force) {
12281
12895
  for (const file of [ttf, atlas]) {
12282
- 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`);
12283
12897
  }
12284
12898
  }
12285
12899
  const sleep3 = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
@@ -12306,9 +12920,9 @@ async function generateFont(client2, opts) {
12306
12920
  client2.download(job.download_ttf_url),
12307
12921
  client2.download(job.download_atlas_url)
12308
12922
  ]);
12309
- await (0, import_promises9.mkdir)(import_node_path17.default.dirname(ttf), { recursive: true });
12310
- await (0, import_promises9.writeFile)(ttf, ttfBytes);
12311
- 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);
12312
12926
  return { jobId, ttf, atlas, glyphPx: job.glyph_px ?? null, usage: job.usage };
12313
12927
  }
12314
12928
  if (Date.now() - started > timeout) {
@@ -12319,17 +12933,72 @@ async function generateFont(client2, opts) {
12319
12933
  await sleep3(interval);
12320
12934
  }
12321
12935
  }
12936
+ var PIXELATE_COST_GENERATIONS = 6;
12937
+ var PIXELATE_MAX_SIDE = 2048;
12938
+ function defaultPixelateOut(input) {
12939
+ const ext = import_node_path18.default.extname(input);
12940
+ return `${ext ? input.slice(0, -ext.length) : input}.pixel.png`;
12941
+ }
12942
+ async function pixelateFile(client2, input, opts = {}) {
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);
12947
+ const metadata = imageMetadata(bytes);
12948
+ if (!metadata) throw new Error(`${input} is not a readable PNG or JPEG`);
12949
+ const { width, height, format } = metadata;
12950
+ const sleep3 = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
12951
+ const interval = opts.intervalMs ?? 5e3;
12952
+ const timeout = opts.timeoutMs ?? 10 * 6e4;
12953
+ const submitted = await client2.imageToPixelartProFlash({
12954
+ image: { base64: bytes.toString("base64"), format },
12955
+ description: opts.description,
12956
+ seed: opts.seed
12957
+ });
12958
+ const jobId = submitted.background_job_id;
12959
+ const started = Date.now();
12960
+ for (; ; ) {
12961
+ const job = await client2.getBackgroundJob(jobId);
12962
+ opts.onProgress?.(job.status);
12963
+ if (job.status === "failed") throw new Error(`PixelLab pixelate job ${jobId} failed upstream`);
12964
+ if (job.status === "completed") {
12965
+ const images = job.last_response?.images;
12966
+ const first = Array.isArray(images) ? images[0] : void 0;
12967
+ if (!first || typeof first.base64 !== "string" || !first.base64) {
12968
+ throw new Error(`PixelLab pixelate job ${jobId} completed without an image at last_response.images[0]`);
12969
+ }
12970
+ const png = Buffer.from(first.base64, "base64");
12971
+ const size = imageMetadata(png);
12972
+ await (0, import_promises10.mkdir)(import_node_path18.default.dirname(out), { recursive: true });
12973
+ await (0, import_promises10.writeFile)(out, png);
12974
+ return {
12975
+ jobId,
12976
+ out,
12977
+ original: { width, height },
12978
+ pixelated: { width: size?.width ?? Number(first.width), height: size?.height ?? Number(first.height) },
12979
+ imageId: submitted.image_id,
12980
+ usage: job.usage ?? null
12981
+ };
12982
+ }
12983
+ if (Date.now() - started > timeout) {
12984
+ throw new Error(
12985
+ `PixelLab pixelate job ${jobId} is still ${job.status} after ${Math.round(timeout / 1e3)} s; it keeps running upstream, but this command has stopped waiting`
12986
+ );
12987
+ }
12988
+ await sleep3(interval);
12989
+ }
12990
+ }
12322
12991
 
12323
12992
  // src/pipeline/quality-regression.ts
12324
- var import_node_path18 = __toESM(require("path"), 1);
12325
- var import_node_fs14 = require("fs");
12326
- 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");
12327
12996
  var import_zod5 = require("zod");
12328
12997
  var SHA256_RE2 = /^[0-9a-f]{64}$/;
12329
12998
  var HEX_RE = /^#[0-9a-f]{6}$/;
12330
12999
  var MAX_REDMEAN_DISTANCE = 765;
12331
13000
  var QualityPathSchema = import_zod5.z.string().min(1).refine(
12332
- (value) => !import_node_path18.default.posix.isAbsolute(value) && !value.includes("\\"),
13001
+ (value) => !import_node_path19.default.posix.isAbsolute(value) && !value.includes("\\"),
12333
13002
  "expected a portable relative path"
12334
13003
  );
12335
13004
  var DEFAULT_QUALITY_TOLERANCES = {
@@ -12476,10 +13145,10 @@ function measureDecodedImageQuality(png, digest2) {
12476
13145
  };
12477
13146
  }
12478
13147
  async function measureImageQuality(file) {
12479
- const absolute = import_node_path18.default.resolve(file);
13148
+ const absolute = import_node_path19.default.resolve(file);
12480
13149
  let bytes;
12481
13150
  try {
12482
- bytes = await (0, import_promises10.readFile)(absolute);
13151
+ bytes = await (0, import_promises11.readFile)(absolute);
12483
13152
  } catch (error) {
12484
13153
  throw new Error(`Cannot read quality image ${absolute}: ${message4(error)}`, { cause: error });
12485
13154
  }
@@ -12498,11 +13167,11 @@ function resolveQualityInputs(raw, inputsFilePath) {
12498
13167
  }
12499
13168
  const ids = parsed.data.map((entry) => entry.id);
12500
13169
  if (new Set(ids).size !== ids.length) throw new Error("--inputs contains a duplicate quality case id.");
12501
- 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));
12502
13171
  return parsed.data.map((entry) => ({
12503
13172
  id: entry.id,
12504
- file: import_node_path18.default.resolve(root, entry.path),
12505
- ...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) } : {},
12506
13175
  tolerances: QualityToleranceSchema.parse({
12507
13176
  ...DEFAULT_QUALITY_TOLERANCES,
12508
13177
  ...entry.tolerances
@@ -12510,7 +13179,7 @@ function resolveQualityInputs(raw, inputsFilePath) {
12510
13179
  }));
12511
13180
  }
12512
13181
  function portableRelative2(from, to) {
12513
- 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("/") || ".";
12514
13183
  }
12515
13184
  async function verifyRecordForImage(recordPath, imagePath) {
12516
13185
  const verification = await verifyArtifactBundle(recordPath);
@@ -12525,20 +13194,20 @@ async function verifyRecordForImage(recordPath, imagePath) {
12525
13194
  if (record.kind !== "refine" || options === null || typeof options !== "object" || options.schema !== "pixelkiln-quality") {
12526
13195
  throw new Error(`Quality record ${recordPath} is not a PixelKiln refinement record.`);
12527
13196
  }
12528
- const outputs = record.outputs.map((output) => import_node_path18.default.resolve(import_node_path18.default.dirname(recordPath), output.path));
12529
- 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))) {
12530
13199
  throw new Error(`Quality record ${recordPath} does not own ${imagePath}.`);
12531
13200
  }
12532
13201
  }
12533
13202
  async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
12534
13203
  if (!inputs.length) throw new Error("Cannot snapshot an empty quality baseline.");
12535
- const absolute = import_node_path18.default.resolve(baselinePath);
13204
+ const absolute = import_node_path19.default.resolve(baselinePath);
12536
13205
  for (const input of inputs) {
12537
- 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) {
12538
13207
  throw new Error(`Quality baseline output would overwrite an input: ${absolute}.`);
12539
13208
  }
12540
13209
  }
12541
- const root = import_node_path18.default.dirname(absolute);
13210
+ const root = import_node_path19.default.dirname(absolute);
12542
13211
  const cases = [];
12543
13212
  for (const input of [...inputs].sort((a, b) => a.id.localeCompare(b.id))) {
12544
13213
  const expected = await measureImageQuality(input.file);
@@ -12565,8 +13234,8 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
12565
13234
  cases
12566
13235
  });
12567
13236
  const data = Buffer.from(JSON.stringify(baseline, null, 2) + "\n");
12568
- if ((0, import_node_fs14.existsSync)(absolute) && !options.force) {
12569
- 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);
12570
13239
  if (!current.equals(data)) {
12571
13240
  throw new OverwriteRefusedError(
12572
13241
  `Quality baseline already exists with different content: ${absolute}. Pass --force to replace it.`,
@@ -12578,10 +13247,10 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
12578
13247
  return { baseline, path: absolute, changed: result.changed.length > 0 };
12579
13248
  }
12580
13249
  async function readQualityBaseline(baselinePath) {
12581
- const absolute = import_node_path18.default.resolve(baselinePath);
13250
+ const absolute = import_node_path19.default.resolve(baselinePath);
12582
13251
  let raw;
12583
13252
  try {
12584
- raw = JSON.parse(await (0, import_promises10.readFile)(absolute, "utf8"));
13253
+ raw = JSON.parse(await (0, import_promises11.readFile)(absolute, "utf8"));
12585
13254
  } catch (error) {
12586
13255
  throw new Error(`Could not read quality baseline ${absolute}: ${message4(error)}`, { cause: error });
12587
13256
  }
@@ -12651,12 +13320,12 @@ function regressionViolations(expected, actual, tolerances) {
12651
13320
  return { violations, warnings };
12652
13321
  }
12653
13322
  async function checkQualityBaseline(baselinePath) {
12654
- const absolute = import_node_path18.default.resolve(baselinePath);
13323
+ const absolute = import_node_path19.default.resolve(baselinePath);
12655
13324
  const baseline = await readQualityBaseline(absolute);
12656
- const root = import_node_path18.default.dirname(absolute);
13325
+ const root = import_node_path19.default.dirname(absolute);
12657
13326
  const cases = [];
12658
13327
  for (const entry of baseline.cases) {
12659
- const file = import_node_path18.default.resolve(root, entry.file);
13328
+ const file = import_node_path19.default.resolve(root, entry.file);
12660
13329
  let actual = null;
12661
13330
  const violations = [];
12662
13331
  const warnings = [];
@@ -12669,7 +13338,7 @@ async function checkQualityBaseline(baselinePath) {
12669
13338
  violations.push(message4(error));
12670
13339
  }
12671
13340
  if (entry.record) {
12672
- const recordPath = import_node_path18.default.resolve(root, entry.record.path);
13341
+ const recordPath = import_node_path19.default.resolve(root, entry.record.path);
12673
13342
  try {
12674
13343
  const recordHash = await sha256File(recordPath);
12675
13344
  if (recordHash !== entry.record.sha256) violations.push("quality record hash changed");
@@ -12705,14 +13374,14 @@ async function checkQualityBaseline(baselinePath) {
12705
13374
  }
12706
13375
 
12707
13376
  // src/pipeline/cache-health.ts
12708
- var import_node_fs18 = require("fs");
12709
- var import_promises13 = require("fs/promises");
12710
- 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);
12711
13380
 
12712
13381
  // src/cache.ts
12713
- var import_promises11 = require("fs/promises");
12714
- var import_node_fs15 = require("fs");
12715
- 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);
12716
13385
  var import_zod6 = require("zod");
12717
13386
  var HashCacheSchema = import_zod6.z.object({
12718
13387
  version: import_zod6.z.literal(1),
@@ -12726,10 +13395,10 @@ function isSha256Hash(value) {
12726
13395
  function parseCache(value) {
12727
13396
  return HashCacheSchema.parse(value);
12728
13397
  }
12729
- async function loadCache(path38) {
12730
- 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: {} };
12731
13400
  try {
12732
- 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")));
12733
13402
  if (!parsed.success) return { version: 1, hashes: {} };
12734
13403
  return {
12735
13404
  version: 1,
@@ -12741,20 +13410,20 @@ async function loadCache(path38) {
12741
13410
  return { version: 1, hashes: {} };
12742
13411
  }
12743
13412
  }
12744
- async function saveCache(path38, cache) {
13413
+ async function saveCache(path39, cache) {
12745
13414
  const sorted = {};
12746
13415
  for (const key of Object.keys(cache.hashes).sort()) {
12747
13416
  const hash = cache.hashes[key];
12748
13417
  if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
12749
13418
  sorted[key] = hash;
12750
13419
  }
12751
- await (0, import_promises11.mkdir)(import_node_path19.default.dirname(import_node_path19.default.resolve(path38)), { recursive: true });
12752
- 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`;
12753
13422
  try {
12754
- await (0, import_promises11.writeFile)(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
12755
- 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);
12756
13425
  } finally {
12757
- await (0, import_promises11.rm)(tmp, { force: true });
13426
+ await (0, import_promises12.rm)(tmp, { force: true });
12758
13427
  }
12759
13428
  }
12760
13429
  function pruneCache(cache, liveIds, namespace = "") {
@@ -12776,20 +13445,20 @@ function cachePathFor(lockPath) {
12776
13445
  }
12777
13446
 
12778
13447
  // src/pipeline/history.ts
12779
- var import_node_fs17 = require("fs");
12780
- var import_node_path21 = __toESM(require("path"), 1);
13448
+ var import_node_fs18 = require("fs");
13449
+ var import_node_path22 = __toESM(require("path"), 1);
12781
13450
 
12782
13451
  // src/pipeline/fetch.ts
12783
- var import_node_fs16 = require("fs");
12784
- var import_promises12 = require("fs/promises");
12785
- 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);
12786
13455
  async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12787
13456
  const log = opts.onProgress ?? (() => {
12788
13457
  });
12789
13458
  const result = { downloaded: 0, skipped: 0, failed: 0, ...opts.refresh ? { unchanged: 0 } : {} };
12790
13459
  const specByKey = new Map(specs.map((s) => [lockKey(s.styleId, s.assetId), s]));
12791
13460
  normalizeLockOutputPaths(lock, specs);
12792
- 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"));
12793
13462
  const pending = Object.entries(lock.entries).filter(([key, e]) => {
12794
13463
  if (e.provider !== provider.id || !specByKey.has(key)) return false;
12795
13464
  if (specByKey.get(key)?.mirror) return false;
@@ -12800,15 +13469,15 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12800
13469
  if (spec && e.specHash === spec.specHash && e.outputs.length && !postprocessCurrent(e, spec)) {
12801
13470
  return Boolean(opts.force) || e.outputs.every((output) => {
12802
13471
  const file = resolveOutputPath(output.path, spec.root);
12803
- 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;
12804
13473
  });
12805
13474
  }
12806
13475
  if (!opts.repair) return false;
12807
13476
  if (e.outputs.length === 0 || !spec) return true;
12808
13477
  return e.outputs.some((output) => {
12809
13478
  const file = resolveOutputPath(output.path, spec.root);
12810
- if (!(0, import_node_fs16.existsSync)(file)) return true;
12811
- 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));
12812
13481
  if (current === output.sha256) return false;
12813
13482
  return Boolean(opts.force) || (e.supersededOutputs ?? []).some((old) => old.sha256 === current);
12814
13483
  });
@@ -12891,21 +13560,21 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12891
13560
  if (!source.url) throw new Error(`no durable source URL to refresh ${source.role ?? "asset"} from`);
12892
13561
  fresh = await provider.download(source.url);
12893
13562
  if (recorded && sha256(fresh) === recordedRaw && !reapply) {
12894
- log(` current ${import_node_path20.default.relative(process.cwd(), target)}`);
13563
+ log(` current ${import_node_path21.default.relative(process.cwd(), target)}`);
12895
13564
  outputs.push({ ...recorded, path: portableOutputPath(target, spec.root) });
12896
13565
  continue;
12897
13566
  }
12898
13567
  }
12899
13568
  let raw = null;
12900
13569
  let final = null;
12901
- if ((0, import_node_fs16.existsSync)(target)) {
13570
+ if ((0, import_node_fs17.existsSync)(target)) {
12902
13571
  const currentHash = await sha256File(target);
12903
13572
  if (recorded && currentHash === recorded.sha256) {
12904
13573
  if (!fresh && !reapply) {
12905
13574
  if (cacheDir) {
12906
13575
  await cacheMedia(
12907
13576
  cacheDir,
12908
- await (0, import_promises12.readFile)(target),
13577
+ await (0, import_promises13.readFile)(target),
12909
13578
  recorded.mediaType ?? MediaType.PNG,
12910
13579
  recorded.sha256
12911
13580
  );
@@ -12914,10 +13583,10 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12914
13583
  continue;
12915
13584
  }
12916
13585
  if (reapply && !fresh) {
12917
- if (!recorded.raw) raw = await (0, import_promises12.readFile)(target);
12918
- 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)}`);
12919
13588
  } else {
12920
- log(` changed ${import_node_path20.default.relative(process.cwd(), target)} (upstream)`);
13589
+ log(` changed ${import_node_path21.default.relative(process.cwd(), target)} (upstream)`);
12921
13590
  }
12922
13591
  } else {
12923
13592
  const superseded = (entry.supersededOutputs ?? []).find(
@@ -12938,14 +13607,14 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12938
13607
  }
12939
13608
  if (outgoing && cacheDir) {
12940
13609
  try {
12941
- const oldBytes = await (0, import_promises12.readFile)(target);
13610
+ const oldBytes = await (0, import_promises13.readFile)(target);
12942
13611
  const oldMediaType = detectMediaType(oldBytes);
12943
13612
  if (oldMediaType) await cacheMedia(cacheDir, oldBytes, oldMediaType, currentHash);
12944
13613
  } catch {
12945
13614
  }
12946
13615
  }
12947
13616
  log(
12948
- ` 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)")
12949
13618
  );
12950
13619
  }
12951
13620
  }
@@ -12954,7 +13623,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12954
13623
  } else if (!raw && recorded && cacheDir) {
12955
13624
  if (!reapply) final = await readCachedMedia(cacheDir, recorded.sha256, expectedMediaType);
12956
13625
  if (!final && recordedRaw) raw = await readCachedMedia(cacheDir, recordedRaw, expectedMediaType);
12957
- 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)}`);
12958
13627
  }
12959
13628
  if (!final && !raw) {
12960
13629
  if (!source.url) {
@@ -12981,13 +13650,13 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12981
13650
  buf = processed.bytes;
12982
13651
  if (processed.changed) {
12983
13652
  rawHash = sha256(raw);
12984
- 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`);
12985
13654
  }
12986
13655
  }
12987
13656
  if (cacheDir) await cacheMedia(cacheDir, buf, mediaType);
12988
- 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 });
12989
13658
  const tmp = `${target}.pixelkiln.tmp`;
12990
- await (0, import_promises12.writeFile)(tmp, buf);
13659
+ await (0, import_promises13.writeFile)(tmp, buf);
12991
13660
  outputs.push({
12992
13661
  path: portableOutputPath(target, spec.root),
12993
13662
  sha256: sha256(buf),
@@ -12997,9 +13666,9 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12997
13666
  });
12998
13667
  upsert(lock, key, { outputs: mergeOutputs(entry.outputs, outputs) });
12999
13668
  await saveLock(lockPath, lock);
13000
- await (0, import_promises12.rename)(tmp, target);
13669
+ await (0, import_promises13.rename)(tmp, target);
13001
13670
  wrote = true;
13002
- log(` wrote ${import_node_path20.default.relative(process.cwd(), target)}`);
13671
+ log(` wrote ${import_node_path21.default.relative(process.cwd(), target)}`);
13003
13672
  }
13004
13673
  if (opts.refresh && !wrote) {
13005
13674
  if (rotated) {
@@ -13043,10 +13712,10 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
13043
13712
  return result;
13044
13713
  }
13045
13714
  async function readCachedMedia(cacheDir, hash, mediaType) {
13046
- const file = import_node_path20.default.join(cacheDir, cacheFileName(hash, mediaType));
13047
- 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;
13048
13717
  try {
13049
- const buf = await (0, import_promises12.readFile)(file);
13718
+ const buf = await (0, import_promises13.readFile)(file);
13050
13719
  if (sha256(buf) !== hash) return null;
13051
13720
  validateMedia(buf, mediaType);
13052
13721
  return buf;
@@ -13057,17 +13726,17 @@ async function readCachedMedia(cacheDir, hash, mediaType) {
13057
13726
  async function cacheMedia(cacheDir, buf, mediaType, knownHash) {
13058
13727
  const hash = knownHash ?? sha256(buf);
13059
13728
  validateMedia(buf, mediaType);
13060
- const file = import_node_path20.default.join(cacheDir, cacheFileName(hash, mediaType));
13729
+ const file = import_node_path21.default.join(cacheDir, cacheFileName(hash, mediaType));
13061
13730
  if (await readCachedMedia(cacheDir, hash, mediaType)) return;
13062
- await (0, import_promises12.mkdir)(cacheDir, { recursive: true });
13731
+ await (0, import_promises13.mkdir)(cacheDir, { recursive: true });
13063
13732
  const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
13064
- await (0, import_promises12.writeFile)(tmp, buf);
13733
+ await (0, import_promises13.writeFile)(tmp, buf);
13065
13734
  try {
13066
- await (0, import_promises12.rename)(tmp, file);
13735
+ await (0, import_promises13.rename)(tmp, file);
13067
13736
  } catch (err) {
13068
- if (!(0, import_node_fs16.existsSync)(file)) throw err;
13737
+ if (!(0, import_node_fs17.existsSync)(file)) throw err;
13069
13738
  } finally {
13070
- await (0, import_promises12.rm)(tmp, { force: true });
13739
+ await (0, import_promises13.rm)(tmp, { force: true });
13071
13740
  }
13072
13741
  }
13073
13742
  function mergeOutputs(previous, current) {
@@ -13085,7 +13754,7 @@ async function pushTags(provider, specs, lock, opts = {}) {
13085
13754
  for (const [key, entry] of Object.entries(lock.entries)) {
13086
13755
  const spec = specByKey.get(key);
13087
13756
  if (!spec || entry.provider !== provider.id || !entry.objectId) continue;
13088
- if (entry.generator === "pixflux") continue;
13757
+ if (entry.generator === "pixflux" || entry.generator === "vfx") continue;
13089
13758
  if (!provider.setTags) return tagged;
13090
13759
  try {
13091
13760
  await provider.setTags(entry.objectId, spec.tags, entry.generator);
@@ -13184,10 +13853,10 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
13184
13853
  }
13185
13854
  for (let i = 0; i < entry.outputs.length; i++) {
13186
13855
  const file = currentEntryOutputPath(entry, spec, i);
13187
- if (!(0, import_node_fs17.existsSync)(file)) continue;
13856
+ if (!(0, import_node_fs18.existsSync)(file)) continue;
13188
13857
  if (await sha256File(file) !== entry.outputs[i].sha256 && !opts.force) {
13189
13858
  throw new OverwriteRefusedError(
13190
- `${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`,
13191
13860
  [file]
13192
13861
  );
13193
13862
  }
@@ -13239,11 +13908,11 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
13239
13908
 
13240
13909
  // src/pipeline/cache-health.ts
13241
13910
  async function inspectCaches(lock, lockPath, options = {}) {
13242
- if (options.prune && !(0, import_node_fs18.existsSync)(lockPath)) {
13243
- 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)}`);
13244
13913
  }
13245
- const contentDir = import_node_path22.default.resolve(import_node_path22.default.dirname(lockPath), ".pixelkiln", "cache");
13246
- 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));
13247
13916
  const referenced = referencedHashes(lock);
13248
13917
  let content = await inspectContentCache(contentDir, referenced);
13249
13918
  let remoteHashes = await inspectRemoteHashCache(remotePath);
@@ -13255,7 +13924,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
13255
13924
  ]);
13256
13925
  for (const name of names) {
13257
13926
  try {
13258
- 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 });
13259
13928
  removed.contentFiles++;
13260
13929
  } catch {
13261
13930
  }
@@ -13264,7 +13933,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
13264
13933
  await saveCache(remotePath, { version: 1, hashes: {} });
13265
13934
  removed.resetRemoteHashCache = true;
13266
13935
  } else if (remoteHashes.invalidIds.length) {
13267
- 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")));
13268
13937
  for (const id of remoteHashes.invalidIds) delete cache.hashes[id];
13269
13938
  removed.remoteHashEntries = remoteHashes.invalidIds.length;
13270
13939
  await saveCache(remotePath, cache);
@@ -13282,7 +13951,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
13282
13951
  async function inspectContentCache(contentDir, referenced) {
13283
13952
  const report = {
13284
13953
  path: contentDir,
13285
- exists: (0, import_node_fs18.existsSync)(contentDir),
13954
+ exists: (0, import_node_fs19.existsSync)(contentDir),
13286
13955
  files: 0,
13287
13956
  bytes: 0,
13288
13957
  valid: 0,
@@ -13296,19 +13965,19 @@ async function inspectContentCache(contentDir, referenced) {
13296
13965
  return report;
13297
13966
  }
13298
13967
  const present = /* @__PURE__ */ new Set();
13299
- const entries = await (0, import_promises13.readdir)(contentDir, { withFileTypes: true });
13968
+ const entries = await (0, import_promises14.readdir)(contentDir, { withFileTypes: true });
13300
13969
  for (const entry of entries) {
13301
13970
  if (!entry.isFile()) {
13302
13971
  report.invalid.push({ name: entry.name, reason: "not a regular file" });
13303
13972
  continue;
13304
13973
  }
13305
13974
  report.files++;
13306
- const file = import_node_path22.default.join(contentDir, entry.name);
13975
+ const file = import_node_path23.default.join(contentDir, entry.name);
13307
13976
  const mediaType = mediaTypeFromExtension(entry.name);
13308
13977
  const expected = mediaType ? entry.name.slice(0, -4) : "";
13309
13978
  let bytes;
13310
13979
  try {
13311
- bytes = await (0, import_promises13.readFile)(file);
13980
+ bytes = await (0, import_promises14.readFile)(file);
13312
13981
  report.bytes += bytes.length;
13313
13982
  } catch (err) {
13314
13983
  report.invalid.push({
@@ -13346,7 +14015,7 @@ async function inspectContentCache(contentDir, referenced) {
13346
14015
  async function inspectRemoteHashCache(remotePath) {
13347
14016
  const report = {
13348
14017
  path: remotePath,
13349
- exists: (0, import_node_fs18.existsSync)(remotePath),
14018
+ exists: (0, import_node_fs19.existsSync)(remotePath),
13350
14019
  entries: 0,
13351
14020
  valid: 0,
13352
14021
  invalidIds: [],
@@ -13355,7 +14024,7 @@ async function inspectRemoteHashCache(remotePath) {
13355
14024
  if (!report.exists) return report;
13356
14025
  let cache;
13357
14026
  try {
13358
- cache = parseCache(JSON.parse(await (0, import_promises13.readFile)(remotePath, "utf8")));
14027
+ cache = parseCache(JSON.parse(await (0, import_promises14.readFile)(remotePath, "utf8")));
13359
14028
  } catch (err) {
13360
14029
  report.error = err instanceof Error ? err.message : String(err);
13361
14030
  return report;
@@ -13370,20 +14039,20 @@ async function inspectRemoteHashCache(remotePath) {
13370
14039
  }
13371
14040
 
13372
14041
  // src/pipeline/pack.ts
13373
- var import_node_fs19 = require("fs");
13374
- var import_node_path23 = __toESM(require("path"), 1);
14042
+ var import_node_fs20 = require("fs");
14043
+ var import_node_path24 = __toESM(require("path"), 1);
13375
14044
  var DEFAULT_FRAME_SET_FPS = 12;
13376
14045
  function resolvePackInputs(raw, inputsFilePath) {
13377
14046
  if (!Array.isArray(raw) || !raw.length) {
13378
14047
  throw new Error("--inputs must be a non-empty JSON array of { id, path }");
13379
14048
  }
13380
- 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));
13381
14050
  return raw.map((entry, i) => {
13382
14051
  const e = entry;
13383
14052
  if (typeof e.id !== "string" || typeof e.path !== "string") {
13384
14053
  throw new Error(`--inputs[${i}] needs string "id" and "path"`);
13385
14054
  }
13386
- 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) };
13387
14056
  });
13388
14057
  }
13389
14058
  function packSprites(inputs, options = {}) {
@@ -13404,7 +14073,7 @@ function packSprites(inputs, options = {}) {
13404
14073
  for (const input of ordered) {
13405
14074
  let sourceHash = null;
13406
14075
  try {
13407
- const bytes = (0, import_node_fs19.readFileSync)(input.path);
14076
+ const bytes = (0, import_node_fs20.readFileSync)(input.path);
13408
14077
  sourceHash = sha256(bytes);
13409
14078
  const png = decodePng(bytes);
13410
14079
  sprites.push({ id: input.id, width: png.width, height: png.height, pixels: png.pixels });
@@ -13467,7 +14136,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
13467
14136
  }
13468
14137
  const inputs2 = Object.entries(options.sourceOverrides).map(([id, source]) => ({
13469
14138
  id,
13470
- path: import_node_path23.default.resolve(source)
14139
+ path: import_node_path24.default.resolve(source)
13471
14140
  }));
13472
14141
  if (!inputs2.length) throw new Error(`No approved quality outputs for style "${styleId}".`);
13473
14142
  const packed2 = packSprites(inputs2, options);
@@ -13491,7 +14160,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
13491
14160
  let outputs = resolveEntryOutputs(entry, id, manifestDir);
13492
14161
  const source = sources[id];
13493
14162
  if (source && outputs.length <= 1) {
13494
- inputs.push({ id, path: import_node_path23.default.resolve(manifestDir, source) });
14163
+ inputs.push({ id, path: import_node_path24.default.resolve(manifestDir, source) });
13495
14164
  continue;
13496
14165
  }
13497
14166
  if (source && sourceIsStem(source, entry, manifestDir)) {
@@ -13520,12 +14189,13 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
13520
14189
  id,
13521
14190
  kind: isFrames ? "frames" : "members",
13522
14191
  ...fps ? { fps } : {},
14192
+ ...isFrames && frameSetLoop(entry) === false ? { loop: false } : {},
13523
14193
  frames: selected.map((output) => output.id)
13524
14194
  });
13525
14195
  }
13526
14196
  }
13527
14197
  for (const [id, source] of Object.entries(sources)) {
13528
- 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) });
13529
14199
  }
13530
14200
  if (!inputs.length) {
13531
14201
  throw new Error(`No readable sprites for style "${styleId}"; ${noOutput.length} skipped.`);
@@ -13562,7 +14232,7 @@ function mountSprites(placements, options) {
13562
14232
  for (const p of placements) {
13563
14233
  let sourceHash = null;
13564
14234
  try {
13565
- const bytes = (0, import_node_fs19.readFileSync)(p.path);
14235
+ const bytes = (0, import_node_fs20.readFileSync)(p.path);
13566
14236
  sourceHash = sha256(bytes);
13567
14237
  const png = decodePng(bytes);
13568
14238
  if (png.width > cellWidth || png.height > cellHeight) {
@@ -13633,7 +14303,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
13633
14303
  const source = sources[id];
13634
14304
  const entry = lock.entries[`${styleId}/${id}`];
13635
14305
  if (source && !(entry && sourceIsStem(source, entry, manifestDir))) {
13636
- 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 });
13637
14307
  continue;
13638
14308
  }
13639
14309
  if (source && entry) {
@@ -13666,7 +14336,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
13666
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}`
13667
14337
  );
13668
14338
  }
13669
- 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;
13670
14340
  const mounted = mountSprites(placements, {
13671
14341
  cellWidth: mount.cellWidth,
13672
14342
  cellHeight: mount.cellHeight,
@@ -13676,7 +14346,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
13676
14346
  ...mounted,
13677
14347
  sources: mount.base ? [{
13678
14348
  id: "$base",
13679
- path: import_node_path23.default.resolve(manifestDir, mount.base),
14349
+ path: import_node_path24.default.resolve(manifestDir, mount.base),
13680
14350
  sha256: sha256(basePng),
13681
14351
  included: true
13682
14352
  }, ...mounted.sources] : mounted.sources,
@@ -14058,7 +14728,7 @@ function renderGodotSpriteFrames(atlas, opts) {
14058
14728
  );
14059
14729
  };
14060
14730
  for (const set of atlas.sets ?? []) {
14061
- 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);
14062
14732
  else for (const id of set.frames) animation(id, [id], GODOT_DEFAULT_SPEED, false);
14063
14733
  }
14064
14734
  for (const frame of atlas.frames) {
@@ -14313,6 +14983,8 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
14313
14983
  const anchor = spec.character?.styleAnchor;
14314
14984
  const anchorEntry = anchor ? lock.entries[lockKey(anchor.spec.styleId, anchor.spec.assetId)] : void 0;
14315
14985
  const replacedMetadata = previousEntry?.providerMetadata?.[provider.id];
14986
+ const lastGeneration = previousEntry?.status === "downloaded" ? previousEntry : previousEntry?.history?.[0];
14987
+ const lastGenerationMetadata = lastGeneration?.providerMetadata?.[provider.id];
14316
14988
  const parentOutputs = spec.character?.kind === "outfit" && parentEntry ? parentEntry.outputs.map((output, index) => ({
14317
14989
  path: currentEntryOutputPath(parentEntry, spec.character.parentSpec, index),
14318
14990
  role: output.role
@@ -14325,6 +14997,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
14325
14997
  ...anchorEntry?.objectId ? { styleObjectId: anchorEntry.objectId } : {},
14326
14998
  ...parentOutputs ? { parentOutputs } : {},
14327
14999
  ...replacedMetadata ? { replacedMetadata } : {},
15000
+ ...lastGenerationMetadata ? { lastGenerationMetadata } : {},
14328
15001
  ...previousMetadata ? { previousMetadata } : {},
14329
15002
  checkpoint: async (checkpoint) => {
14330
15003
  if (!checkpoint.jobId) throw new Error("Provider checkpoint returned an empty job id");
@@ -14487,9 +15160,9 @@ async function poll(provider, lock, lockPath, opts = {}) {
14487
15160
  }
14488
15161
 
14489
15162
  // src/pipeline/doctor.ts
14490
- var import_node_fs20 = require("fs");
14491
- var import_promises14 = require("fs/promises");
14492
- 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);
14493
15166
  async function doctor(loaded, specs, lock, lockPath, opts = {}) {
14494
15167
  const checks = [];
14495
15168
  const add = (id, level, message6) => checks.push({ id, level, message: message6 });
@@ -14500,14 +15173,14 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
14500
15173
  );
14501
15174
  add("lock", "ok", `${Object.keys(lock.entries).length} valid v2 lock entr(ies)`);
14502
15175
  const dirs = [...new Set(specs.flatMap((spec) => [
14503
- import_node_path24.default.dirname(spec.outFile),
14504
- ...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)] : []
14505
15178
  ]))];
14506
15179
  const unwritable = [];
14507
15180
  for (const dir of dirs) {
14508
15181
  const ancestor = await nearestExistingDirectory(dir);
14509
15182
  try {
14510
- 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);
14511
15184
  } catch {
14512
15185
  unwritable.push(dir);
14513
15186
  }
@@ -14524,7 +15197,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
14524
15197
  for (let index = 0; index < entry.outputs.length; index++) {
14525
15198
  const output = entry.outputs[index];
14526
15199
  const spec = specByKey.get(key);
14527
- 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));
14528
15201
  const owner = outputOwners.get(absolute);
14529
15202
  if (owner && owner !== key) duplicateOutputs.push(`${output.path} (${owner}, ${key})`);
14530
15203
  outputOwners.set(absolute, key);
@@ -14539,7 +15212,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
14539
15212
  } else {
14540
15213
  add("lock-outputs", "ok", `${outputOwners.size} recorded output path(s) are uniquely owned`);
14541
15214
  }
14542
- 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");
14543
15216
  const stranded = [];
14544
15217
  const invalidState = [];
14545
15218
  const staleMapUrls = [];
@@ -14555,11 +15228,11 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
14555
15228
  const hasSource = Boolean(entry.sourceUrl || entry.sourceUrls?.length);
14556
15229
  let hasCache = false;
14557
15230
  for (const output of entry.outputs) {
14558
- const cached = import_node_path24.default.join(
15231
+ const cached = import_node_path25.default.join(
14559
15232
  cacheDir,
14560
15233
  cacheFileName(output.sha256, output.mediaType ?? MediaType.PNG)
14561
15234
  );
14562
- 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) {
14563
15236
  hasCache = true;
14564
15237
  break;
14565
15238
  }
@@ -14652,19 +15325,19 @@ async function nearestExistingDirectory(start) {
14652
15325
  let current = start;
14653
15326
  for (; ; ) {
14654
15327
  try {
14655
- if ((await (0, import_promises14.stat)(current)).isDirectory()) return current;
15328
+ if ((await (0, import_promises15.stat)(current)).isDirectory()) return current;
14656
15329
  } catch {
14657
15330
  }
14658
- const parent = import_node_path24.default.dirname(current);
15331
+ const parent = import_node_path25.default.dirname(current);
14659
15332
  if (parent === current) return current;
14660
15333
  current = parent;
14661
15334
  }
14662
15335
  }
14663
15336
 
14664
15337
  // src/pipeline/adopt.ts
14665
- var import_node_fs21 = require("fs");
14666
- var import_promises15 = require("fs/promises");
14667
- 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);
14668
15341
  async function adopt(provider, specs, lock, lockPath, opts = {}) {
14669
15342
  const log = opts.onProgress ?? (() => {
14670
15343
  });
@@ -14675,11 +15348,11 @@ async function adopt(provider, specs, lock, lockPath, opts = {}) {
14675
15348
  const localByHash = /* @__PURE__ */ new Map();
14676
15349
  const unmatchedLocal = [];
14677
15350
  for (const spec of specs) {
14678
- if (!(0, import_node_fs21.existsSync)(spec.outFile)) {
15351
+ if (!(0, import_node_fs22.existsSync)(spec.outFile)) {
14679
15352
  unmatchedLocal.push(`${spec.styleId}/${spec.assetId} (no file at ${spec.outFile})`);
14680
15353
  continue;
14681
15354
  }
14682
- const bytes = await (0, import_promises15.readFile)(spec.outFile);
15355
+ const bytes = await (0, import_promises16.readFile)(spec.outFile);
14683
15356
  try {
14684
15357
  decodePng(bytes);
14685
15358
  } catch (err) {
@@ -14797,13 +15470,13 @@ async function tagAdopted(provider, specs, lock, opts = {}) {
14797
15470
  }
14798
15471
 
14799
15472
  // src/pick/server.ts
14800
- var import_node_fs23 = require("fs");
14801
- var import_promises18 = require("fs/promises");
15473
+ var import_node_fs24 = require("fs");
15474
+ var import_promises19 = require("fs/promises");
14802
15475
 
14803
15476
  // src/pipeline/init.ts
14804
- var import_promises16 = require("fs/promises");
14805
- var import_node_fs22 = require("fs");
14806
- 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);
14807
15480
  function pngSize(buf) {
14808
15481
  if (buf.length < 24) return null;
14809
15482
  const isPng = buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834;
@@ -14816,8 +15489,8 @@ function slugify(value) {
14816
15489
  }
14817
15490
  async function walk(dir, exclude) {
14818
15491
  const out = [];
14819
- for (const entry of await (0, import_promises16.readdir)(dir, { withFileTypes: true })) {
14820
- 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);
14821
15494
  if (exclude.some((x) => full.includes(x))) continue;
14822
15495
  if (entry.isDirectory()) out.push(...await walk(full, exclude));
14823
15496
  else if (entry.name.toLowerCase().endsWith(".png")) out.push(full);
@@ -14831,14 +15504,14 @@ async function scanAssets(root, opts = {}) {
14831
15504
  const skipped = [];
14832
15505
  const seen = /* @__PURE__ */ new Map();
14833
15506
  for (const file of files.sort()) {
14834
- const size = pngSize(await (0, import_promises16.readFile)(file));
15507
+ const size = pngSize(await (0, import_promises17.readFile)(file));
14835
15508
  if (!size) {
14836
15509
  skipped.push(`${file} (not a readable PNG)`);
14837
15510
  continue;
14838
15511
  }
14839
- const rel = import_node_path26.default.relative(root, file);
14840
- const category = import_node_path26.default.dirname(rel) === "." ? "" : import_node_path26.default.dirname(rel);
14841
- 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));
14842
15515
  if (seen.has(id)) {
14843
15516
  const n = seen.get(id) + 1;
14844
15517
  seen.set(id, n);
@@ -15302,7 +15975,7 @@ document.getElementById('submit').onclick = async () => {
15302
15975
 
15303
15976
  // src/pick/review-server.ts
15304
15977
  var import_node_http = require("http");
15305
- var import_promises17 = require("fs/promises");
15978
+ var import_promises18 = require("fs/promises");
15306
15979
  function serveReviewPage(opts) {
15307
15980
  const log = opts.onProgress ?? (() => {
15308
15981
  });
@@ -15311,7 +15984,7 @@ function serveReviewPage(opts) {
15311
15984
  if (req.method === "GET" && req.url && opts.assets?.has(req.url)) {
15312
15985
  try {
15313
15986
  const asset = opts.assets.get(req.url);
15314
- const bytes = await (0, import_promises17.readFile)(asset.path);
15987
+ const bytes = await (0, import_promises18.readFile)(asset.path);
15315
15988
  res.writeHead(200, {
15316
15989
  "Content-Type": asset.contentType,
15317
15990
  "Cache-Control": "no-store",
@@ -15387,13 +16060,13 @@ async function currentArt(entry, spec) {
15387
16060
  const previous = primaryOutput({ ...entry, outputs: entry.supersededOutputs });
15388
16061
  if (!previous) return null;
15389
16062
  const file = resolveOutputPath(previous.path, spec.root);
15390
- if (!(0, import_node_fs23.existsSync)(file)) return null;
16063
+ if (!(0, import_node_fs24.existsSync)(file)) return null;
15391
16064
  const contentType = previous.mediaType ?? mediaTypeFromExtension(file);
15392
16065
  if (!contentType) return null;
15393
16066
  let width = entry.width;
15394
16067
  let height = entry.height;
15395
16068
  if (contentType === "image/png") {
15396
- const size = pngSize(await (0, import_promises18.readFile)(file));
16069
+ const size = pngSize(await (0, import_promises19.readFile)(file));
15397
16070
  if (size) ({ width, height } = size);
15398
16071
  }
15399
16072
  return { path: file, contentType, width, height };
@@ -15401,7 +16074,7 @@ async function currentArt(entry, spec) {
15401
16074
  async function cachedFrame(url) {
15402
16075
  if (!url.startsWith("file://")) return null;
15403
16076
  const file = url.slice("file://".length);
15404
- 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";
15405
16078
  return { path: file, contentType };
15406
16079
  }
15407
16080
  async function prepareReview(provider, lock, opts = {}) {
@@ -15583,14 +16256,14 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
15583
16256
 
15584
16257
  // src/gallery/snapshot.ts
15585
16258
  var import_node_crypto7 = require("crypto");
15586
- var import_node_fs29 = require("fs");
15587
- var import_promises26 = require("fs/promises");
15588
- 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);
15589
16262
 
15590
16263
  // src/workspace.ts
15591
- var import_promises19 = require("fs/promises");
15592
- var import_node_fs24 = require("fs");
15593
- 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);
15594
16267
  var import_zod7 = require("zod");
15595
16268
  var WorkspaceProjectSchema = import_zod7.z.object({
15596
16269
  id: import_zod7.z.string().min(1),
@@ -15615,10 +16288,10 @@ ${parsed.error.issues.slice(0, 5).map((i) => ` ${i.path.join(".")}: ${i.message
15615
16288
  );
15616
16289
  }
15617
16290
  async function loadWorkspace(workspacePath) {
15618
- if (!(0, import_node_fs24.existsSync)(workspacePath)) return { version: 1, projects: [] };
16291
+ if (!(0, import_node_fs25.existsSync)(workspacePath)) return { version: 1, projects: [] };
15619
16292
  let raw;
15620
16293
  try {
15621
- raw = JSON.parse(await (0, import_promises19.readFile)(workspacePath, "utf8"));
16294
+ raw = JSON.parse(await (0, import_promises20.readFile)(workspacePath, "utf8"));
15622
16295
  } catch (err) {
15623
16296
  throw new ProjectError(
15624
16297
  `Workspace catalog at ${workspacePath} is malformed:
@@ -15632,22 +16305,22 @@ async function saveWorkspace(workspacePath, ws) {
15632
16305
  version: 1,
15633
16306
  projects: [...ws.projects].sort((a, b) => a.id.localeCompare(b.id))
15634
16307
  };
15635
- 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 });
15636
16309
  const tmp = `${workspacePath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
15637
16310
  try {
15638
- await (0, import_promises19.writeFile)(tmp, JSON.stringify(sorted, null, 2) + "\n");
15639
- 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);
15640
16313
  } finally {
15641
- await (0, import_promises19.rm)(tmp, { force: true });
16314
+ await (0, import_promises20.rm)(tmp, { force: true });
15642
16315
  }
15643
16316
  }
15644
16317
  function toPortablePath(dir, absolute) {
15645
- 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("/");
15646
16319
  }
15647
16320
  function resolveProject(dir, project) {
15648
16321
  return {
15649
- manifestPath: import_node_path27.default.resolve(dir, project.manifest.split("/").join(import_node_path27.default.sep)),
15650
- 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))
15651
16324
  };
15652
16325
  }
15653
16326
  function validateWorkspace(ws, dir) {
@@ -15660,21 +16333,21 @@ function validateWorkspace(ws, dir) {
15660
16333
  const { manifestPath, lockPath } = resolveProject(dir, project);
15661
16334
  lockOwners.set(lockPath, [...lockOwners.get(lockPath) ?? [], project.id]);
15662
16335
  manifestOwners.set(manifestPath, [...manifestOwners.get(manifestPath) ?? [], project.id]);
15663
- 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)) {
15664
16337
  diagnostics.push({
15665
16338
  id: "absolute-path",
15666
16339
  level: "warning",
15667
16340
  message: `project "${project.id}" stores an absolute path; the catalog will not resolve correctly if this tree is cloned or moved elsewhere`
15668
16341
  });
15669
16342
  }
15670
- if (!(0, import_node_fs24.existsSync)(manifestPath)) {
16343
+ if (!(0, import_node_fs25.existsSync)(manifestPath)) {
15671
16344
  diagnostics.push({
15672
16345
  id: "missing-manifest",
15673
16346
  level: "error",
15674
16347
  message: `project "${project.id}" manifest not found: ${manifestPath}`
15675
16348
  });
15676
16349
  }
15677
- if (!(0, import_node_fs24.existsSync)(lockPath)) {
16350
+ if (!(0, import_node_fs25.existsSync)(lockPath)) {
15678
16351
  diagnostics.push({
15679
16352
  id: "missing-lock",
15680
16353
  level: "error",
@@ -15721,16 +16394,16 @@ function validateWorkspace(ws, dir) {
15721
16394
  }
15722
16395
 
15723
16396
  // src/gallery/edit.ts
15724
- var import_promises25 = require("fs/promises");
15725
- var import_node_path33 = __toESM(require("path"), 1);
16397
+ var import_promises26 = require("fs/promises");
16398
+ var import_node_path34 = __toESM(require("path"), 1);
15726
16399
  var import_zod13 = require("zod");
15727
16400
 
15728
16401
  // src/manifest-edit.ts
15729
- var import_promises20 = require("fs/promises");
15730
- var import_node_path28 = __toESM(require("path"), 1);
16402
+ var import_promises21 = require("fs/promises");
16403
+ var import_node_path29 = __toESM(require("path"), 1);
15731
16404
  var import_zod8 = require("zod");
15732
16405
  var HexSha = import_zod8.z.string().regex(/^[0-9a-f]{64}$/);
15733
- 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");
15734
16407
  var AssetPatchSchema = import_zod8.z.object({
15735
16408
  prompt: import_zod8.z.string().optional(),
15736
16409
  /** Per-style override for the style being viewed; null removes it. */
@@ -16149,8 +16822,8 @@ function applyEdit(raw, edit) {
16149
16822
  }
16150
16823
  }
16151
16824
  async function withCandidate(manifestPath, edit, use, unchanged) {
16152
- const absolute = import_node_path28.default.resolve(manifestPath);
16153
- 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");
16154
16827
  if (sha256(original) !== edit.expectedSha256) throw new ManifestDriftError(absolute);
16155
16828
  let raw;
16156
16829
  try {
@@ -16163,12 +16836,12 @@ async function withCandidate(manifestPath, edit, use, unchanged) {
16163
16836
  applyEdit(raw, edit);
16164
16837
  const next = serializeLike(original, raw);
16165
16838
  if (next === original) return unchanged(absolute, original);
16166
- const tmp = import_node_path28.default.join(
16167
- import_node_path28.default.dirname(absolute),
16168
- `.${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`
16169
16842
  );
16170
16843
  try {
16171
- await (0, import_promises20.writeFile)(tmp, next);
16844
+ await (0, import_promises21.writeFile)(tmp, next);
16172
16845
  let specs;
16173
16846
  try {
16174
16847
  specs = await resolveSpecs(await loadManifest(tmp));
@@ -16178,7 +16851,7 @@ async function withCandidate(manifestPath, edit, use, unchanged) {
16178
16851
  }
16179
16852
  return await use({ absolute, original, next, tmp, specs });
16180
16853
  } finally {
16181
- await (0, import_promises20.rm)(tmp, { force: true });
16854
+ await (0, import_promises21.rm)(tmp, { force: true });
16182
16855
  }
16183
16856
  }
16184
16857
  async function applyManifestEdit(manifestPath, edit) {
@@ -16186,7 +16859,7 @@ async function applyManifestEdit(manifestPath, edit) {
16186
16859
  manifestPath,
16187
16860
  edit,
16188
16861
  async ({ absolute, next, tmp }) => {
16189
- await (0, import_promises20.rename)(tmp, absolute);
16862
+ await (0, import_promises21.rename)(tmp, absolute);
16190
16863
  return { manifestPath: absolute, sha256: sha256(next), changed: true };
16191
16864
  },
16192
16865
  (absolute, original) => ({ manifestPath: absolute, sha256: sha256(original), changed: false })
@@ -16194,18 +16867,18 @@ async function applyManifestEdit(manifestPath, edit) {
16194
16867
  }
16195
16868
 
16196
16869
  // src/pipeline/hand-edit.ts
16197
- var import_node_fs25 = require("fs");
16198
- var import_promises21 = require("fs/promises");
16199
- 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);
16200
16873
  var import_zod9 = require("zod");
16201
16874
  var HAND_EDIT_DIR = "edits";
16202
16875
  function handEditPath(loaded, spec) {
16203
16876
  const style = loaded.manifest.styles[spec.styleId];
16204
- const outDir = import_node_path29.default.resolve(loaded.root, style?.outDir ?? ".");
16205
- const relative = import_node_path29.default.relative(outDir, spec.outFile);
16206
- const inside = relative && !relative.startsWith("..") && !import_node_path29.default.isAbsolute(relative);
16207
- const file = inside ? relative : import_node_path29.default.basename(spec.outFile);
16208
- 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"));
16209
16882
  }
16210
16883
  function handEditBases(spec, lock) {
16211
16884
  const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
@@ -16218,10 +16891,10 @@ function handEditBases(spec, lock) {
16218
16891
  }));
16219
16892
  if (members.some((member) => member.mediaType && member.mediaType !== "image/png")) return null;
16220
16893
  if (entry.outputs.length === 1 && !primaryOutput(entry)) return null;
16221
- 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;
16222
16895
  return members.map(({ role, path: file }) => ({ role, path: file }));
16223
16896
  }
16224
- 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;
16225
16898
  }
16226
16899
  function handEditBase(spec, lock) {
16227
16900
  const bases = handEditBases(spec, lock);
@@ -16251,19 +16924,19 @@ async function startHandEdit(loaded, lock, spec, opts = {}) {
16251
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`
16252
16925
  );
16253
16926
  }
16254
- 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;
16255
16928
  const editPath = existing ?? handEditPath(loaded, spec);
16256
16929
  const members = handEditMembers(loaded, spec, lock, editPath);
16257
16930
  let created = false;
16258
16931
  for (const member of members) {
16259
- if ((0, import_node_fs25.existsSync)(member.path)) continue;
16932
+ if ((0, import_node_fs26.existsSync)(member.path)) continue;
16260
16933
  if (!member.base) {
16261
16934
  throw new ManifestEditError(
16262
16935
  `${spec.styleId}/${spec.assetId} has no generated PNG on disk to start from; generate or restore it first`
16263
16936
  );
16264
16937
  }
16265
- await (0, import_promises21.mkdir)(import_node_path29.default.dirname(member.path), { recursive: true });
16266
- 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);
16267
16940
  created = true;
16268
16941
  }
16269
16942
  const source = portableOutputPath(editPath, loaded.root);
@@ -16341,7 +17014,7 @@ function handEditProjectPath(editPath) {
16341
17014
  async function readHandEditCompanion(editPath) {
16342
17015
  let text;
16343
17016
  try {
16344
- text = await (0, import_promises21.readFile)(handEditCompanionPath(editPath), "utf8");
17017
+ text = await (0, import_promises22.readFile)(handEditCompanionPath(editPath), "utf8");
16345
17018
  } catch {
16346
17019
  return null;
16347
17020
  }
@@ -16384,7 +17057,7 @@ async function saveHandEdit(loaded, lock, spec, input) {
16384
17057
  }
16385
17058
  });
16386
17059
  const bases = handEditBases(spec, lock);
16387
- 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 };
16388
17061
  for (const [index, image] of decoded.entries()) {
16389
17062
  if (image.width !== expectedSize.width || image.height !== expectedSize.height) {
16390
17063
  const which = memberSet ? `member ${incoming[index].role ?? index}` : "the edit";
@@ -16400,7 +17073,7 @@ async function saveHandEdit(loaded, lock, spec, input) {
16400
17073
  editor: input.editor,
16401
17074
  protocol: input.protocol,
16402
17075
  savedAt: (input.now ?? /* @__PURE__ */ new Date()).toISOString(),
16403
- project: projectPath ? import_node_path29.default.basename(projectPath) : null,
17076
+ project: projectPath ? import_node_path30.default.basename(projectPath) : null,
16404
17077
  outputs: incoming.map((frame, index) => ({
16405
17078
  role: frame.role,
16406
17079
  basedOn: basedOnHash(entry, index),
@@ -16430,10 +17103,10 @@ function basedOnHash(entry, index) {
16430
17103
  async function replaceFile(file, bytes) {
16431
17104
  const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
16432
17105
  try {
16433
- await (0, import_promises21.writeFile)(tmp, bytes);
16434
- await (0, import_promises21.rename)(tmp, file);
17106
+ await (0, import_promises22.writeFile)(tmp, bytes);
17107
+ await (0, import_promises22.rename)(tmp, file);
16435
17108
  } finally {
16436
- await (0, import_promises21.rm)(tmp, { force: true });
17109
+ await (0, import_promises22.rm)(tmp, { force: true });
16437
17110
  }
16438
17111
  }
16439
17112
  function openInEditor(file, editor = process.env.PIXELKILN_EDITOR) {
@@ -16442,9 +17115,9 @@ function openInEditor(file, editor = process.env.PIXELKILN_EDITOR) {
16442
17115
  }
16443
17116
 
16444
17117
  // src/gallery/upload.ts
16445
- var import_node_fs26 = require("fs");
16446
- var import_promises22 = require("fs/promises");
16447
- 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);
16448
17121
  var import_zod10 = require("zod");
16449
17122
  var MAX_UPLOAD_BYTES = 8 * 1024 * 1024;
16450
17123
  var UploadImageSchema = import_zod10.z.object({
@@ -16457,12 +17130,12 @@ var UploadImageSchema = import_zod10.z.object({
16457
17130
  overwrite: import_zod10.z.boolean().optional()
16458
17131
  }).strict();
16459
17132
  async function saveProjectImage(root, request) {
16460
- const file = import_node_path30.default.resolve(root, request.path);
16461
- const relative = import_node_path30.default.relative(root, file);
16462
- 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)) {
16463
17136
  throw new ManifestEditError("an upload must stay inside the project");
16464
17137
  }
16465
- const extension = import_node_path30.default.extname(file).toLowerCase();
17138
+ const extension = import_node_path31.default.extname(file).toLowerCase();
16466
17139
  const expected = extension === ".png" ? "png" : extension === ".jpg" || extension === ".jpeg" ? "jpeg" : null;
16467
17140
  if (!expected) throw new ManifestEditError("an upload must be a .png, .jpg, or .jpeg path");
16468
17141
  const bytes = Buffer.from(request.base64, "base64");
@@ -16474,18 +17147,18 @@ async function saveProjectImage(root, request) {
16474
17147
  if (metadata.format !== expected) {
16475
17148
  throw new ManifestEditError(`the upload is a ${metadata.format.toUpperCase()}; name it ${metadata.format === "png" ? ".png" : ".jpg"}`);
16476
17149
  }
16477
- if ((0, import_node_fs26.existsSync)(file) && !request.overwrite) {
16478
- 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`);
16479
17152
  }
16480
- await (0, import_promises22.mkdir)(import_node_path30.default.dirname(file), { recursive: true });
16481
- await (0, import_promises22.writeFile)(file, bytes);
16482
- 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("/");
16483
17156
  }
16484
17157
 
16485
17158
  // src/gallery/image-revision.ts
16486
- var import_node_fs27 = require("fs");
16487
- var import_promises23 = require("fs/promises");
16488
- 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);
16489
17162
  var import_zod11 = require("zod");
16490
17163
  var InpaintRevisionRequestSchema = import_zod11.z.object({
16491
17164
  action: import_zod11.z.literal("create-inpaint-revision"),
@@ -16504,15 +17177,15 @@ var InpaintRevisionRequestSchema = import_zod11.z.object({
16504
17177
  overwrite: import_zod11.z.boolean().optional()
16505
17178
  }).strict();
16506
17179
  async function createInpaintRevision(manifestPath, request) {
16507
- const root = import_node_path31.default.dirname(import_node_path31.default.resolve(manifestPath));
16508
- 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") {
16509
17182
  throw new ManifestEditError("the mask must be a .png path inside the project");
16510
17183
  }
16511
17184
  const bytes = Buffer.from(request.maskBase64, "base64");
16512
17185
  const metadata = imageMetadata(bytes);
16513
17186
  if (!metadata || metadata.format !== "png") throw new ManifestEditError("the mask is not a readable PNG");
16514
- const file = import_node_path31.default.resolve(root, request.maskPath);
16515
- 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;
16516
17189
  const mask = await saveProjectImage(root, {
16517
17190
  action: "upload-image",
16518
17191
  path: request.maskPath,
@@ -16531,17 +17204,17 @@ async function createInpaintRevision(manifestPath, request) {
16531
17204
  }
16532
17205
  });
16533
17206
  } catch (error) {
16534
- if (previous) await (0, import_promises23.writeFile)(file, previous);
16535
- 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 });
16536
17209
  throw error;
16537
17210
  }
16538
17211
  return mask;
16539
17212
  }
16540
17213
 
16541
17214
  // src/gallery/skeleton.ts
16542
- var import_node_fs28 = require("fs");
16543
- var import_promises24 = require("fs/promises");
16544
- 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);
16545
17218
  var import_zod12 = require("zod");
16546
17219
  var SkeletonEstimateRequestSchema = import_zod12.z.object({
16547
17220
  project: import_zod12.z.string().min(1).optional(),
@@ -16579,21 +17252,21 @@ var SkeletonKeypointsUpdateSchema = import_zod12.z.object({
16579
17252
  set: SkeletonSetSchema
16580
17253
  }).strict();
16581
17254
  async function createSkeletonAnimation(manifestPath, request) {
16582
- const root = import_node_path32.default.dirname(import_node_path32.default.resolve(manifestPath));
16583
- const file = import_node_path32.default.resolve(root, request.keypointsFile);
16584
- const relative = import_node_path32.default.relative(root, file);
16585
- 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)) {
16586
17259
  throw new ManifestEditError("the keypoints file must be a .json path inside the project");
16587
17260
  }
16588
- const existed = (0, import_node_fs28.existsSync)(file);
17261
+ const existed = (0, import_node_fs29.existsSync)(file);
16589
17262
  let previous = null;
16590
17263
  if (request.set) {
16591
17264
  if (existed && !request.overwrite) {
16592
17265
  throw new ManifestEditError(`${request.keypointsFile} already exists; choose another path, or replace it`);
16593
17266
  }
16594
- if (existed) previous = await (0, import_promises24.readFile)(file);
16595
- await (0, import_promises24.mkdir)(import_node_path32.default.dirname(file), { recursive: true });
16596
- 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");
16597
17270
  } else if (!existed) {
16598
17271
  throw new ManifestEditError(`${request.keypointsFile} does not exist; paste poses or estimate them from the sprite`);
16599
17272
  }
@@ -16608,7 +17281,7 @@ async function createSkeletonAnimation(manifestPath, request) {
16608
17281
  revision: {
16609
17282
  mode: "animate-skeleton",
16610
17283
  from: request.from,
16611
- keypointsFile: relative.split(import_node_path32.default.sep).join("/"),
17284
+ keypointsFile: relative.split(import_node_path33.default.sep).join("/"),
16612
17285
  direction: request.direction,
16613
17286
  ...request.description ? { description: request.description } : {},
16614
17287
  ...request.skeletonTemplate ? { skeletonTemplate: request.skeletonTemplate } : {}
@@ -16617,8 +17290,8 @@ async function createSkeletonAnimation(manifestPath, request) {
16617
17290
  });
16618
17291
  } catch (error) {
16619
17292
  if (request.set) {
16620
- if (previous) await (0, import_promises24.writeFile)(file, previous);
16621
- 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 });
16622
17295
  }
16623
17296
  throw error;
16624
17297
  }
@@ -16629,19 +17302,19 @@ async function updateSkeletonKeypoints(ctx, request) {
16629
17302
  if (!spec) throw new ManifestEditError(`"${key}" is not declared by the manifest`);
16630
17303
  const file = spec.revision?.mode === "animate-skeleton" ? spec.revision.keypointsFile : void 0;
16631
17304
  if (!file) throw new ManifestEditError(`${key} is not an animate-skeleton revision`);
16632
- const relative = import_node_path32.default.relative(ctx.loaded.root, file);
16633
- 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)) {
16634
17307
  throw new ManifestEditError(`${key}'s keypoints file is outside the project; edit it by hand`);
16635
17308
  }
16636
- 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;
16637
17310
  if (current !== request.expectedSha256) {
16638
17311
  throw Object.assign(
16639
- 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`),
16640
17313
  { status: 409 }
16641
17314
  );
16642
17315
  }
16643
- await (0, import_promises24.writeFile)(file, JSON.stringify(request.set, null, 2) + "\n");
16644
- 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("/");
16645
17318
  }
16646
17319
 
16647
17320
  // src/gallery/edit.ts
@@ -16695,7 +17368,7 @@ function createGalleryEditHandler(opts) {
16695
17368
  const history = /* @__PURE__ */ new Map();
16696
17369
  const manifestOf = async (project) => {
16697
17370
  try {
16698
- 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));
16699
17372
  } catch {
16700
17373
  return null;
16701
17374
  }
@@ -16705,7 +17378,7 @@ function createGalleryEditHandler(opts) {
16705
17378
  if (undo.success) {
16706
17379
  const manifestPath2 = await manifestOf(undo.data.project);
16707
17380
  if (!manifestPath2) throw new ManifestEditError("no manifest to undo an edit in");
16708
- const current = await (0, import_promises25.readFile)(manifestPath2, "utf8");
17381
+ const current = await (0, import_promises26.readFile)(manifestPath2, "utf8");
16709
17382
  if (sha256(current) !== undo.data.expectedSha256) throw new ManifestDriftError(manifestPath2);
16710
17383
  const stack = history.get(manifestPath2) ?? [];
16711
17384
  const last = stack[stack.length - 1];
@@ -16715,17 +17388,17 @@ function createGalleryEditHandler(opts) {
16715
17388
  throw Object.assign(new Error("the manifest changed outside the gallery since its last edit; undo would discard that change"), { status: 409 });
16716
17389
  }
16717
17390
  const tmp = manifestPath2 + ".pixelkiln-undo";
16718
- await (0, import_promises25.writeFile)(tmp, last.before);
16719
- await (0, import_promises25.rename)(tmp, manifestPath2);
17391
+ await (0, import_promises26.writeFile)(tmp, last.before);
17392
+ await (0, import_promises26.rename)(tmp, manifestPath2);
16720
17393
  stack.pop();
16721
- 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}`);
16722
17395
  return opts.reload();
16723
17396
  }
16724
17397
  const manifestPath = await manifestOf(body?.project);
16725
- 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;
16726
17399
  const build = await apply(body);
16727
17400
  if (manifestPath && before !== null) {
16728
- const after = await (0, import_promises25.readFile)(manifestPath, "utf8").catch(() => null);
17401
+ const after = await (0, import_promises26.readFile)(manifestPath, "utf8").catch(() => null);
16729
17402
  if (after !== null && after !== before) {
16730
17403
  const stack = history.get(manifestPath) ?? [];
16731
17404
  stack.push({ before, afterSha: sha256(after) });
@@ -16780,7 +17453,7 @@ function createEditApplier(opts, log) {
16780
17453
  } catch (error) {
16781
17454
  throw new ManifestEditError(error instanceof Error ? error.message : String(error));
16782
17455
  }
16783
- 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);
16784
17457
  log(` image added: ${written}`);
16785
17458
  return opts.reload();
16786
17459
  }
@@ -16847,7 +17520,7 @@ function createEditApplier(opts, log) {
16847
17520
  }
16848
17521
  const result = await applyManifestEdit(manifestPath, parsed.data);
16849
17522
  if (result.changed) {
16850
- const relative = import_node_path33.default.relative(process.cwd(), result.manifestPath);
17523
+ const relative = import_node_path34.default.relative(process.cwd(), result.manifestPath);
16851
17524
  const shown = !relative || relative.startsWith("..") ? result.manifestPath : relative;
16852
17525
  log(` manifest edited: ${describeEdit(parsed.data)} in ${shown}`);
16853
17526
  }
@@ -16875,15 +17548,20 @@ function describeEdit(edit) {
16875
17548
  // src/gallery/snapshot.ts
16876
17549
  var PROJECT_FILE_TYPE = "application/octet-stream";
16877
17550
  function galleryMediaId(absolutePath) {
16878
- 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);
16879
17552
  }
16880
17553
  function galleryMediaRoute(id) {
16881
17554
  return `/media/${id}`;
16882
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
+ }
16883
17560
  function isDeclared(loaded, styleId, assetId) {
16884
17561
  const asset = loaded.manifest.assets[assetId];
16885
- if (!asset || !loaded.manifest.styles[styleId]) return false;
16886
- 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);
16887
17565
  }
16888
17566
  function matchesFilter(filter, styleId, assetId) {
16889
17567
  if (filter?.styles?.length && !filter.styles.includes(styleId)) return false;
@@ -16894,9 +17572,9 @@ function describeHistory(media, entry, cacheDir) {
16894
17572
  return (entry.history ?? []).map((generation, i) => {
16895
17573
  const outputs = generation.outputs.map((output) => {
16896
17574
  const mediaType = output.mediaType ?? MediaType.PNG;
16897
- 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));
16898
17576
  let url = null;
16899
- if ((0, import_node_fs29.existsSync)(file)) {
17577
+ if ((0, import_node_fs30.existsSync)(file)) {
16900
17578
  const id = galleryMediaId(file);
16901
17579
  media.set(id, { path: file, contentType: mediaType });
16902
17580
  url = `${galleryMediaRoute(id)}?v=${output.sha256.slice(0, 24)}`;
@@ -16928,7 +17606,7 @@ function describeHistory(media, entry, cacheDir) {
16928
17606
  var MAX_COMPARED_BYTES = 8 * 1024 * 1024;
16929
17607
  async function samePixels(a, b) {
16930
17608
  try {
16931
- 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)]);
16932
17610
  if (left.length > MAX_COMPARED_BYTES || right.length > MAX_COMPARED_BYTES) return false;
16933
17611
  const x = decodePng(left);
16934
17612
  const y = decodePng(right);
@@ -16994,7 +17672,7 @@ function describeCharacter(spec, entry) {
16994
17672
  }
16995
17673
  async function fileInfo(absolutePath) {
16996
17674
  try {
16997
- const info = await (0, import_promises26.stat)(absolutePath);
17675
+ const info = await (0, import_promises27.stat)(absolutePath);
16998
17676
  return info.isFile() ? { bytes: info.size, modifiedAt: info.mtime.toISOString() } : null;
16999
17677
  } catch {
17000
17678
  return null;
@@ -17026,10 +17704,10 @@ async function describeOutput(media, root, absolutePath, recorded = {}) {
17026
17704
  async function describeQuality(media, root, inspection) {
17027
17705
  const outputPaths = inspection.outputs ?? [inspection.output];
17028
17706
  const outputs = await Promise.all(
17029
- 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)))
17030
17708
  );
17031
- const recordPath = import_node_path34.default.resolve(root, inspection.record);
17032
- 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);
17033
17711
  let options = null;
17034
17712
  let check = null;
17035
17713
  if (recordExists) {
@@ -17048,7 +17726,7 @@ async function describeQuality(media, root, inspection) {
17048
17726
  return {
17049
17727
  state: inspection.state,
17050
17728
  reason: inspection.reason,
17051
- output: portableOutputPath(import_node_path34.default.resolve(root, inspection.output), root),
17729
+ output: portableOutputPath(import_node_path35.default.resolve(root, inspection.output), root),
17052
17730
  record: portableOutputPath(recordPath, root),
17053
17731
  recordExists,
17054
17732
  outputs,
@@ -17086,9 +17764,9 @@ async function buildGallerySnapshot(opts) {
17086
17764
  const media = /* @__PURE__ */ new Map();
17087
17765
  const plan = await buildPlan(specs, lock);
17088
17766
  const items = [];
17089
- const manifestText = await (0, import_promises26.readFile)(loaded.path, "utf8");
17767
+ const manifestText = await (0, import_promises27.readFile)(loaded.path, "utf8");
17090
17768
  const manifestSha256 = sha256(manifestText);
17091
- 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");
17092
17770
  let rawStyles = {};
17093
17771
  try {
17094
17772
  const raw = JSON.parse(manifestText);
@@ -17112,12 +17790,12 @@ async function buildGallerySnapshot(opts) {
17112
17790
  )
17113
17791
  );
17114
17792
  if (spec.source) {
17115
- const editPath = import_node_path34.default.resolve(root, spec.source);
17793
+ const editPath = import_node_path35.default.resolve(root, spec.source);
17116
17794
  const memberCount = sourceIsStem(spec.source, entry, root) ? entry.outputs.length : 1;
17117
17795
  const editShas = await Promise.all(
17118
17796
  Array.from({ length: memberCount }, async (_, index) => {
17119
17797
  const file = sourceOutputPath(spec.source, entry, index, root);
17120
- 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 };
17121
17799
  })
17122
17800
  );
17123
17801
  edits = await Promise.all(editShas.map(({ file, sha256: editSha }) => describeOutput(media, root, file, { sha256: editSha })));
@@ -17125,7 +17803,7 @@ async function buildGallerySnapshot(opts) {
17125
17803
  const companion = await readHandEditCompanion(editPath);
17126
17804
  if (companion) {
17127
17805
  const projectPath = handEditProjectPath(editPath);
17128
- 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;
17129
17807
  let projectUrl = null;
17130
17808
  if (project) {
17131
17809
  const id = galleryMediaId(projectPath);
@@ -17161,8 +17839,8 @@ async function buildGallerySnapshot(opts) {
17161
17839
  editStatus = edits.some((member) => !member.exists) ? "missing" : editChanged.every((changed) => !changed) ? "same" : regenerated ? "regenerated-since" : "edited";
17162
17840
  }
17163
17841
  } else if (spec.source) {
17164
- outputs = [await describeOutput(media, root, import_node_path34.default.resolve(root, spec.source))];
17165
- } 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)) {
17166
17844
  outputs = [await describeOutput(media, root, spec.outFile)];
17167
17845
  } else {
17168
17846
  outputs = [];
@@ -17183,8 +17861,8 @@ async function buildGallerySnapshot(opts) {
17183
17861
  generator: entry?.generator ?? spec.generator,
17184
17862
  prompt: entry?.prompt ?? spec.prompt,
17185
17863
  currentPrompt: entry && entry.prompt !== spec.prompt ? spec.prompt : null,
17186
- width: entry?.width ?? spec.width,
17187
- height: entry?.height ?? spec.height,
17864
+ width: renderedSize(entry)?.width ?? entry?.width ?? spec.width,
17865
+ height: renderedSize(entry)?.height ?? entry?.height ?? spec.height,
17188
17866
  tileFeature: entry?.tileFeature ?? spec.tileFeature ?? null,
17189
17867
  fps: quality?.frameSet?.fps ?? metadataFps(entry) ?? spec.quality?.fps ?? null,
17190
17868
  cost: entry?.cost ?? 0,
@@ -17365,7 +18043,7 @@ async function buildGallerySnapshot(opts) {
17365
18043
  id: loaded.manifest.name,
17366
18044
  name: loaded.manifest.name,
17367
18045
  manifest: loaded.path,
17368
- lock: import_node_path34.default.resolve(opts.lockPath),
18046
+ lock: import_node_path35.default.resolve(opts.lockPath),
17369
18047
  root,
17370
18048
  provider: loaded.manifest.provider,
17371
18049
  account: null,
@@ -17391,7 +18069,7 @@ async function buildGallerySnapshot(opts) {
17391
18069
  return { snapshot, media };
17392
18070
  }
17393
18071
  async function buildWorkspaceGallerySnapshot(opts) {
17394
- 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));
17395
18073
  const media = /* @__PURE__ */ new Map();
17396
18074
  const projects = [];
17397
18075
  const items = [];
@@ -17434,11 +18112,11 @@ async function buildWorkspaceGallerySnapshot(opts) {
17434
18112
  id: project.id,
17435
18113
  name: loaded.manifest.name,
17436
18114
  manifest: manifestPath,
17437
- lock: import_node_path34.default.resolve(lockPath),
18115
+ lock: import_node_path35.default.resolve(lockPath),
17438
18116
  root: loaded.root,
17439
18117
  provider: loaded.manifest.provider,
17440
18118
  account: project.account ?? null,
17441
- manifestSha256: manifestSha256 ?? sha256(await (0, import_promises26.readFile)(manifestPath, "utf8")),
18119
+ manifestSha256: manifestSha256 ?? sha256(await (0, import_promises27.readFile)(manifestPath, "utf8")),
17442
18120
  historyLimit: historyLimit(loaded.manifest),
17443
18121
  entries: Object.keys(lock.entries).length,
17444
18122
  items: projectItems.length,
@@ -17450,8 +18128,8 @@ async function buildWorkspaceGallerySnapshot(opts) {
17450
18128
  id: project.id,
17451
18129
  name: project.id,
17452
18130
  manifest: manifestPath,
17453
- lock: import_node_path34.default.resolve(lockPath),
17454
- root: import_node_path34.default.dirname(manifestPath),
18131
+ lock: import_node_path35.default.resolve(lockPath),
18132
+ root: import_node_path35.default.dirname(manifestPath),
17455
18133
  provider: project.provider,
17456
18134
  account: project.account ?? null,
17457
18135
  manifestSha256: null,
@@ -17478,7 +18156,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
17478
18156
  version: 1,
17479
18157
  generatedAt: (opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
17480
18158
  project: null,
17481
- workspace: { path: import_node_path34.default.resolve(opts.workspacePath), dir, projects },
18159
+ workspace: { path: import_node_path35.default.resolve(opts.workspacePath), dir, projects },
17482
18160
  filter,
17483
18161
  totals: {
17484
18162
  entries: projects.reduce((sum, project) => sum + project.entries, 0),
@@ -17495,7 +18173,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
17495
18173
  }
17496
18174
 
17497
18175
  // src/gallery/page.ts
17498
- var import_node_fs30 = require("fs");
18176
+ var import_node_fs31 = require("fs");
17499
18177
 
17500
18178
  // src/gallery/client-bundle.ts
17501
18179
  var import_node_module = require("module");
@@ -17535,8 +18213,8 @@ var client;
17535
18213
  function clientAssets() {
17536
18214
  const prebuilt = new URL("gallery.js", CLIENT_DIR);
17537
18215
  client ??= {
17538
- css: (0, import_node_fs30.readFileSync)(new URL("gallery.css", CLIENT_DIR), "utf8"),
17539
- 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()
17540
18218
  };
17541
18219
  return client;
17542
18220
  }
@@ -17679,10 +18357,10 @@ var EDITOR_RELEASE_BASE = "https://github.com/gfargo/pixelkiln/releases/download
17679
18357
 
17680
18358
  // src/editor/install.ts
17681
18359
  var import_node_crypto8 = require("crypto");
17682
- var import_node_fs31 = require("fs");
17683
- var import_promises27 = require("fs/promises");
18360
+ var import_node_fs32 = require("fs");
18361
+ var import_promises28 = require("fs/promises");
17684
18362
  var import_node_os4 = __toESM(require("os"), 1);
17685
- var import_node_path35 = __toESM(require("path"), 1);
18363
+ var import_node_path36 = __toESM(require("path"), 1);
17686
18364
  var EditorInstallError = class extends Error {
17687
18365
  constructor(message6, file) {
17688
18366
  super(message6);
@@ -17693,15 +18371,15 @@ var EditorInstallError = class extends Error {
17693
18371
  };
17694
18372
  function editorToolsRoot() {
17695
18373
  const override = process.env.PIXELKILN_TOOLS_DIR?.trim();
17696
- if (override) return import_node_path35.default.resolve(override);
17697
- 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");
17698
18376
  if (process.platform === "win32") {
17699
- 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");
17700
18378
  }
17701
- 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");
17702
18380
  }
17703
18381
  function editorDir(pin = EDITOR_PIN, root = editorToolsRoot()) {
17704
- return import_node_path35.default.join(root, "pixelorama", pin.release ?? "unpublished");
18382
+ return import_node_path36.default.join(root, "pixelorama", pin.release ?? "unpublished");
17705
18383
  }
17706
18384
  function editorBaseUrl(pin = EDITOR_PIN) {
17707
18385
  const override = process.env.PIXELKILN_EDITOR_URL?.trim();
@@ -17711,7 +18389,7 @@ function editorBaseUrl(pin = EDITOR_PIN) {
17711
18389
  }
17712
18390
  async function sha256Of(file) {
17713
18391
  try {
17714
- 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");
17715
18393
  } catch {
17716
18394
  return null;
17717
18395
  }
@@ -17724,8 +18402,8 @@ async function editorStatus(opts = {}) {
17724
18402
  let totalBytes = 0;
17725
18403
  for (const [name, expected] of Object.entries(pin.files)) {
17726
18404
  totalBytes += expected.bytes;
17727
- const file = import_node_path35.default.join(dir, name);
17728
- 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;
17729
18407
  if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
17730
18408
  installedBytes += expected.bytes;
17731
18409
  } else {
@@ -17754,7 +18432,7 @@ async function installEditor(opts = {}) {
17754
18432
  );
17755
18433
  }
17756
18434
  if (!baseUrl) throw new EditorInstallError("no download location for the editor build");
17757
- await (0, import_promises27.mkdir)(dir, { recursive: true });
18435
+ await (0, import_promises28.mkdir)(dir, { recursive: true });
17758
18436
  const downloaded = [];
17759
18437
  const skipped = Object.keys(pin.files).filter((name) => !before.missing.includes(name));
17760
18438
  let fetchedBytes = before.installedBytes;
@@ -17782,13 +18460,13 @@ async function installEditor(opts = {}) {
17782
18460
  if (digest2 !== expected.sha256) {
17783
18461
  throw new EditorInstallError(`${name}: sha256 ${digest2.slice(0, 12)}\u2026 does not match the pinned ${expected.sha256.slice(0, 12)}\u2026`, name);
17784
18462
  }
17785
- const final = import_node_path35.default.join(dir, name);
18463
+ const final = import_node_path36.default.join(dir, name);
17786
18464
  const tmp = `${final}.${process.pid}.part`;
17787
18465
  try {
17788
- await (0, import_promises27.writeFile)(tmp, bytes);
17789
- await (0, import_promises27.rename)(tmp, final);
18466
+ await (0, import_promises28.writeFile)(tmp, bytes);
18467
+ await (0, import_promises28.rename)(tmp, final);
17790
18468
  } finally {
17791
- await (0, import_promises27.rm)(tmp, { force: true });
18469
+ await (0, import_promises28.rm)(tmp, { force: true });
17792
18470
  }
17793
18471
  fetchedBytes += bytes.length;
17794
18472
  downloaded.push(name);
@@ -17828,12 +18506,12 @@ var EDITOR_CONTENT_TYPES = {
17828
18506
  ".json": "application/json; charset=utf-8"
17829
18507
  };
17830
18508
  function editorContentType(name) {
17831
- 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;
17832
18510
  }
17833
18511
 
17834
18512
  // src/gallery/editor.ts
17835
- var import_promises28 = require("fs/promises");
17836
- var import_node_path36 = __toESM(require("path"), 1);
18513
+ var import_promises29 = require("fs/promises");
18514
+ var import_node_path37 = __toESM(require("path"), 1);
17837
18515
  function editorRoute(release, name = "index.html") {
17838
18516
  return `/editor/${encodeURIComponent(release)}/${encodeURIComponent(name)}`;
17839
18517
  }
@@ -17906,7 +18584,7 @@ function createGalleryEditorHandlers(opts = {}) {
17906
18584
  if (!status.installed) return null;
17907
18585
  let bytes;
17908
18586
  try {
17909
- 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));
17910
18588
  } catch {
17911
18589
  verified = null;
17912
18590
  return null;
@@ -17923,7 +18601,7 @@ function createGalleryEditorHandlers(opts = {}) {
17923
18601
  // src/gallery/server.ts
17924
18602
  var import_node_crypto9 = require("crypto");
17925
18603
  var import_node_http2 = require("http");
17926
- var import_promises29 = require("fs/promises");
18604
+ var import_promises30 = require("fs/promises");
17927
18605
  var MAX_EDIT_BYTES = 48 * 1024 * 1024;
17928
18606
  var EDITOR_CSP = [
17929
18607
  "default-src 'self'",
@@ -18104,7 +18782,7 @@ async function serveGallery(opts) {
18104
18782
  const asset = reviewAssets.get(jobId)?.get(url2.pathname);
18105
18783
  if (!asset) return fail(404, "no such review asset");
18106
18784
  try {
18107
- const bytes = await (0, import_promises29.readFile)(asset.path);
18785
+ const bytes = await (0, import_promises30.readFile)(asset.path);
18108
18786
  res.writeHead(200, {
18109
18787
  "Content-Type": asset.contentType,
18110
18788
  "Cache-Control": "no-store",
@@ -18187,7 +18865,7 @@ async function serveGallery(opts) {
18187
18865
  if (!asset) return fail(404, "no such gallery media");
18188
18866
  let bytes;
18189
18867
  try {
18190
- bytes = await (0, import_promises29.readFile)(asset.path);
18868
+ bytes = await (0, import_promises30.readFile)(asset.path);
18191
18869
  } catch {
18192
18870
  return fail(404, "gallery media is no longer on disk; refresh the page");
18193
18871
  }
@@ -18597,19 +19275,19 @@ function createGenerateHandlers(opts) {
18597
19275
  }
18598
19276
 
18599
19277
  // src/pipeline/salvage.ts
18600
- var import_promises30 = require("fs/promises");
18601
- var import_node_fs32 = require("fs");
18602
- 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);
18603
19281
  function providerClaimId(provider, objectId) {
18604
19282
  return `${encodeURIComponent(provider)}:${encodeURIComponent(objectId)}`;
18605
19283
  }
18606
19284
  async function loadClaims(lockPaths, opts = {}) {
18607
19285
  const claimed = /* @__PURE__ */ new Set();
18608
19286
  for (const p of lockPaths) {
18609
- 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}`);
18610
19288
  let parsed;
18611
19289
  try {
18612
- parsed = parseLock(JSON.parse(await (0, import_promises30.readFile)(p, "utf8")));
19290
+ parsed = parseLock(JSON.parse(await (0, import_promises31.readFile)(p, "utf8")));
18613
19291
  } catch {
18614
19292
  throw new Error(`Claim lockfile is malformed: ${p}`);
18615
19293
  }
@@ -18713,20 +19391,20 @@ function matchOrphanStyle(prompt, manifest) {
18713
19391
  return matchStyleByPattern(prompt, manifest);
18714
19392
  }
18715
19393
  async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, claimPaths) {
18716
- const own = import_node_path37.default.resolve(ownManifestPath);
19394
+ const own = import_node_path38.default.resolve(ownManifestPath);
18717
19395
  const siblingManifestPaths = [
18718
19396
  .../* @__PURE__ */ new Set([
18719
19397
  ...workspaceManifestPaths,
18720
- ...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"))
18721
19399
  ])
18722
19400
  ];
18723
19401
  const siblings = [];
18724
19402
  for (const siblingManifestPath of siblingManifestPaths) {
18725
- if (import_node_path37.default.resolve(siblingManifestPath) === own) continue;
18726
- 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;
18727
19405
  try {
18728
19406
  const { manifest } = await loadManifest(siblingManifestPath);
18729
- 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 });
18730
19408
  } catch {
18731
19409
  }
18732
19410
  }
@@ -18846,8 +19524,8 @@ async function applyTags(provider, decisions, existing, opts = {}) {
18846
19524
  }
18847
19525
 
18848
19526
  // src/pick/salvage-server.ts
18849
- var import_promises31 = require("fs/promises");
18850
- var import_node_path38 = __toESM(require("path"), 1);
19527
+ var import_promises32 = require("fs/promises");
19528
+ var import_node_path39 = __toESM(require("path"), 1);
18851
19529
 
18852
19530
  // src/pick/salvage-sheet.ts
18853
19531
  var escapeHtml3 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
@@ -19013,7 +19691,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
19013
19691
  });
19014
19692
  const html = renderSalvageSheet(orphans, {
19015
19693
  styleId: ctx.styleId,
19016
- importDir: import_node_path38.default.relative(process.cwd(), ctx.importDir) || "."
19694
+ importDir: import_node_path39.default.relative(process.cwd(), ctx.importDir) || "."
19017
19695
  });
19018
19696
  const byId = new Map(orphans.map((o) => [o.id, o]));
19019
19697
  const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
@@ -19069,10 +19747,10 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
19069
19747
  if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
19070
19748
  decodePng(buf);
19071
19749
  const assetId = idFromPrompt(orphan.prompt, taken);
19072
- const rel = import_node_path38.default.join("_salvaged", `${assetId}.png`);
19073
- const outFile = import_node_path38.default.resolve(ctx.importDir, rel);
19074
- await (0, import_promises31.mkdir)(import_node_path38.default.dirname(outFile), { recursive: true });
19075
- 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);
19076
19754
  ctx.manifest.assets[assetId] = {
19077
19755
  prompt: orphan.prompt,
19078
19756
  promptByStyle: {},
@@ -19104,7 +19782,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
19104
19782
  sourceUrl: durableSource,
19105
19783
  sourceUrls: durableSource ? [{ url: durableSource }] : [],
19106
19784
  outputs: [{
19107
- path: portableOutputPath(outFile, import_node_path38.default.dirname(ctx.manifestPath)),
19785
+ path: portableOutputPath(outFile, import_node_path39.default.dirname(ctx.manifestPath)),
19108
19786
  sha256: sha256(buf)
19109
19787
  }],
19110
19788
  submittedAt: orphan.createdAt,
@@ -19128,9 +19806,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
19128
19806
  }
19129
19807
  }
19130
19808
  await applyTags(provider, decisions, existingTags, { onProgress: log, characters: characterMembers });
19131
- 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"));
19132
19810
  for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
19133
- 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");
19134
19812
  await saveLock(ctx.lockPath, ctx.lock);
19135
19813
  if (characterAssetIds.length) result.characterAssetIds = characterAssetIds;
19136
19814
  return result;
@@ -19241,6 +19919,7 @@ async function workspaceStatus(ws, dir) {
19241
19919
  0 && (module.exports = {
19242
19920
  AssetBatchSchema,
19243
19921
  AssetSchema,
19922
+ AssetVfxSchema,
19244
19923
  BudgetError,
19245
19924
  CHARACTER_DIRECTIONS_4,
19246
19925
  CHARACTER_DIRECTIONS_8,
@@ -19295,11 +19974,16 @@ async function workspaceStatus(ws, dir) {
19295
19974
  MapSceneSchema,
19296
19975
  MediaType,
19297
19976
  OverwriteRefusedError,
19977
+ PIXELATE_COST_GENERATIONS,
19978
+ PIXELATE_MAX_SIDE,
19979
+ PIXELLAB_MCP_URL,
19298
19980
  PIXEL_ART_FIXER_REVISION,
19299
19981
  PIXEL_ART_FIXER_URL,
19982
+ PRO_TIER_PRICES,
19300
19983
  PixelKilnError,
19301
19984
  PixelLabClient,
19302
19985
  PixelLabError,
19986
+ PixelLabMcpClient,
19303
19987
  PixelLabProvider,
19304
19988
  PostprocessSchema,
19305
19989
  ProjectError,
@@ -19322,12 +20006,15 @@ async function workspaceStatus(ws, dir) {
19322
20006
  UiPieceSchema,
19323
20007
  UnsupportedCapabilityError,
19324
20008
  UsageError,
20009
+ VFX_TOOL,
20010
+ VfxPartSchema,
19325
20011
  WorkspaceProjectSchema,
19326
20012
  WorkspaceSchema,
19327
20013
  adopt,
19328
20014
  applyManifestEdit,
19329
20015
  applyTags,
19330
20016
  approveQualityRecord,
20017
+ assetInStyle,
19331
20018
  auditStyle,
19332
20019
  availableProviders,
19333
20020
  backoffMs,
@@ -19352,7 +20039,9 @@ async function workspaceStatus(ws, dir) {
19352
20039
  currentEntryOutputPath,
19353
20040
  currentOutputPath,
19354
20041
  defaultOpenCommand,
20042
+ defaultPixelateOut,
19355
20043
  defaultUnzoomOut,
20044
+ describeVfx,
19356
20045
  detachHandEdit,
19357
20046
  detectMediaType,
19358
20047
  doctor,
@@ -19373,6 +20062,7 @@ async function workspaceStatus(ws, dir) {
19373
20062
  fontOutputPaths,
19374
20063
  formatCost,
19375
20064
  frameSetFps,
20065
+ frameSetLoop,
19376
20066
  galleryContentSecurityPolicy,
19377
20067
  galleryMediaId,
19378
20068
  galleryMediaRoute,
@@ -19395,9 +20085,11 @@ async function workspaceStatus(ws, dir) {
19395
20085
  inspectRevisionReadiness,
19396
20086
  installEditor,
19397
20087
  installRecipe,
20088
+ isBusyReply,
19398
20089
  isFrameSetEntry,
19399
20090
  isMemberSetEntry,
19400
20091
  isSensitiveSourceUrl,
20092
+ isVfxReference,
19401
20093
  isometricTileCost,
19402
20094
  listBundledRecipes,
19403
20095
  loadClaims,
@@ -19406,7 +20098,9 @@ async function workspaceStatus(ws, dir) {
19406
20098
  loadSiblingManifests,
19407
20099
  loadWorkspace,
19408
20100
  lockKey,
20101
+ makeVfx,
19409
20102
  matchOrphanStyle,
20103
+ mcpClientFromEnv,
19410
20104
  measureBalanceChange,
19411
20105
  measureDecodedImageQuality,
19412
20106
  measureImageQuality,
@@ -19430,15 +20124,21 @@ async function workspaceStatus(ws, dir) {
19430
20124
  paletteDistance,
19431
20125
  parentAssetId,
19432
20126
  parseLock,
20127
+ parseMcpBody,
19433
20128
  parseProFlashQuote,
19434
20129
  parseTerrainDescriptions,
20130
+ parseVfxExport,
20131
+ parseVfxRecipe,
20132
+ parseVfxStep,
19435
20133
  parseWorkspace,
19436
20134
  pickHistory,
20135
+ pixelateFile,
19437
20136
  pngSize,
19438
20137
  poll,
19439
20138
  portableOutputPath,
19440
20139
  prepareReview,
19441
20140
  primaryOutput,
20141
+ proTierCost,
19442
20142
  proportionsPayload,
19443
20143
  providerCredentialEnvs,
19444
20144
  providerFactory,
@@ -19449,6 +20149,8 @@ async function workspaceStatus(ws, dir) {
19449
20149
  readArtifactBundleManifest,
19450
20150
  readHandEditCompanion,
19451
20151
  readQualityBaseline,
20152
+ readSavedVfxRecipe,
20153
+ readVfxRecipe,
19452
20154
  recipeDigest,
19453
20155
  referencedHashes,
19454
20156
  refineAsset,
@@ -19479,6 +20181,7 @@ async function workspaceStatus(ws, dir) {
19479
20181
  resolveSpecs,
19480
20182
  resolveStyleImages,
19481
20183
  resolveStyleOutputs,
20184
+ resolveVfxSprite,
19482
20185
  resumeActions,
19483
20186
  resumeCommandForStatus,
19484
20187
  retireGeneration,
@@ -19486,6 +20189,7 @@ async function workspaceStatus(ws, dir) {
19486
20189
  revertGeneration,
19487
20190
  runPicker,
19488
20191
  runSalvage,
20192
+ runVfxWorkbench,
19489
20193
  saveHandEdit,
19490
20194
  saveLock,
19491
20195
  saveWorkspace,
@@ -19496,6 +20200,7 @@ async function workspaceStatus(ws, dir) {
19496
20200
  sha256File,
19497
20201
  shouldPersistSourceUrl,
19498
20202
  shouldRetry,
20203
+ sliceVfxSheet,
19499
20204
  slugify,
19500
20205
  snapshotQualityBaseline,
19501
20206
  sourceIsStem,
@@ -19520,6 +20225,8 @@ async function workspaceStatus(ws, dir) {
19520
20225
  validateWorkspace,
19521
20226
  verifyArtifactBundle,
19522
20227
  verifyRecipe,
20228
+ vfxOutputPaths,
20229
+ vfxRecipeSha256,
19523
20230
  withArtifactManifest,
19524
20231
  workspaceClaims,
19525
20232
  workspaceStatus,