pixelkiln 0.87.0 → 0.88.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -67,8 +67,8 @@ function exitCodeFor(err) {
67
67
  }
68
68
 
69
69
  // src/cli/main.ts
70
- import { existsSync as existsSync38 } from "fs";
71
- import { readFile as readFile34 } from "fs/promises";
70
+ import { existsSync as existsSync39 } from "fs";
71
+ import { readFile as readFile35 } from "fs/promises";
72
72
 
73
73
  // src/cli/args.ts
74
74
  import path from "path";
@@ -109,7 +109,13 @@ var VALUE_FLAGS = [
109
109
  "--weight",
110
110
  "--glyph-px",
111
111
  "--frames",
112
- "--estimate-limit"
112
+ "--estimate-limit",
113
+ "--preset",
114
+ "--recipe",
115
+ "--effect",
116
+ "--part",
117
+ "--sprite",
118
+ "--fps"
113
119
  ];
114
120
  var BOOL_FLAGS = [
115
121
  "--force",
@@ -126,7 +132,8 @@ var BOOL_FLAGS = [
126
132
  "--prune",
127
133
  "--edit",
128
134
  "--refresh",
129
- "--no-editor"
135
+ "--no-editor",
136
+ "--game-pack"
130
137
  ];
131
138
  var COMMANDS = [
132
139
  "init",
@@ -163,6 +170,7 @@ var COMMANDS = [
163
170
  "skeleton-preview",
164
171
  "unzoom",
165
172
  "pixelate",
173
+ "vfx",
166
174
  "font",
167
175
  "help",
168
176
  "--help",
@@ -177,6 +185,7 @@ var QUALITY_SUBCOMMANDS = ["snapshot", "check"];
177
185
  var EDIT_SUBCOMMANDS = ["start", "detach"];
178
186
  var TOOLS_SUBCOMMANDS = ["status", "install"];
179
187
  var TOOLS = ["editor"];
188
+ var VFX_SUBCOMMANDS = ["make", "presets", "parts"];
180
189
  function parseArgs(argv) {
181
190
  const [command = "help"] = argv;
182
191
  if (!COMMANDS.includes(command)) {
@@ -266,6 +275,12 @@ function parseArgs(argv) {
266
275
  throw new UsageError("skeleton-preview needs a keypoints file or an animate-skeleton asset id");
267
276
  }
268
277
  rest = rest.slice(1);
278
+ } else if (command === "vfx") {
279
+ subcommand = rest[0]?.startsWith("-") || rest[0] === void 0 ? "make" : rest[0];
280
+ if (!VFX_SUBCOMMANDS.includes(subcommand)) {
281
+ throw new UsageError(`Unknown vfx subcommand "${subcommand}". Known: ${VFX_SUBCOMMANDS.join(", ")}`);
282
+ }
283
+ if (rest[0] === subcommand) rest = rest.slice(1);
269
284
  } else if (command === "refine") {
270
285
  subcommand = rest[0]?.startsWith("-") || rest[0] === void 0 ? "run" : rest[0];
271
286
  if (!REFINE_SUBCOMMANDS.includes(subcommand)) {
@@ -433,7 +448,14 @@ function parseArgs(argv) {
433
448
  weight: get("--weight"),
434
449
  glyphPx: numberOption("--glyph-px", { min: 8, max: 64, integer: true }),
435
450
  frames: numberOption("--frames", { min: 3, max: 15, integer: true }),
436
- estimateLimit: numberOption("--estimate-limit", { min: 0, max: 1e3, integer: true })
451
+ estimateLimit: numberOption("--estimate-limit", { min: 0, max: 1e3, integer: true }),
452
+ preset: get("--preset"),
453
+ recipe: get("--recipe"),
454
+ effect: get("--effect"),
455
+ parts: list("--part"),
456
+ sprite: get("--sprite"),
457
+ fps: numberOption("--fps", { min: 1, max: 60, integer: true }),
458
+ gamePack: rest.includes("--game-pack")
437
459
  };
438
460
  }
439
461
 
@@ -504,6 +526,10 @@ Commands
504
526
  pixel art with Image to Pixel Art Pro Flash; PixelLab picks the
505
527
  output size. Optional --description steers the style. 6
506
528
  generations; asks first.
529
+ vfx PixelLab: build an animated pixel-art effect with the VFX
530
+ workbench (free) from a --preset, a --recipe file, a saved
531
+ --effect, and/or --part; writes <out>.png/.gif/.json and a
532
+ .vfx.json recipe record. vfx presets / vfx parts list options.
507
533
  font PixelLab: generate a pixel font (--description, --out); writes a
508
534
  .ttf and a glyph atlas PNG. 15 generations; asks first.
509
535
 
@@ -531,6 +557,13 @@ Options
531
557
  --description <text> font: the style to draw, e.g. "warm orange arcade font";
532
558
  pixelate: an optional style steer, e.g. "light dithering"
533
559
  --weight <w> font: Bold or Regular (default)
560
+ --preset <name> vfx: start from a whole-effect preset (vfx presets lists them)
561
+ --recipe <file> vfx: render a recipe or an earlier run's .vfx.json record
562
+ --effect <id> vfx: start from a saved My effects id and export over it
563
+ --part <name> vfx: add a part (repeatable; vfx parts lists them)
564
+ --sprite <ref> vfx: build around a sprite: a file, a URL, or character:<id>:<dir>
565
+ --fps <n> vfx: frames per second, 1-60
566
+ --game-pack vfx: also download the game-pack ZIP
534
567
  --glyph-px <n> font: native glyph size, 8, 16 (default), 32, or 64
535
568
  --prune cache: remove invalid/unreferenced local cache data
536
569
  --manifest <path> Default: pixelkiln.manifest.json
@@ -590,6 +623,7 @@ Examples
590
623
  pixelkiln quality check --from pixelkiln.quality.json
591
624
  pixelkiln unzoom --from refs/knight-512.png --out refs/knight.png
592
625
  pixelkiln pixelate --from renders/lodge.png --out art/lodge.png
626
+ pixelkiln vfx --preset explosion --part scorch_mark --out fx/explosion --game-pack
593
627
  pixelkiln font --description "warm orange arcade font" --weight Bold --out fonts/arcade
594
628
  pixelkiln workspace add ../other-game/pixelkiln.manifest.json
595
629
  pixelkiln workspace status --json
@@ -672,16 +706,17 @@ import { createReadStream } from "fs";
672
706
  function sha256(data) {
673
707
  return createHash("sha256").update(data).digest("hex");
674
708
  }
675
- async function sha256File(path52) {
709
+ async function sha256File(path53) {
676
710
  return new Promise((resolve, reject) => {
677
711
  const hash = createHash("sha256");
678
- const stream = createReadStream(path52);
712
+ const stream = createReadStream(path53);
679
713
  stream.on("data", (chunk2) => hash.update(chunk2));
680
714
  stream.on("error", reject);
681
715
  stream.on("end", () => resolve(hash.digest("hex")));
682
716
  });
683
717
  }
684
718
  function specHash(spec, styleImageHashes, providerOptionIdentity = spec.providerOptions, providerInputIdentity = spec.providerInputs ?? {}) {
719
+ const effect = Boolean(spec.vfx);
685
720
  return sha256(
686
721
  JSON.stringify({
687
722
  // Preserve every existing PixelLab hash while making a provider switch
@@ -690,15 +725,15 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
690
725
  providerOptions: providerOptionIdentity && (typeof providerOptionIdentity !== "object" || Object.keys(providerOptionIdentity).length > 0) ? providerOptionIdentity : void 0,
691
726
  providerInputs: providerInputIdentity && (typeof providerInputIdentity !== "object" || Object.keys(providerInputIdentity).length > 0) ? providerInputIdentity : void 0,
692
727
  generator: spec.generator,
693
- prompt: spec.prompt,
694
- width: spec.width,
695
- height: spec.height,
696
- view: spec.view,
697
- outline: spec.outline ?? null,
698
- shading: spec.shading ?? null,
699
- detail: spec.detail ?? null,
700
- seed: spec.seed ?? null,
701
- palette: spec.palette,
728
+ prompt: effect ? void 0 : spec.prompt,
729
+ width: effect ? void 0 : spec.width,
730
+ height: effect ? void 0 : spec.height,
731
+ view: effect ? void 0 : spec.view,
732
+ outline: effect ? void 0 : spec.outline ?? null,
733
+ shading: effect ? void 0 : spec.shading ?? null,
734
+ detail: effect ? void 0 : spec.detail ?? null,
735
+ seed: effect ? void 0 : spec.seed ?? null,
736
+ palette: effect ? void 0 : spec.palette,
702
737
  // `noBackground` only reaches the wire for pixflux and uiAsset; the
703
738
  // tile fields are undefined for every other generator. `tileSize` is
704
739
  // intentionally absent; width/height are derived from it, so it is
@@ -773,6 +808,15 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
773
808
  styleTraits: spec.objectPro.styleTraits,
774
809
  reference: spec.objectPro.reference?.sha256
775
810
  } : void 0,
811
+ // A recipe and a local sprite enter as their contents' hashes, never
812
+ // their paths; the My effects name is not identity.
813
+ vfx: spec.vfx ? {
814
+ preset: spec.vfx.preset,
815
+ recipe: spec.vfx.recipe?.sha256,
816
+ parts: spec.vfx.parts,
817
+ fps: spec.vfx.fps,
818
+ sprite: spec.vfx.sprite ? "reference" in spec.vfx.sprite ? spec.vfx.sprite.reference : spec.vfx.sprite.sha256 : void 0
819
+ } : void 0,
776
820
  // A mirror's bytes come from its source's recorded outputs; the plan
777
821
  // compares those directly, so only the choice of source is identity.
778
822
  mirror: spec.mirror ? { of: spec.mirror.sourceAssetId } : void 0,
@@ -822,7 +866,7 @@ import path2 from "path";
822
866
  // src/types.ts
823
867
  import { z } from "zod";
824
868
  var MediaTypeSchema = z.enum(["image/png", "image/gif"]);
825
- var GeneratorSchema = z.enum(["1dir", "map", "pixflux", "tiles", "animation", "frames", "character", "terrain", "imagePro", "isometricTile", "objectPro", "uiAsset", "uiElement", "imageProFlash"]);
869
+ var GeneratorSchema = z.enum(["1dir", "map", "pixflux", "tiles", "animation", "frames", "character", "terrain", "imagePro", "isometricTile", "objectPro", "uiAsset", "uiElement", "imageProFlash", "vfx"]);
826
870
  var GridConfidenceSchema = z.enum(["low", "medium", "high"]);
827
871
  var RevisionModeSchema = z.enum([
828
872
  "image-to-image",
@@ -855,6 +899,7 @@ function candidateCount(size) {
855
899
  return 1;
856
900
  }
857
901
  function generationCost(width, height, generator = "map") {
902
+ if (generator === "vfx") return 0;
858
903
  if (generator === "map" || generator === "pixflux") return 1;
859
904
  if (generator === "imagePro") return proTierCost(width, height);
860
905
  const px = width * height;
@@ -1039,6 +1084,29 @@ var MapSceneSchema = z.object({
1039
1084
  image: z.string().min(1),
1040
1085
  placement: MapScenePlacementSchema.default({ oval: 0.3 })
1041
1086
  }).strict();
1087
+ var VfxPartSchema = z.object({ name: z.string().min(1) }).catchall(z.unknown());
1088
+ var AssetVfxSchema = z.object({
1089
+ preset: z.string().min(1).optional(),
1090
+ recipe: z.string().min(1).optional(),
1091
+ parts: z.array(VfxPartSchema).optional(),
1092
+ sprite: z.string().min(1).optional(),
1093
+ fps: z.number().int().min(1).max(60).optional(),
1094
+ name: z.string().min(1).optional()
1095
+ }).strict().superRefine((vfx, context) => {
1096
+ if (vfx.preset && vfx.recipe) {
1097
+ context.addIssue({ code: z.ZodIssueCode.custom, message: "start a vfx from a preset or a recipe, not both", path: ["recipe"] });
1098
+ }
1099
+ if (!vfx.preset && !vfx.recipe && !vfx.parts?.length) {
1100
+ context.addIssue({ code: z.ZodIssueCode.custom, message: "a vfx needs a preset, a recipe, or at least one part", path: ["preset"] });
1101
+ }
1102
+ if (vfx.sprite && /^(https?|data):/i.test(vfx.sprite)) {
1103
+ context.addIssue({
1104
+ code: z.ZodIssueCode.custom,
1105
+ message: "a vfx sprite is a manifest-relative image or a character:/object:/gallery: reference; a URL has no content to track",
1106
+ path: ["sprite"]
1107
+ });
1108
+ }
1109
+ });
1042
1110
  var CharacterOutfitSchema = z.object({
1043
1111
  /** The loop (a character animation asset) to re-clothe, in the same style. */
1044
1112
  of: z.string().min(1),
@@ -1672,7 +1740,7 @@ var StyleSchema = StyleObjectSchema.refine((s) => !(s.tileFeature && s.styleImag
1672
1740
  message: "tileFeature and styleImages cannot be combined; a connectable set derives its own tile geometry, so remove one or the other",
1673
1741
  path: ["tileFeature"]
1674
1742
  }).superRefine((style, ctx) => {
1675
- if (style.quality && (style.generator === "tiles" || style.generator === "terrain" || style.generator === "animation")) {
1743
+ if (style.quality && (style.generator === "tiles" || style.generator === "terrain" || style.generator === "animation" || style.generator === "vfx")) {
1676
1744
  ctx.addIssue({
1677
1745
  code: z.ZodIssueCode.custom,
1678
1746
  message: "quality profiles currently support single-image generators only",
@@ -1802,6 +1870,8 @@ var AssetSchema = z.object({
1802
1870
  outfit: CharacterOutfitSchema.optional(),
1803
1871
  /** `map` styles only: draw this object in the style of a scene image. See `MapSceneSchema`. */
1804
1872
  scene: MapSceneSchema.optional(),
1873
+ /** `vfx` styles only: the effect to render. See `AssetVfxSchema`. */
1874
+ vfx: AssetVfxSchema.optional(),
1805
1875
  /** `character` styles, `standard` humanoid bases: this character's proportions, over the style's. */
1806
1876
  proportions: CharacterProportionsSchema.optional(),
1807
1877
  /**
@@ -1878,7 +1948,7 @@ var AssetSchema = z.object({
1878
1948
  path: ["revision"]
1879
1949
  });
1880
1950
  }
1881
- 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);
1951
+ 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);
1882
1952
  if (shapes.length > 1) {
1883
1953
  context.addIssue({
1884
1954
  code: z.ZodIssueCode.custom,
@@ -1893,7 +1963,7 @@ var AssetSchema = z.object({
1893
1963
  path: ["mirror"]
1894
1964
  });
1895
1965
  }
1896
- if (asset.prompt === void 0 && !asset.mirror && !asset.portrait && !asset.outfit && !asset.animation?.template) {
1966
+ if (asset.prompt === void 0 && !asset.mirror && !asset.portrait && !asset.outfit && !asset.animation?.template && !asset.vfx) {
1897
1967
  context.addIssue({
1898
1968
  code: z.ZodIssueCode.custom,
1899
1969
  message: "Required",
@@ -2918,6 +2988,11 @@ function frameSetFps(entry) {
2918
2988
  }
2919
2989
  return null;
2920
2990
  }
2991
+ function frameSetLoop(entry) {
2992
+ const frameSet = entry.providerMetadata?.[entry.provider]?.frameSet;
2993
+ const loop = frameSet && typeof frameSet === "object" ? frameSet.loop : void 0;
2994
+ return typeof loop === "boolean" ? loop : null;
2995
+ }
2921
2996
 
2922
2997
  // src/palette.ts
2923
2998
  function normalizePalette(values) {
@@ -4339,6 +4414,7 @@ async function inspectRevisionReadiness(spec, lock) {
4339
4414
  if (spec.character?.styleAnchor) return inspectStyleAnchor(spec, lock, /* @__PURE__ */ new Set());
4340
4415
  if (spec.objectPro?.parentSpec) return inspectObjectProParent(spec, lock, /* @__PURE__ */ new Set());
4341
4416
  if (spec.scene) return inspectScene(spec);
4417
+ if (spec.vfx?.sprite && "file" in spec.vfx.sprite) return inspectVfxSprite(spec);
4342
4418
  if (!spec.revision) return null;
4343
4419
  return inspectRevision(spec, lock, /* @__PURE__ */ new Set());
4344
4420
  }
@@ -4366,6 +4442,12 @@ async function inspectScene(spec) {
4366
4442
  }
4367
4443
  return { ready: true, reason: "scene is current" };
4368
4444
  }
4445
+ async function inspectVfxSprite(spec) {
4446
+ const sprite = spec.vfx.sprite;
4447
+ if (!sprite.sha256 || !existsSync4(sprite.file)) return { ready: false, reason: `vfx sprite does not exist yet: ${sprite.file}` };
4448
+ if (await sha256File(sprite.file) !== sprite.sha256) return { ready: false, reason: "vfx sprite changed after the manifest was resolved" };
4449
+ return { ready: true, reason: "vfx sprite is current" };
4450
+ }
4369
4451
  async function inspectMirror(spec, lock, seen) {
4370
4452
  const source = spec.mirror.sourceSpec;
4371
4453
  const label = `mirror source ${spec.styleId}/${spec.mirror.sourceAssetId}`;
@@ -4602,7 +4684,7 @@ async function buildPlan(specs, lock, opts = {}) {
4602
4684
  } else if (opts.force) {
4603
4685
  state = "missing";
4604
4686
  reason = "--force";
4605
- } else if (!entry && spec.source && spec.generator !== "frames") {
4687
+ } else if (!entry && spec.source && spec.generator !== "frames" && spec.generator !== "vfx") {
4606
4688
  if (existsSync5(path9.resolve(spec.root, spec.source))) {
4607
4689
  state = "ok";
4608
4690
  reason = `placed from ${spec.source}; not generated`;
@@ -4842,8 +4924,8 @@ async function confirm(question, auto) {
4842
4924
  }
4843
4925
 
4844
4926
  // src/cli/commands/account.ts
4845
- import path23 from "path";
4846
- import { existsSync as existsSync16 } from "fs";
4927
+ import path24 from "path";
4928
+ import { existsSync as existsSync17 } from "fs";
4847
4929
 
4848
4930
  // src/env.ts
4849
4931
  import { readFileSync, existsSync as existsSync6 } from "fs";
@@ -5855,10 +5937,10 @@ function canonical2(value) {
5855
5937
  }
5856
5938
 
5857
5939
  // src/providers/pixellab.ts
5858
- import { mkdirSync, existsSync as existsSync7, readFileSync as readFileSync2, writeFileSync } from "fs";
5940
+ import { mkdirSync, existsSync as existsSync8, readFileSync as readFileSync2, writeFileSync } from "fs";
5859
5941
  import { randomUUID as randomUUID3 } from "crypto";
5860
5942
  import os from "os";
5861
- import path12 from "path";
5943
+ import path13 from "path";
5862
5944
 
5863
5945
  // src/client.ts
5864
5946
  import { z as z3 } from "zod";
@@ -6275,9 +6357,9 @@ var PixelLabClient = class {
6275
6357
  apiKey;
6276
6358
  http;
6277
6359
  /** Retries transport failures, 429, and 5xx; see http.ts for the policy. */
6278
- async request(path52, init) {
6360
+ async request(path53, init) {
6279
6361
  const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
6280
- const res = await this.http(`${BASE}${path52}`, {
6362
+ const res = await this.http(`${BASE}${path53}`, {
6281
6363
  ...init,
6282
6364
  headers: {
6283
6365
  Authorization: auth,
@@ -6287,13 +6369,13 @@ var PixelLabClient = class {
6287
6369
  });
6288
6370
  const text = await res.text();
6289
6371
  if (!res.ok) {
6290
- throw new PixelLabError(`${init?.method ?? "GET"} ${path52} \u2192 ${res.status}`, res.status, text);
6372
+ throw new PixelLabError(`${init?.method ?? "GET"} ${path53} \u2192 ${res.status}`, res.status, text);
6291
6373
  }
6292
6374
  if (!text) return {};
6293
6375
  try {
6294
6376
  return JSON.parse(text);
6295
6377
  } catch {
6296
- throw new Error(`${init?.method ?? "GET"} ${path52} returned invalid JSON`);
6378
+ throw new Error(`${init?.method ?? "GET"} ${path53} returned invalid JSON`);
6297
6379
  }
6298
6380
  }
6299
6381
  async balance() {
@@ -6698,10 +6780,10 @@ var PixelLabClient = class {
6698
6780
  const palette = args.paletteSwatchBase64 ? { color_image: { type: "base64", base64: args.paletteSwatchBase64, format: "png" }, force_colors: true } : {};
6699
6781
  const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
6700
6782
  const south = args.reference?.south;
6701
- let path52;
6783
+ let path53;
6702
6784
  let body;
6703
6785
  if (args.mode === "v3") {
6704
- path52 = "/create-character-v3";
6786
+ path53 = "/create-character-v3";
6705
6787
  body = {
6706
6788
  description: args.description,
6707
6789
  image_size: imageSize,
@@ -6715,7 +6797,7 @@ var PixelLabClient = class {
6715
6797
  ...args.enhancePrompt ? { enhance_prompt: true } : {}
6716
6798
  };
6717
6799
  } else if (args.mode === "pro-flash") {
6718
- path52 = "/create-character-pro-flash";
6800
+ path53 = "/create-character-pro-flash";
6719
6801
  const traits = args.styleTraits;
6720
6802
  body = {
6721
6803
  description: args.description,
@@ -6739,7 +6821,7 @@ var PixelLabClient = class {
6739
6821
  } : {}
6740
6822
  };
6741
6823
  } else if (args.mode === "pro") {
6742
- path52 = "/create-character-pro";
6824
+ path53 = "/create-character-pro";
6743
6825
  body = {
6744
6826
  description: args.description,
6745
6827
  image_size: imageSize,
@@ -6753,7 +6835,7 @@ var PixelLabClient = class {
6753
6835
  ...!south && args.styleCharacterId ? { style_character_id: args.styleCharacterId } : {}
6754
6836
  };
6755
6837
  } else {
6756
- path52 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
6838
+ path53 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
6757
6839
  body = {
6758
6840
  description: args.description,
6759
6841
  image_size: imageSize,
@@ -6770,8 +6852,8 @@ var PixelLabClient = class {
6770
6852
  ...palette
6771
6853
  };
6772
6854
  }
6773
- const raw = await this.request(path52, { method: "POST", body: JSON.stringify(body) });
6774
- return validateResponse(CharacterSubmitSchema, raw, path52.slice(1));
6855
+ const raw = await this.request(path53, { method: "POST", body: JSON.stringify(body) });
6856
+ return validateResponse(CharacterSubmitSchema, raw, path53.slice(1));
6775
6857
  }
6776
6858
  /** A text edit of an existing character, applied to every direction and stored as a sibling. */
6777
6859
  async createCharacterState(args) {
@@ -7071,8 +7153,8 @@ var PixelLabClient = class {
7071
7153
  /**
7072
7154
  * `/image-to-pixelart-pro-flash`: a photo, painting, or render in, pixel
7073
7155
  * art out, with no size to pass: PixelLab detects the native pixel scale
7074
- * and picks the output size (a 256x256 render came back 108x109, on a
7075
- * transparent background). A flat 6 generations, billed exactly that live
7156
+ * and picks the output size (a 256x256 render came back 108x109). The
7157
+ * result is opaque, on a near-white background. A flat 6 generations, billed exactly that live
7076
7158
  * in October 2026. Sources over 2048px on the longer side are scaled down
7077
7159
  * upstream. The finished job carries the image at `last_response.images[0]`.
7078
7160
  */
@@ -7443,6 +7525,337 @@ function clientFromEnv() {
7443
7525
  return new PixelLabClient(key);
7444
7526
  }
7445
7527
 
7528
+ // src/pixellab-mcp.ts
7529
+ var PIXELLAB_MCP_URL = "https://api.pixellab.ai/mcp";
7530
+ var PROTOCOL_VERSION = "2025-06-18";
7531
+ function parseMcpBody(body) {
7532
+ const trimmed = body.trim();
7533
+ if (trimmed.startsWith("{")) return JSON.parse(trimmed);
7534
+ let last;
7535
+ for (const line of body.split(/\r?\n/)) {
7536
+ if (!line.startsWith("data:")) continue;
7537
+ const payload = line.slice(5).trim();
7538
+ if (!payload) continue;
7539
+ const message6 = JSON.parse(payload);
7540
+ if ("id" in message6) last = message6;
7541
+ }
7542
+ if (!last) throw new Error(`PixelLab MCP reply had no JSON-RPC response: ${body.slice(0, 200)}`);
7543
+ return last;
7544
+ }
7545
+ function isBusyReply(text) {
7546
+ return /\bbusy\b.*\bretry\b/i.test(text);
7547
+ }
7548
+ var PixelLabMcpClient = class {
7549
+ constructor(apiKey, opts = {}) {
7550
+ this.apiKey = apiKey;
7551
+ if (!apiKey) throw new Error("PIXELLAB_API_KEY is required");
7552
+ this.url = opts.url ?? PIXELLAB_MCP_URL;
7553
+ this.http = fetchWithRetry(opts.fetch, { timeoutMs: opts.timeoutMs ?? 18e4 });
7554
+ this.busyRetries = opts.busyRetries ?? 4;
7555
+ this.sleep = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
7556
+ }
7557
+ apiKey;
7558
+ http;
7559
+ url;
7560
+ busyRetries;
7561
+ sleep;
7562
+ nextId = 1;
7563
+ /**
7564
+ * Call one tool and return its text and images. A tool-level error
7565
+ * (`isError`) throws with the tool's own message, after a few spaced
7566
+ * retries when the tool only said it was busy.
7567
+ */
7568
+ async callTool(name, args) {
7569
+ for (let attempt = 0; ; attempt++) {
7570
+ const reply = await this.callOnce(name, args);
7571
+ if (!reply.isError) return { text: reply.text, images: reply.images };
7572
+ if (attempt < this.busyRetries && isBusyReply(reply.text)) {
7573
+ await this.sleep(2e3 * 2 ** attempt);
7574
+ continue;
7575
+ }
7576
+ throw new Error(`PixelLab ${name}: ${reply.text || "the tool reported an error"}`);
7577
+ }
7578
+ }
7579
+ async callOnce(name, args) {
7580
+ const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
7581
+ const id = this.nextId++;
7582
+ const res = await this.http(this.url, {
7583
+ method: "POST",
7584
+ headers: {
7585
+ Authorization: auth,
7586
+ "Content-Type": "application/json",
7587
+ Accept: "application/json, text/event-stream",
7588
+ "MCP-Protocol-Version": PROTOCOL_VERSION
7589
+ },
7590
+ body: JSON.stringify({ jsonrpc: "2.0", id, method: "tools/call", params: { name, arguments: args } })
7591
+ });
7592
+ const body = await res.text();
7593
+ if (!res.ok) throw new PixelLabError(`MCP ${name} \u2192 ${res.status}`, res.status, body);
7594
+ const message6 = parseMcpBody(body);
7595
+ const error = message6.error;
7596
+ if (error) throw new Error(`PixelLab MCP ${name} failed: ${error.message ?? JSON.stringify(error)}`);
7597
+ const result = message6.result ?? {};
7598
+ const text = (result.content ?? []).filter((block) => block.type === "text" && typeof block.text === "string").map((block) => block.text).join("\n");
7599
+ const images = (result.content ?? []).filter((block) => block.type === "image" && typeof block.data === "string").map((block) => ({ mimeType: String(block.mimeType ?? "image/png"), data: block.data }));
7600
+ return { text, images, isError: Boolean(result.isError) };
7601
+ }
7602
+ };
7603
+ function mcpClientFromEnv() {
7604
+ const key = process.env.PIXELLAB_API_KEY;
7605
+ if (!key) {
7606
+ throw new Error(
7607
+ "PIXELLAB_API_KEY is not set.\nLooked in the environment, and in .env.local / .env beside the manifest and in the current directory."
7608
+ );
7609
+ }
7610
+ return new PixelLabMcpClient(key);
7611
+ }
7612
+
7613
+ // src/pixellab-vfx.ts
7614
+ import { existsSync as existsSync7 } from "fs";
7615
+ import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
7616
+ import path12 from "path";
7617
+ var VFX_TOOL = "vfx_workbench";
7618
+ var UUID = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
7619
+ function parseVfxStep(text) {
7620
+ const match = new RegExp(`effect step: (${UUID})`).exec(text);
7621
+ if (!match) throw new Error(`PixelLab vfx_workbench returned no step id:
7622
+ ${text.slice(0, 600)}`);
7623
+ return match[1];
7624
+ }
7625
+ function parseVfxRecipe(text) {
7626
+ const lines = text.split(/\r?\n/);
7627
+ const at = lines.findIndex((line) => line.trim() === "working recipe:");
7628
+ const json = at >= 0 ? lines[at + 1]?.trim() : void 0;
7629
+ if (!json?.startsWith("{")) return null;
7630
+ return JSON.parse(json);
7631
+ }
7632
+ function parseVfxExport(text) {
7633
+ const effectId = new RegExp(`effect id (${UUID})`).exec(text)?.[1];
7634
+ const download = /download (https:\/\/\S+\/): (.+)$/m.exec(text);
7635
+ const size = /(\d+)x(\d+), (\d+) frames @ ([\d.]+) fps, ([^;\n]+)/.exec(text);
7636
+ if (!effectId || !download || !size) {
7637
+ throw new Error(`PixelLab vfx_workbench export reply was not understood:
7638
+ ${text.slice(0, 600)}`);
7639
+ }
7640
+ const anchor = /anchor[^\n]*? at (-?\d+),\s*(-?\d+)/.exec(text);
7641
+ return {
7642
+ effectId,
7643
+ editUrl: /(https:\/\/pixellab\.ai\/create-vfx\?effect=[0-9a-f-]+)/.exec(text)?.[1] ?? null,
7644
+ width: Number(size[1]),
7645
+ height: Number(size[2]),
7646
+ frames: Number(size[3]),
7647
+ fps: Number(size[4]),
7648
+ loop: !/plays once/.test(size[5]),
7649
+ anchor: anchor ? { x: Number(anchor[1]), y: Number(anchor[2]) } : null,
7650
+ downloadBase: download[1],
7651
+ files: download[2].split(",").map((file) => file.trim()).filter(Boolean),
7652
+ imageId: new RegExp(`image id: (${UUID})`).exec(text)?.[1] ?? null
7653
+ };
7654
+ }
7655
+ async function describeVfx(client2, topic) {
7656
+ return (await client2.callTool(VFX_TOOL, { argv: ["describe", topic] })).text;
7657
+ }
7658
+ function vfxOutputPaths(out) {
7659
+ const base = path12.resolve(out.replace(/\.(png|gif|json|zip)$/i, "").replace(/\.vfx$/i, ""));
7660
+ return {
7661
+ sheet: `${base}.png`,
7662
+ gif: `${base}.gif`,
7663
+ atlas: `${base}.json`,
7664
+ provenance: `${base}.vfx.json`,
7665
+ gamePack: `${base}-game.zip`
7666
+ };
7667
+ }
7668
+ function isVfxReference(sprite) {
7669
+ return /^(character|object|gallery):/.test(sprite) || /^(https?|data):/.test(sprite);
7670
+ }
7671
+ async function resolveVfxSprite(sprite) {
7672
+ if (isVfxReference(sprite)) return sprite;
7673
+ const bytes = await readFile6(sprite);
7674
+ const metadata = imageMetadata(bytes);
7675
+ if (!metadata) throw new Error(`${sprite} is not a readable PNG or JPEG`);
7676
+ return `data:image/${metadata.format};base64,${bytes.toString("base64")}`;
7677
+ }
7678
+ async function readVfxRecipe(file) {
7679
+ const parsed = JSON.parse(await readFile6(file, "utf8"));
7680
+ const record = parsed.tool === VFX_TOOL && parsed.recipe && typeof parsed.recipe === "object";
7681
+ const recipe = record ? parsed.recipe : parsed;
7682
+ if (!Array.isArray(recipe.layers)) throw new Error(`${file} is not a VFX recipe: it has no "layers" list`);
7683
+ const recorded = record ? parsed.source?.sprite : void 0;
7684
+ if (typeof recorded !== "string") return { recipe };
7685
+ const sprite = isVfxReference(recorded) ? recorded : path12.resolve(path12.dirname(path12.resolve(file)), recorded);
7686
+ return { recipe, sprite };
7687
+ }
7688
+ async function defaultDownload(url) {
7689
+ const http = fetchWithRetry();
7690
+ const res = await http(url);
7691
+ if (!res.ok) throw new Error(`download ${url} \u2192 ${res.status}`);
7692
+ const buf = Buffer.from(await res.arrayBuffer());
7693
+ if (buf.length > MAX_DOWNLOAD_BYTES) throw new Error(`download ${url} exceeds the ${MAX_DOWNLOAD_BYTES}-byte safety limit`);
7694
+ return buf;
7695
+ }
7696
+ function localPathFor(file, paths) {
7697
+ if (/-game\.zip$/i.test(file)) return paths.gamePack;
7698
+ if (/\.gif$/i.test(file)) return paths.gif;
7699
+ if (/\.json$/i.test(file)) return paths.atlas;
7700
+ if (/\.png$/i.test(file)) return paths.sheet;
7701
+ return null;
7702
+ }
7703
+ async function runVfxWorkbench(client2, opts) {
7704
+ const starts = [opts.preset, opts.recipe, opts.effect].filter((value) => value !== void 0).length;
7705
+ if (starts > 1) throw new Error("Start a VFX from one of a preset, a recipe, or a saved effect, not several");
7706
+ if (starts === 0 && !opts.parts?.length) {
7707
+ throw new Error("A VFX needs something to start from: a preset, a recipe, a saved effect, or at least one part");
7708
+ }
7709
+ const progress = opts.onProgress ?? (() => {
7710
+ });
7711
+ const call = async (argv) => (await client2.callTool(VFX_TOOL, { argv, ...opts.notes ? { notes: opts.notes } : {} })).text;
7712
+ const startArgv = ["start"];
7713
+ if (opts.sprite) startArgv.push("--sprite", opts.sprite);
7714
+ if (opts.preset) startArgv.push("--preset", opts.preset);
7715
+ if (opts.effect) startArgv.push("--effect", opts.effect);
7716
+ progress(opts.preset ? `starting from preset ${opts.preset}` : opts.effect ? `starting from saved effect ${opts.effect}` : "starting");
7717
+ const started = await call(startArgv);
7718
+ let step = parseVfxStep(started);
7719
+ const savedName = opts.effect ? parseVfxRecipe(started)?.name : opts.recipe?.name;
7720
+ const name = opts.name ?? (typeof savedName === "string" ? savedName : void 0) ?? opts.defaultName;
7721
+ if (opts.recipe) {
7722
+ progress("rendering the recipe");
7723
+ step = parseVfxStep(await call([step, "render", JSON.stringify({ recipe: opts.recipe })]));
7724
+ }
7725
+ if (opts.parts?.length) {
7726
+ progress(`adding ${opts.parts.map((part) => part.name).join(", ")}`);
7727
+ step = parseVfxStep(await call([step, "add_part", JSON.stringify({ parts: opts.parts })]));
7728
+ }
7729
+ if (opts.fps !== void 0) {
7730
+ progress(`setting ${opts.fps} fps`);
7731
+ step = parseVfxStep(await call([step, "render", JSON.stringify({ patch: [{ op: "set", path: "fps", value: opts.fps }] })]));
7732
+ }
7733
+ const exportArgv = [step, "export", "--name", name];
7734
+ if (opts.replace) exportArgv.push("--replace", opts.replace);
7735
+ if (opts.gamePack) exportArgv.push("--game-pack");
7736
+ progress(opts.gamePack ? "exporting with a game pack" : "exporting");
7737
+ const exported = parseVfxExport(await call(exportArgv));
7738
+ await opts.onExported?.(exported, name);
7739
+ progress("reading the saved recipe");
7740
+ const recipe = parseVfxRecipe(await call(["start", "--effect", exported.effectId]));
7741
+ return { exported, recipe, name };
7742
+ }
7743
+ async function readSavedVfxRecipe(client2, effectId) {
7744
+ try {
7745
+ return parseVfxRecipe((await client2.callTool(VFX_TOOL, { argv: ["start", "--effect", effectId] })).text);
7746
+ } catch (error) {
7747
+ if (error instanceof Error && /no saved effect/i.test(error.message)) return null;
7748
+ throw error;
7749
+ }
7750
+ }
7751
+ function vfxRecipeSha256(recipe) {
7752
+ const { name: _name, notes: _notes, ...rest } = recipe;
7753
+ return sha256(JSON.stringify(rest));
7754
+ }
7755
+ function sliceVfxSheet(sheet, facts, atlas) {
7756
+ const image = decodePng(sheet);
7757
+ const listed = atlas?.frames;
7758
+ const rects = Array.isArray(listed) && listed.length === facts.frames ? listed.map((entry) => entry.frame) : Array.from({ length: facts.frames }, (_, index) => {
7759
+ const columns = Math.max(1, Math.floor(image.width / facts.width));
7760
+ return { x: index % columns * facts.width, y: Math.floor(index / columns) * facts.height, w: facts.width, h: facts.height };
7761
+ });
7762
+ return rects.map((rect, index) => {
7763
+ if (rect.x < 0 || rect.y < 0 || rect.x + rect.w > image.width || rect.y + rect.h > image.height) {
7764
+ throw new Error(
7765
+ `VFX frame ${index + 1} (${rect.w}x${rect.h} at ${rect.x},${rect.y}) falls outside the ${image.width}x${image.height} sheet`
7766
+ );
7767
+ }
7768
+ const out = Buffer.alloc(rect.w * rect.h * 4);
7769
+ for (let y = 0; y < rect.h; y++) {
7770
+ const from = ((rect.y + y) * image.width + rect.x) * 4;
7771
+ image.pixels.copy(out, y * rect.w * 4, from, from + rect.w * 4);
7772
+ }
7773
+ return encodeRgbaPng(rect.w, rect.h, out);
7774
+ });
7775
+ }
7776
+ async function makeVfx(client2, opts) {
7777
+ const starts = [opts.preset, opts.recipe, opts.effect].filter((value) => value !== void 0).length;
7778
+ if (starts > 1) throw new Error("Start a VFX from one of a preset, a recipe, or a saved effect, not several");
7779
+ if (starts === 0 && !opts.parts?.length) {
7780
+ throw new Error("A VFX needs something to start from: a preset, a recipe, a saved effect, or at least one part");
7781
+ }
7782
+ const paths = vfxOutputPaths(opts.out);
7783
+ const wanted = [paths.sheet, paths.gif, paths.atlas, paths.provenance, ...opts.gamePack ? [paths.gamePack] : []];
7784
+ if (!opts.force) {
7785
+ for (const file of wanted) {
7786
+ if (existsSync7(file)) throw new Error(`${file} already exists; pass --force to replace it`);
7787
+ }
7788
+ }
7789
+ const progress = opts.onProgress ?? (() => {
7790
+ });
7791
+ const { exported, recipe, name } = await runVfxWorkbench(client2, {
7792
+ preset: opts.preset,
7793
+ recipe: opts.recipe,
7794
+ effect: opts.effect,
7795
+ parts: opts.parts,
7796
+ sprite: opts.sprite ? await resolveVfxSprite(opts.sprite) : void 0,
7797
+ fps: opts.fps,
7798
+ name: opts.name,
7799
+ defaultName: opts.preset ?? path12.basename(paths.sheet, ".png"),
7800
+ replace: opts.replace ?? opts.effect,
7801
+ gamePack: opts.gamePack,
7802
+ notes: "pixelkiln vfx",
7803
+ onProgress: progress
7804
+ });
7805
+ const download = opts.download ?? defaultDownload;
7806
+ const written = { sheet: paths.sheet, gif: paths.gif, atlas: paths.atlas, provenance: paths.provenance };
7807
+ await mkdir4(path12.dirname(paths.sheet), { recursive: true });
7808
+ for (const file of exported.files) {
7809
+ const target = localPathFor(file, paths);
7810
+ if (!target) continue;
7811
+ let bytes = await download(`${exported.downloadBase}${encodeURIComponent(file)}`);
7812
+ if (target === paths.atlas) {
7813
+ const atlas = JSON.parse(bytes.toString("utf8"));
7814
+ if (atlas.meta) atlas.meta.image = path12.basename(paths.sheet);
7815
+ bytes = Buffer.from(`${JSON.stringify(atlas, null, 2)}
7816
+ `);
7817
+ }
7818
+ await writeFile4(target, bytes);
7819
+ if (target === paths.gamePack) written.gamePack = target;
7820
+ }
7821
+ for (const file of [paths.sheet, paths.gif, paths.atlas]) {
7822
+ if (!existsSync7(file)) throw new Error(`PixelLab's export listed no file for ${path12.basename(file)}: ${exported.files.join(", ")}`);
7823
+ }
7824
+ const spriteRecord = !opts.sprite || /^data:/.test(opts.sprite) ? void 0 : isVfxReference(opts.sprite) ? opts.sprite : path12.relative(path12.dirname(paths.provenance), path12.resolve(opts.sprite)).split(path12.sep).join("/");
7825
+ const provenance = {
7826
+ version: 1,
7827
+ provider: "pixellab",
7828
+ tool: VFX_TOOL,
7829
+ name,
7830
+ effectId: exported.effectId,
7831
+ editUrl: exported.editUrl,
7832
+ source: {
7833
+ ...opts.preset ? { preset: opts.preset } : {},
7834
+ ...opts.effect ? { effect: opts.effect } : {},
7835
+ ...opts.recipe ? { recipeFile: true } : {},
7836
+ ...opts.parts?.length ? { parts: opts.parts } : {},
7837
+ ...spriteRecord ? { sprite: spriteRecord } : {}
7838
+ },
7839
+ width: exported.width,
7840
+ height: exported.height,
7841
+ frames: exported.frames,
7842
+ fps: exported.fps,
7843
+ loop: exported.loop,
7844
+ anchor: exported.anchor,
7845
+ files: {
7846
+ sheet: path12.basename(paths.sheet),
7847
+ gif: path12.basename(paths.gif),
7848
+ atlas: path12.basename(paths.atlas),
7849
+ ...written.gamePack ? { gamePack: path12.basename(written.gamePack) } : {}
7850
+ },
7851
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
7852
+ recipe
7853
+ };
7854
+ await writeFile4(paths.provenance, `${JSON.stringify(provenance, null, 2)}
7855
+ `);
7856
+ return { ...exported, paths: written, recipe };
7857
+ }
7858
+
7446
7859
  // src/providers/pixellab.ts
7447
7860
  function pixelLabObjectUrl(generator, objectId) {
7448
7861
  if (!objectId) return null;
@@ -7457,6 +7870,7 @@ function pixelLabObjectUrl(generator, objectId) {
7457
7870
  const characterId = objectId.split("#")[0];
7458
7871
  return characterId ? `https://www.pixellab.ai/create-character/${encodeURIComponent(characterId)}` : null;
7459
7872
  }
7873
+ if (generator === "vfx") return `https://pixellab.ai/create-vfx?effect=${encodeURIComponent(objectId)}`;
7460
7874
  return null;
7461
7875
  }
7462
7876
  function billedFromUsage(usage) {
@@ -7632,13 +8046,20 @@ function decodeObjectAnimationJob(jobId) {
7632
8046
  return null;
7633
8047
  }
7634
8048
  var PixelLabProvider = class _PixelLabProvider {
7635
- constructor(client2) {
8049
+ /**
8050
+ * `mcp` reaches the tools PixelLab offers only over MCP (the VFX
8051
+ * workbench). Offline and download-only providers have none, and a `vfx`
8052
+ * submission through one says so.
8053
+ */
8054
+ constructor(client2, mcp) {
7636
8055
  this.client = client2;
8056
+ this.mcp = mcp;
7637
8057
  }
7638
8058
  client;
8059
+ mcp;
7639
8060
  id = "pixellab";
7640
8061
  static fromEnv() {
7641
- return new _PixelLabProvider(clientFromEnv());
8062
+ return new _PixelLabProvider(clientFromEnv(), mcpClientFromEnv());
7642
8063
  }
7643
8064
  /** Public storage URLs and the local content cache do not require API auth. */
7644
8065
  static forDownloads() {
@@ -7649,7 +8070,7 @@ var PixelLabProvider = class _PixelLabProvider {
7649
8070
  return new _PixelLabProvider(new PixelLabClient("download-only"));
7650
8071
  }
7651
8072
  supports(generator) {
7652
- 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";
8073
+ 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";
7653
8074
  }
7654
8075
  /** `/map-objects` style matching: `background_image` plus `inpainting`. */
7655
8076
  supportsMapScene = true;
@@ -7691,11 +8112,12 @@ var PixelLabProvider = class _PixelLabProvider {
7691
8112
  * is a file read.
7692
8113
  */
7693
8114
  static cacheDir(namespace = "pixflux") {
7694
- const dir = path12.join(os.tmpdir(), `pixelkiln-${namespace}`);
8115
+ const dir = path13.join(os.tmpdir(), `pixelkiln-${namespace}`);
7695
8116
  mkdirSync(dir, { recursive: true });
7696
8117
  return dir;
7697
8118
  }
7698
8119
  estimate(spec) {
8120
+ if (spec.vfx) return { unit: "generations", amount: 0, candidates: 1 };
7699
8121
  if (spec.batch?.role === "member") {
7700
8122
  return { unit: "generations", amount: 0, candidates: 1 };
7701
8123
  }
@@ -7753,6 +8175,9 @@ var PixelLabProvider = class _PixelLabProvider {
7753
8175
  };
7754
8176
  }
7755
8177
  validate(spec, styleImages) {
8178
+ if (spec.generator === "vfx" && styleImages.length) {
8179
+ throw new Error("PixelLab vfx takes no style images; an effect's look comes from its preset, parts, or recipe");
8180
+ }
7756
8181
  const members = spec.revision?.sourceMembers;
7757
8182
  if (spec.revision && members && !MEMBER_SET_REVISION_MODES.includes(spec.revision.mode)) {
7758
8183
  throw new Error(
@@ -8037,7 +8462,7 @@ var PixelLabProvider = class _PixelLabProvider {
8037
8462
  }
8038
8463
  }
8039
8464
  if (endFrame) {
8040
- const start = startFrame ?? (character.parentFile && existsSync7(character.parentFile) ? imageMetadata(readFileSync2(character.parentFile)) : null);
8465
+ const start = startFrame ?? (character.parentFile && existsSync8(character.parentFile) ? imageMetadata(readFileSync2(character.parentFile)) : null);
8041
8466
  if (start && (start.width !== endFrame.width || start.height !== endFrame.height)) {
8042
8467
  throw new Error(
8043
8468
  `${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`
@@ -8195,7 +8620,7 @@ var PixelLabProvider = class _PixelLabProvider {
8195
8620
  }
8196
8621
  }
8197
8622
  if (endFrame) {
8198
- const start = startFrame ?? (object.parentFile && existsSync7(object.parentFile) ? imageMetadata(readFileSync2(object.parentFile)) : null);
8623
+ const start = startFrame ?? (object.parentFile && existsSync8(object.parentFile) ? imageMetadata(readFileSync2(object.parentFile)) : null);
8199
8624
  if (start && (start.width !== endFrame.width || start.height !== endFrame.height)) {
8200
8625
  throw new Error(
8201
8626
  `${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`
@@ -8505,7 +8930,7 @@ var PixelLabProvider = class _PixelLabProvider {
8505
8930
  const sources = images.map((image, index) => {
8506
8931
  if (!image.base64) throw new Error(`outfit transfer frame ${index} has no image data`);
8507
8932
  const role = meta?.frameRoles?.[index] ?? void 0;
8508
- const file = path12.join(dir, `${jobId}-${index}.png`);
8933
+ const file = path13.join(dir, `${jobId}-${index}.png`);
8509
8934
  writeFileSync(file, Buffer.from(image.base64, "base64"));
8510
8935
  return { url: `file://${file}`, ...role ? { role } : {} };
8511
8936
  });
@@ -8657,6 +9082,7 @@ var PixelLabProvider = class _PixelLabProvider {
8657
9082
  async submit(spec, styleImages, context) {
8658
9083
  this.validate(spec, styleImages);
8659
9084
  if (spec.revision) return this.submitRevision(spec);
9085
+ if (spec.vfx) return this.submitVfx(spec, context);
8660
9086
  if (spec.generator === "character") return this.submitCharacter(spec, styleImages, context);
8661
9087
  if (spec.generator === "objectPro") return this.submitObjectPro(spec, styleImages, context);
8662
9088
  if (spec.generator === "uiAsset") {
@@ -8710,7 +9136,7 @@ var PixelLabProvider = class _PixelLabProvider {
8710
9136
  seed: spec.seed
8711
9137
  });
8712
9138
  const jobId = randomUUID3();
8713
- writeFileSync(path12.join(_PixelLabProvider.cacheDir(), `${jobId}.png`), png);
9139
+ writeFileSync(path13.join(_PixelLabProvider.cacheDir(), `${jobId}.png`), png);
8714
9140
  return { jobId };
8715
9141
  }
8716
9142
  if (spec.generator === "tiles") {
@@ -9039,11 +9465,11 @@ var PixelLabProvider = class _PixelLabProvider {
9039
9465
  cacheCleanupResult(pngs, usage, roles, fps) {
9040
9466
  const jobId = randomUUID3();
9041
9467
  if (!roles) {
9042
- writeFileSync(path12.join(_PixelLabProvider.cacheDir(), `${jobId}.png`), pngs[0]);
9468
+ writeFileSync(path13.join(_PixelLabProvider.cacheDir(), `${jobId}.png`), pngs[0]);
9043
9469
  return { jobId, metadata: { revisionUsage: usage } };
9044
9470
  }
9045
9471
  for (const [index, png] of pngs.entries()) {
9046
- writeFileSync(path12.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`), png);
9472
+ writeFileSync(path13.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`), png);
9047
9473
  }
9048
9474
  return { jobId, metadata: { revisionUsage: usage, revisionMembers: roles, ...fps != null ? { revisionFps: fps } : {} } };
9049
9475
  }
@@ -9071,10 +9497,11 @@ var PixelLabProvider = class _PixelLabProvider {
9071
9497
  return this.pollAnimateRevision(jobId, context);
9072
9498
  }
9073
9499
  if (context?.spec?.revision) return this.pollRevision(jobId);
9500
+ if (generator === "vfx") return this.pollVfx(jobId, context);
9074
9501
  if (generator === "imageProFlash") return this.pollRevision(jobId);
9075
9502
  if (generator === "pixflux") {
9076
- const file = path12.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
9077
- if (existsSync7(file)) {
9503
+ const file = path13.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
9504
+ if (existsSync8(file)) {
9078
9505
  const sourceUrl = `file://${file}`;
9079
9506
  return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }] };
9080
9507
  }
@@ -9117,6 +9544,120 @@ var PixelLabProvider = class _PixelLabProvider {
9117
9544
  * background job to ask afterwards the way `billedForJob` does for async
9118
9545
  * work.
9119
9546
  */
9547
+ /**
9548
+ * Render a `vfx` asset with the VFX workbench, which is synchronous and
9549
+ * free. The frames are cut from the exported sheet and parked in the
9550
+ * cache like pixflux's bytes; the export's download folder is recorded so
9551
+ * `poll` can cut them again if the cache is cleared.
9552
+ *
9553
+ * A regeneration exports over the effect this asset rendered last time, so
9554
+ * My effects keeps one entry per asset, unless that effect has changed on
9555
+ * pixellab.ai since (someone moved its sliders) or is gone. Then a new
9556
+ * effect is saved and the edited one is left alone.
9557
+ */
9558
+ async submitVfx(spec, context) {
9559
+ const vfx = spec.vfx;
9560
+ if (!this.mcp) throw new Error("PixelLab vfx renders over PixelLab's MCP server, which this provider was built without");
9561
+ let sprite;
9562
+ if (vfx.sprite && "reference" in vfx.sprite) {
9563
+ sprite = vfx.sprite.reference;
9564
+ } else if (vfx.sprite) {
9565
+ const { file } = vfx.sprite;
9566
+ if (!existsSync8(file)) throw new Error(`vfx sprite does not exist yet: ${file}`);
9567
+ const bytes = readFileSync2(file);
9568
+ if (sha256(bytes) !== vfx.sprite.sha256) {
9569
+ throw new Error(`vfx sprite changed after the manifest was resolved: ${file}; run the command again`);
9570
+ }
9571
+ const format = imageMetadata(bytes)?.format ?? "png";
9572
+ sprite = `data:image/${format};base64,${bytes.toString("base64")}`;
9573
+ }
9574
+ const previous = context?.replacedMetadata?.vfx ?? context?.lastGenerationMetadata?.vfx;
9575
+ let replace;
9576
+ let newEffect;
9577
+ if (previous?.effectId) {
9578
+ const upstream = await readSavedVfxRecipe(this.mcp, previous.effectId);
9579
+ if (!upstream) newEffect = `effect ${previous.effectId} is no longer in My effects`;
9580
+ else if (!previous.recipe || vfxRecipeSha256(upstream) !== vfxRecipeSha256(previous.recipe)) {
9581
+ newEffect = `effect ${previous.effectId} changed on pixellab.ai since it was rendered; it was kept`;
9582
+ } else replace = previous.effectId;
9583
+ }
9584
+ const jobId = randomUUID3();
9585
+ const facts = (exported2, name2) => ({
9586
+ effectId: exported2.effectId,
9587
+ editUrl: exported2.editUrl,
9588
+ name: name2,
9589
+ imageId: exported2.imageId,
9590
+ width: exported2.width,
9591
+ height: exported2.height,
9592
+ frames: exported2.frames,
9593
+ fps: exported2.fps,
9594
+ loop: exported2.loop,
9595
+ anchor: exported2.anchor,
9596
+ downloadBase: exported2.downloadBase,
9597
+ files: exported2.files,
9598
+ replaced: replace ?? null,
9599
+ ...newEffect ? { newEffect } : {}
9600
+ });
9601
+ const { exported, recipe, name } = await runVfxWorkbench(this.mcp, {
9602
+ preset: vfx.preset,
9603
+ recipe: vfx.recipe?.recipe,
9604
+ parts: vfx.parts,
9605
+ sprite,
9606
+ fps: vfx.fps,
9607
+ name: vfx.name,
9608
+ defaultName: vfx.name,
9609
+ replace,
9610
+ notes: `pixelkiln ${spec.styleId}/${spec.assetId}`,
9611
+ // The effect exists from here on; record it before anything else can fail.
9612
+ onExported: (done, effectName) => context?.checkpoint({ jobId, complete: true, metadata: { vfx: facts(done, effectName) } })
9613
+ });
9614
+ await this.cacheVfxFrames(jobId, exported);
9615
+ return { jobId, metadata: { vfx: { ...facts(exported, name), recipe } } };
9616
+ }
9617
+ /** Cut an export's sheet into frames, with a sidecar `poll` reads without any lock metadata. */
9618
+ async cacheVfxFrames(jobId, exported) {
9619
+ const sheetFile = exported.files.find((file) => /\.png$/i.test(file));
9620
+ const atlasFile = exported.files.find((file) => /\.json$/i.test(file));
9621
+ if (!sheetFile) throw new Error(`PixelLab's vfx export listed no spritesheet: ${exported.files.join(", ")}`);
9622
+ const sheet = await this.client.download(`${exported.downloadBase}${encodeURIComponent(sheetFile)}`);
9623
+ const atlas = atlasFile ? JSON.parse((await this.client.download(`${exported.downloadBase}${encodeURIComponent(atlasFile)}`)).toString("utf8")) : void 0;
9624
+ const frames = sliceVfxSheet(sheet, exported, atlas);
9625
+ const dir = _PixelLabProvider.cacheDir("vfx");
9626
+ frames.forEach((frame, index) => writeFileSync(path13.join(dir, `${jobId}-${index}.png`), frame));
9627
+ writeFileSync(
9628
+ path13.join(dir, `${jobId}.json`),
9629
+ JSON.stringify({ effectId: exported.effectId, count: frames.length, fps: exported.fps, loop: exported.loop })
9630
+ );
9631
+ }
9632
+ async pollVfx(jobId, context) {
9633
+ const dir = _PixelLabProvider.cacheDir("vfx");
9634
+ const sidecar = path13.join(dir, `${jobId}.json`);
9635
+ const cached = () => {
9636
+ if (!existsSync8(sidecar)) return null;
9637
+ const facts2 = JSON.parse(readFileSync2(sidecar, "utf8"));
9638
+ const files2 = Array.from({ length: facts2.count }, (_, index) => path13.join(dir, `${jobId}-${index}.png`));
9639
+ return files2.every((file) => existsSync8(file)) ? { facts: facts2, files: files2 } : null;
9640
+ };
9641
+ let found = cached();
9642
+ const recorded = context?.metadata?.vfx;
9643
+ if (!found && recorded?.downloadBase && Array.isArray(recorded.files)) {
9644
+ await this.cacheVfxFrames(jobId, recorded);
9645
+ found = cached();
9646
+ }
9647
+ if (!found) {
9648
+ return { status: "failed", error: "vfx frames are no longer cached locally; re-run submit for this asset (free)" };
9649
+ }
9650
+ const { facts, files } = found;
9651
+ const sources = files.length === 1 ? [{ url: `file://${files[0]}` }] : files.map((file, index) => ({ url: `file://${file}`, role: `frame-${String(index).padStart(2, "0")}` }));
9652
+ return {
9653
+ status: "ready",
9654
+ objectId: facts.effectId,
9655
+ sourceUrl: sources[0].url,
9656
+ sources,
9657
+ ...files.length > 1 ? { metadata: { frameSet: { fps: facts.fps, count: files.length, loop: facts.loop } } } : {},
9658
+ billed: null
9659
+ };
9660
+ }
9120
9661
  async pollCachedRevision(jobId, context) {
9121
9662
  const usage = context?.metadata?.revisionUsage;
9122
9663
  const roles = context?.metadata?.revisionMembers;
@@ -9126,10 +9667,10 @@ var PixelLabProvider = class _PixelLabProvider {
9126
9667
  };
9127
9668
  if (Array.isArray(roles) && roles.length) {
9128
9669
  const sources = roles.map((role, index) => ({
9129
- url: `file://${path12.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`)}`,
9670
+ url: `file://${path13.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`)}`,
9130
9671
  role: String(role)
9131
9672
  }));
9132
- if (sources.some((source) => !existsSync7(source.url.slice("file://".length)))) return gone;
9673
+ if (sources.some((source) => !existsSync8(source.url.slice("file://".length)))) return gone;
9133
9674
  const fps = context?.metadata?.revisionFps;
9134
9675
  const frames = sources.every((source) => source.role.startsWith("frame-"));
9135
9676
  return {
@@ -9141,8 +9682,8 @@ var PixelLabProvider = class _PixelLabProvider {
9141
9682
  billed: billedFromUsage(usage)
9142
9683
  };
9143
9684
  }
9144
- const file = path12.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
9145
- if (!existsSync7(file)) return gone;
9685
+ const file = path13.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
9686
+ if (!existsSync8(file)) return gone;
9146
9687
  const sourceUrl = `file://${file}`;
9147
9688
  return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }], billed: billedFromUsage(usage) };
9148
9689
  }
@@ -9198,7 +9739,7 @@ var PixelLabProvider = class _PixelLabProvider {
9198
9739
  }
9199
9740
  const base64 = extractBase64(item);
9200
9741
  if (base64) {
9201
- const file = path12.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`);
9742
+ const file = path13.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`);
9202
9743
  writeFileSync(file, Buffer.from(base64, "base64"));
9203
9744
  urls.push(`file://${file}`);
9204
9745
  continue;
@@ -9249,7 +9790,7 @@ var PixelLabProvider = class _PixelLabProvider {
9249
9790
  const candidate = done[key];
9250
9791
  const base64 = extractBase64(candidate);
9251
9792
  if (base64) {
9252
- const file = path12.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
9793
+ const file = path13.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
9253
9794
  writeFileSync(file, Buffer.from(base64, "base64"));
9254
9795
  const sourceUrl = `file://${file}`;
9255
9796
  return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }], billed };
@@ -9258,7 +9799,7 @@ var PixelLabProvider = class _PixelLabProvider {
9258
9799
  if (Array.isArray(done.images) && done.images.length) {
9259
9800
  const base64 = extractBase64(done.images[0]);
9260
9801
  if (base64) {
9261
- const file = path12.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
9802
+ const file = path13.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
9262
9803
  writeFileSync(file, Buffer.from(base64, "base64"));
9263
9804
  const sourceUrl = `file://${file}`;
9264
9805
  return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }], billed };
@@ -9292,7 +9833,7 @@ var PixelLabProvider = class _PixelLabProvider {
9292
9833
  for (const [index, candidate] of done.images.entries()) {
9293
9834
  const base64 = extractBase64(candidate);
9294
9835
  if (!base64) continue;
9295
- const file = path12.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`);
9836
+ const file = path13.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`);
9296
9837
  writeFileSync(file, Buffer.from(base64, "base64"));
9297
9838
  urls.push(`file://${file}`);
9298
9839
  }
@@ -9322,7 +9863,7 @@ var PixelLabProvider = class _PixelLabProvider {
9322
9863
  for (const [index, candidate] of images.entries()) {
9323
9864
  const base64 = extractBase64(candidate);
9324
9865
  if (!base64) continue;
9325
- const file = path12.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`);
9866
+ const file = path13.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`);
9326
9867
  writeFileSync(file, Buffer.from(base64, "base64"));
9327
9868
  urls.push(`file://${file}`);
9328
9869
  }
@@ -9420,7 +9961,7 @@ var PixelLabProvider = class _PixelLabProvider {
9420
9961
  for (const [index, tile] of tileset.tiles.entries()) {
9421
9962
  const slug2 = tile.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "tile";
9422
9963
  const role = `tile-${String(index).padStart(2, "0")}-${slug2}`;
9423
- const file = path12.join(_PixelLabProvider.cacheDir(), `${tilesetId}-${index}.png`);
9964
+ const file = path13.join(_PixelLabProvider.cacheDir(), `${tilesetId}-${index}.png`);
9424
9965
  writeFileSync(file, Buffer.from(tile.image.base64, "base64"));
9425
9966
  sources.push({ url: `file://${file}`, role });
9426
9967
  terrainTiles.push({ role, corners: tile.corners, pattern4x4: tile.pattern_4x4 });
@@ -9450,7 +9991,7 @@ var PixelLabProvider = class _PixelLabProvider {
9450
9991
  const { image, usage } = await this.client.getIsometricTile(tileId);
9451
9992
  const backgroundJobId = context?.metadata?.backgroundJobId;
9452
9993
  const billed = billedFromUsage(usage) ?? await this.billedForJob(backgroundJobId);
9453
- const file = path12.join(_PixelLabProvider.cacheDir(), `${tileId}.png`);
9994
+ const file = path13.join(_PixelLabProvider.cacheDir(), `${tileId}.png`);
9454
9995
  writeFileSync(file, Buffer.from(image.base64, "base64"));
9455
9996
  return {
9456
9997
  status: "ready",
@@ -9494,8 +10035,8 @@ var PixelLabProvider = class _PixelLabProvider {
9494
10035
  return { objectId: `${jobId}#${index}`, sourceUrl: url };
9495
10036
  }
9496
10037
  if (generator === "imagePro" || generator === "uiElement") {
9497
- const file = path12.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`);
9498
- if (!existsSync7(file)) throw new Error(`${generator} job ${jobId} has no cached candidate at index ${index}`);
10038
+ const file = path13.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`);
10039
+ if (!existsSync8(file)) throw new Error(`${generator} job ${jobId} has no cached candidate at index ${index}`);
9499
10040
  return { objectId: `${jobId}#${index}`, sourceUrl: `file://${file}` };
9500
10041
  }
9501
10042
  const promoted = await this.client.selectFrames(jobId, [index], commonTag);
@@ -9772,9 +10313,9 @@ var RetroDiffusionClient = class {
9772
10313
  if (!response.ok) throw new ProviderError("retrodiffusion", `Retro Diffusion download failed (${response.status})`, { status: response.status });
9773
10314
  return Buffer.from(await response.arrayBuffer());
9774
10315
  }
9775
- async call(path52, init = {}) {
10316
+ async call(path53, init = {}) {
9776
10317
  if (!this.token) throw new Error("RD_API_KEY is not set");
9777
- const response = await this.request(`${this.baseUrl}${path52}`, {
10318
+ const response = await this.request(`${this.baseUrl}${path53}`, {
9778
10319
  ...init,
9779
10320
  headers: {
9780
10321
  "Content-Type": "application/json",
@@ -10086,7 +10627,7 @@ function retroError(value) {
10086
10627
  import { createHash as createHash3 } from "crypto";
10087
10628
  import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, renameSync, writeFileSync as writeFileSync2 } from "fs";
10088
10629
  import os2 from "os";
10089
- import path13 from "path";
10630
+ import path14 from "path";
10090
10631
  var DEFAULT_BASE_URL3 = "https://api.cloud.scenario.com/v1";
10091
10632
  var SOURCE_PROTOCOL2 = "scenario:";
10092
10633
  var SUBMIT_RETRIES = 8;
@@ -10469,7 +11010,7 @@ var ScenarioProvider = class _ScenarioProvider {
10469
11010
  }
10470
11011
  };
10471
11012
  function uploadCachePath() {
10472
- return process.env.SCENARIO_UPLOAD_CACHE ?? path13.join(os2.tmpdir(), "pixelkiln-scenario", "uploads.json");
11013
+ return process.env.SCENARIO_UPLOAD_CACHE ?? path14.join(os2.tmpdir(), "pixelkiln-scenario", "uploads.json");
10473
11014
  }
10474
11015
  function uploadCacheKey(baseUrl, projectId, digest2) {
10475
11016
  return createHash3("sha256").update(`${baseUrl}
@@ -10487,7 +11028,7 @@ function readUploadCache() {
10487
11028
  function writeUploadCache(key, assetId) {
10488
11029
  try {
10489
11030
  const file = uploadCachePath();
10490
- mkdirSync2(path13.dirname(file), { recursive: true });
11031
+ mkdirSync2(path14.dirname(file), { recursive: true });
10491
11032
  const tmp = `${file}.${process.pid}.tmp`;
10492
11033
  writeFileSync2(tmp, JSON.stringify({ ...readUploadCache(), [key]: assetId }));
10493
11034
  renameSync(tmp, file);
@@ -10747,9 +11288,9 @@ registerProvider({
10747
11288
  });
10748
11289
 
10749
11290
  // src/manifest.ts
10750
- import { readFile as readFile6 } from "fs/promises";
10751
- import { existsSync as existsSync8 } from "fs";
10752
- import path14 from "path";
11291
+ import { readFile as readFile7 } from "fs/promises";
11292
+ import { existsSync as existsSync9 } from "fs";
11293
+ import path15 from "path";
10753
11294
 
10754
11295
  // src/loop-directions.ts
10755
11296
  var SINGLE_OUTPUT_FIELDS = ["file", "cell", "remoteId", "source", "sourceByStyle", "outputRole"];
@@ -10838,9 +11379,9 @@ function isObject2(value) {
10838
11379
 
10839
11380
  // src/manifest.ts
10840
11381
  async function loadManifest(manifestPath) {
10841
- const abs = path14.resolve(manifestPath);
10842
- if (!existsSync8(abs)) throw new ProjectError(`No manifest at ${abs}`);
10843
- const expansion = expandLoopDirections(JSON.parse(await readFile6(abs, "utf8")));
11382
+ const abs = path15.resolve(manifestPath);
11383
+ if (!existsSync9(abs)) throw new ProjectError(`No manifest at ${abs}`);
11384
+ const expansion = expandLoopDirections(JSON.parse(await readFile7(abs, "utf8")));
10844
11385
  if (expansion.issues.length) {
10845
11386
  throw new ProjectError(`Manifest at ${abs} is invalid:
10846
11387
  ${expansion.issues.map((i) => ` ${i}`).join("\n")}`);
@@ -10895,7 +11436,7 @@ ${issues}`);
10895
11436
  if (fromStyle) {
10896
11437
  if (!styleIds.has(fromStyle)) {
10897
11438
  unknownReferences.push(`assets.${assetId}.revision.fromStyle: unknown style "${fromStyle}"`);
10898
- } else if (parent && parent.styles.length && !parent.styles.includes(fromStyle)) {
11439
+ } else if (parent && !assetInStyle(parent, fromStyle, parsed.data.styles[fromStyle])) {
10899
11440
  unknownReferences.push(
10900
11441
  `assets.${assetId}.revision.fromStyle: "${asset.revision.from}" is not in style "${fromStyle}"`
10901
11442
  );
@@ -10942,7 +11483,11 @@ ${issues}`);
10942
11483
  ${unknownReferences.map((i) => ` ${i}`).join("\n")}`);
10943
11484
  }
10944
11485
  const loopFamilies = Object.keys(expansion.families).length ? { loopFamilies: expansion.families } : {};
10945
- return { manifest: parsed.data, root: path14.dirname(abs), path: abs, ...loopFamilies };
11486
+ return { manifest: parsed.data, root: path15.dirname(abs), path: abs, ...loopFamilies };
11487
+ }
11488
+ function assetInStyle(asset, styleId, style) {
11489
+ if (asset.styles.length) return asset.styles.includes(styleId);
11490
+ return style?.generator === "vfx" === Boolean(asset.vfx);
10946
11491
  }
10947
11492
  function parentAssetId(asset) {
10948
11493
  return asset?.revision?.from ?? asset?.state?.of ?? asset?.animation?.of ?? asset?.portrait?.of ?? asset?.outfit?.of;
@@ -11046,8 +11591,8 @@ async function resolveSpecs(loaded, filter) {
11046
11591
  const needed = /* @__PURE__ */ new Set();
11047
11592
  for (const assetId of resolutionAssetIds) {
11048
11593
  const asset = manifest.assets[assetId];
11049
- if (asset?.styles.length && !asset.styles.includes(styleId)) continue;
11050
- const fromStyle = asset?.revision?.fromStyle;
11594
+ if (!asset || !assetInStyle(asset, styleId, manifest.styles[styleId])) continue;
11595
+ const fromStyle = asset.revision?.fromStyle;
11051
11596
  if (fromStyle && fromStyle !== styleId) needed.add(fromStyle);
11052
11597
  }
11053
11598
  styleDependencies.set(styleId, needed);
@@ -11076,11 +11621,11 @@ async function resolveSpecs(loaded, filter) {
11076
11621
  const styleFinalizers = /* @__PURE__ */ new Map();
11077
11622
  const styleImageCache = /* @__PURE__ */ new Map();
11078
11623
  async function loadStyleImage(rel, what = "Style image") {
11079
- const abs = path14.resolve(root, rel);
11624
+ const abs = path15.resolve(root, rel);
11080
11625
  let hit = styleImageCache.get(abs);
11081
11626
  if (!hit) {
11082
- if (!existsSync8(abs)) throw new Error(`${what} not found: ${abs}`);
11083
- const buf = await readFile6(abs);
11627
+ if (!existsSync9(abs)) throw new Error(`${what} not found: ${abs}`);
11628
+ const buf = await readFile7(abs);
11084
11629
  const metadata = imageMetadata(buf);
11085
11630
  if (!metadata) throw new Error(`${what} is not a readable PNG or JPEG: ${abs}`);
11086
11631
  if (metadata.width < 1 || metadata.height < 1) {
@@ -11106,14 +11651,14 @@ async function resolveSpecs(loaded, filter) {
11106
11651
  styleImageDimensions.push(loadedImage);
11107
11652
  }
11108
11653
  const resolvedImages = style.styleImages.map((image) => {
11109
- const hit = styleImageCache.get(path14.resolve(root, image.path));
11654
+ const hit = styleImageCache.get(path15.resolve(root, image.path));
11110
11655
  return { base64: hit.base64, width: hit.width, height: hit.height, format: hit.format };
11111
11656
  });
11112
11657
  const styleSpecs = /* @__PURE__ */ new Map();
11113
11658
  const providerInputIdentities = /* @__PURE__ */ new Map();
11114
11659
  for (const [assetId, asset] of Object.entries(manifest.assets)) {
11115
11660
  if (!resolutionAssetIds.has(assetId)) continue;
11116
- if (asset.styles.length && !asset.styles.includes(styleId)) continue;
11661
+ if (!assetInStyle(asset, styleId, style)) continue;
11117
11662
  const generator = style.generator;
11118
11663
  if (!activeProvider.supports(generator)) {
11119
11664
  throw new Error(`Provider "${activeProvider.id}" does not support generator "${generator}"`);
@@ -11133,6 +11678,15 @@ async function resolveSpecs(loaded, filter) {
11133
11678
  const providerInputs = inputResolution.inputs;
11134
11679
  const providerInputIdentity = inputResolution.identity ?? providerInputs;
11135
11680
  providerInputIdentities.set(assetId, providerInputIdentity);
11681
+ if (asset.vfx && generator !== "vfx") {
11682
+ throw new Error(`assets.${assetId}: vfx needs a vfx style; "${styleId}" generates ${generator}`);
11683
+ }
11684
+ if (generator === "vfx" && !asset.vfx && !asset.mirror && !asset.revision) {
11685
+ throw new Error(
11686
+ `assets.${assetId}: style "${styleId}" renders effects; give the asset a vfx block, or make it a mirror or revision of one`
11687
+ );
11688
+ }
11689
+ const vfx = generator === "vfx" && asset.vfx ? await resolveVfxShape(root, assetId, asset.vfx, `pixelkiln:${styleId}/${assetId}`) : void 0;
11136
11690
  let width;
11137
11691
  let height;
11138
11692
  let size;
@@ -11160,6 +11714,11 @@ async function resolveSpecs(loaded, filter) {
11160
11714
  size = asset.size ?? style.size ?? 64;
11161
11715
  width = size;
11162
11716
  height = size;
11717
+ } else if (generator === "vfx") {
11718
+ const canvas = vfx?.recipe?.recipe.canvas;
11719
+ width = typeof canvas?.w === "number" ? canvas.w : asset.width ?? style.size ?? 64;
11720
+ height = typeof canvas?.h === "number" ? canvas.h : asset.height ?? style.size ?? 64;
11721
+ size = Math.max(width, height);
11163
11722
  } else if (generator === "uiAsset" || generator === "uiElement") {
11164
11723
  width = asset.width ?? style.size ?? 256;
11165
11724
  height = asset.height ?? style.size ?? 256;
@@ -11202,14 +11761,14 @@ async function resolveSpecs(loaded, filter) {
11202
11761
  terrainUpperDescription = wrap(parsed.upper);
11203
11762
  terrainTransitionDescription = parsed.transition ? wrap(parsed.transition) : void 0;
11204
11763
  prompt = subject.trim();
11205
- } else if ((generator === "character" || generator === "objectPro") && characterKind !== "base") {
11764
+ } else if ((generator === "character" || generator === "objectPro") && characterKind !== "base" || vfx) {
11206
11765
  prompt = subject.trim();
11207
11766
  } else {
11208
11767
  prompt = [style.promptPrefix, subject, style.promptSuffix].map((p) => p.trim()).filter(Boolean).join(", ");
11209
11768
  }
11210
- const relFile = asset.file ?? path14.join(asset.category ?? "", `${assetId}.png`);
11211
- const outFile = path14.resolve(root, style.outDir, relFile);
11212
- const qualityOutFile = style.quality ? path14.resolve(root, style.quality.outDir, pngPath(relFile)) : void 0;
11769
+ const relFile = asset.file ?? path15.join(asset.category ?? "", `${assetId}.png`);
11770
+ const outFile = path15.resolve(root, style.outDir, relFile);
11771
+ const qualityOutFile = style.quality ? path15.resolve(root, style.quality.outDir, pngPath(relFile)) : void 0;
11213
11772
  const tileSize = generator === "tiles" ? size : style.tileSize ?? 32;
11214
11773
  const tileVariations = tileFeatureOutputCount(generator === "tiles" ? style.tileFeature : void 0) ?? tileVariationCount(countNumberedDescriptions(prompt));
11215
11774
  const terrainTiles = generator === "terrain" ? terrainTileCount(style.terrainTransitionSize) : 0;
@@ -11280,6 +11839,7 @@ async function resolveSpecs(loaded, filter) {
11280
11839
  // A portrait needs a character style (refused above), so an
11281
11840
  // objectPro asset is only ever a base, state, or animation.
11282
11841
  ...generator === "objectPro" && !asset.revision && characterKind !== "portrait" ? { objectPro: await resolveObjectProShape(assetId, asset, style, characterKind, { root, load: loadStyleImage }) } : {},
11842
+ ...vfx ? { vfx } : {},
11283
11843
  cost: generator === "tiles" ? tilesCost(tileSize, tileVariations) : generator === "terrain" ? tilesCost(size, terrainTiles) : generator === "isometricTile" ? isometricTileCost() : generationCost(width, height, generator),
11284
11844
  costUnit: "generations",
11285
11845
  candidates: generator === "tiles" ? tileVariations : generator === "terrain" ? terrainTiles : generator === "1dir" || generator === "imagePro" ? candidateCount(size) : 1
@@ -11304,7 +11864,7 @@ async function resolveSpecs(loaded, filter) {
11304
11864
  minGridConfidence: style.quality.minGridConfidence,
11305
11865
  ...style.quality.minTransparency == null ? {} : { minTransparency: style.quality.minTransparency },
11306
11866
  ...style.quality.fixerRevision ? { fixerRevision: style.quality.fixerRevision } : {},
11307
- ...style.quality.fixerPython ? { fixerPython: path14.resolve(root, style.quality.fixerPython) } : {},
11867
+ ...style.quality.fixerPython ? { fixerPython: path15.resolve(root, style.quality.fixerPython) } : {},
11308
11868
  ...style.quality.fps == null ? {} : { fps: style.quality.fps }
11309
11869
  }
11310
11870
  } : {},
@@ -11387,7 +11947,7 @@ async function resolveSpecs(loaded, filter) {
11387
11947
  const file = memberPath(anchorSpec.outFile, "south", 0, anchorSpec.character.directions, MediaType.PNG);
11388
11948
  resolved.character = {
11389
11949
  ...resolved.character,
11390
- styleAnchor: { assetId: anchorId, spec: anchorSpec, file, sha256: existsSync8(file) ? await sha256File(file) : null }
11950
+ styleAnchor: { assetId: anchorId, spec: anchorSpec, file, sha256: existsSync9(file) ? await sha256File(file) : null }
11391
11951
  };
11392
11952
  }
11393
11953
  const characterParent = asset.state?.of ?? asset.animation?.of ?? asset.portrait?.of;
@@ -11400,7 +11960,7 @@ async function resolveSpecs(loaded, filter) {
11400
11960
  parentAssetId: characterParent,
11401
11961
  parentSpec,
11402
11962
  parentFile,
11403
- parentSha256: existsSync8(parentFile) ? await sha256File(parentFile) : null,
11963
+ parentSha256: existsSync9(parentFile) ? await sha256File(parentFile) : null,
11404
11964
  // A state and its animations keep the engine and rotations of the base.
11405
11965
  mode: resolved.character.kind === "base" ? resolved.character.mode : parentSpec.character?.mode ?? resolved.character.mode,
11406
11966
  directions: resolved.character.kind === "base" ? resolved.character.directions : parentSpec.character?.directions ?? resolved.character.directions
@@ -11418,7 +11978,7 @@ async function resolveSpecs(loaded, filter) {
11418
11978
  parentAssetId: characterParent,
11419
11979
  parentSpec,
11420
11980
  parentFile,
11421
- parentSha256: existsSync8(parentFile) ? await sha256File(parentFile) : null,
11981
+ parentSha256: existsSync9(parentFile) ? await sha256File(parentFile) : null,
11422
11982
  // A state and its animations keep the base's own rotation count.
11423
11983
  directions: resolved.objectPro.kind === "base" ? resolved.objectPro.directions : parentSpec.objectPro.directions
11424
11984
  };
@@ -11440,7 +12000,7 @@ async function resolveSpecs(loaded, filter) {
11440
12000
  animation: sourceSpec.character.animation,
11441
12001
  outfit: {
11442
12002
  reference: {
11443
- path: path14.resolve(root, asset.outfit.reference),
12003
+ path: path15.resolve(root, asset.outfit.reference),
11444
12004
  sha256: referenceImage.hash,
11445
12005
  width: referenceImage.width,
11446
12006
  height: referenceImage.height,
@@ -11463,23 +12023,23 @@ async function resolveSpecs(loaded, filter) {
11463
12023
  }
11464
12024
  const sourceStyleId = asset.revision.fromStyle && asset.revision.fromStyle !== styleId ? asset.revision.fromStyle : void 0;
11465
12025
  const sourceSpec = sourceStyleId ? await styleFinalizers.get(sourceStyleId)(asset.revision.from) : await finalize(asset.revision.from);
11466
- const sourceStem = sourceSpec.quality?.outFile ?? (sourceSpec.source ? path14.resolve(root, sourceSpec.source) : sourceSpec.outFile);
12026
+ const sourceStem = sourceSpec.quality?.outFile ?? (sourceSpec.source ? path15.resolve(root, sourceSpec.source) : sourceSpec.outFile);
11467
12027
  const sourceMembers = await revisionSourceMembers(sourceStem, `revision source ${asset.revision.from}`);
11468
12028
  const sourceFile = sourceMembers?.[0]?.file ?? sourceStem;
11469
12029
  const sourceImage = sourceMembers ? { ...sourceMembers[0], hash: sha256(JSON.stringify(sourceMembers.map((member) => member.sha256))) } : await optionalRevisionImage(sourceFile, "revision source");
11470
12030
  const sourceFps = sourceSpec.character?.animation?.fps ?? sourceSpec.objectPro?.animation?.fps ?? sourceSpec.revision?.fps ?? sourceSpec.revision?.sourceFps;
11471
- const maskFile = asset.revision.mask ? path14.resolve(root, asset.revision.mask) : void 0;
12031
+ const maskFile = asset.revision.mask ? path15.resolve(root, asset.revision.mask) : void 0;
11472
12032
  const maskImage = maskFile ? await optionalRevisionImage(maskFile, "revision mask", "png") : null;
11473
12033
  if (sourceImage && maskImage && (sourceImage.width !== maskImage.width || sourceImage.height !== maskImage.height)) {
11474
12034
  throw new Error(
11475
12035
  `Revision mask for ${styleId}/${assetId} is ${maskImage.width}x${maskImage.height}; source ${asset.revision.from} is ${sourceImage.width}x${sourceImage.height}`
11476
12036
  );
11477
12037
  }
11478
- const paletteImageFile = asset.revision.paletteImage ? path14.resolve(root, asset.revision.paletteImage) : void 0;
12038
+ const paletteImageFile = asset.revision.paletteImage ? path15.resolve(root, asset.revision.paletteImage) : void 0;
11479
12039
  const paletteImage = paletteImageFile ? await optionalRevisionImage(paletteImageFile, "revision palette image", "png") : null;
11480
- const lastFrameFile = asset.revision.lastFrame ? path14.resolve(root, asset.revision.lastFrame) : void 0;
12040
+ const lastFrameFile = asset.revision.lastFrame ? path15.resolve(root, asset.revision.lastFrame) : void 0;
11481
12041
  const lastFrame = lastFrameFile ? await optionalRevisionImage(lastFrameFile, "revision last frame") : null;
11482
- const keypointsFile = asset.revision.keypointsFile ? path14.resolve(root, asset.revision.keypointsFile) : void 0;
12042
+ const keypointsFile = asset.revision.keypointsFile ? path15.resolve(root, asset.revision.keypointsFile) : void 0;
11483
12043
  const keypoints = keypointsFile ? await optionalSkeletonSet(keypointsFile) : null;
11484
12044
  resolved.revision = {
11485
12045
  mode: asset.revision.mode,
@@ -11649,7 +12209,7 @@ async function resolveSpecs(loaded, filter) {
11649
12209
  return specs;
11650
12210
  }
11651
12211
  function pngPath(file) {
11652
- const extension = path14.extname(file);
12212
+ const extension = path15.extname(file);
11653
12213
  return extension ? `${file.slice(0, -extension.length)}.png` : `${file}.png`;
11654
12214
  }
11655
12215
  async function resolveMapScene(root, styleId, assetId, generator, scene, provider) {
@@ -11659,12 +12219,12 @@ async function resolveMapScene(root, styleId, assetId, generator, scene, provide
11659
12219
  if (!provider.supportsMapScene) {
11660
12220
  throw new Error(`assets.${assetId}.scene: provider "${provider.id}" cannot draw into a scene; PixelLab's /map-objects can`);
11661
12221
  }
11662
- const file = path14.resolve(root, scene.image);
12222
+ const file = path15.resolve(root, scene.image);
11663
12223
  const image = await optionalRevisionImage(file, "scene image", "png");
11664
12224
  const base = { file, sha256: image?.hash ?? null, width: image?.width ?? null, height: image?.height ?? null };
11665
12225
  const placement = scene.placement;
11666
12226
  if ("mask" in placement) {
11667
- const maskFile = path14.resolve(root, placement.mask);
12227
+ const maskFile = path15.resolve(root, placement.mask);
11668
12228
  const mask = await optionalRevisionImage(maskFile, "scene mask", "png");
11669
12229
  if (image && mask && (image.width !== mask.width || image.height !== mask.height)) {
11670
12230
  throw new Error(
@@ -11678,9 +12238,38 @@ async function resolveMapScene(root, styleId, assetId, generator, scene, provide
11678
12238
  }
11679
12239
  return "oval" in placement ? { ...base, placement: { type: "oval", fraction: placement.oval } } : { ...base, placement: { type: "rectangle", fraction: placement.rectangle } };
11680
12240
  }
12241
+ async function resolveVfxShape(root, assetId, vfx, defaultName) {
12242
+ let recipe;
12243
+ let recordedSprite;
12244
+ if (vfx.recipe) {
12245
+ const file = path15.resolve(root, vfx.recipe);
12246
+ if (!existsSync9(file)) throw new Error(`assets.${assetId}.vfx.recipe: VFX recipe not found: ${file}`);
12247
+ const read = await readVfxRecipe(file).catch((error) => {
12248
+ throw new Error(`assets.${assetId}.vfx.recipe: ${error instanceof Error ? error.message : String(error)}`);
12249
+ });
12250
+ recipe = { file, sha256: vfxRecipeSha256(read.recipe), recipe: read.recipe };
12251
+ recordedSprite = read.sprite;
12252
+ }
12253
+ const spriteRef = vfx.sprite ? isVfxReference(vfx.sprite) ? vfx.sprite : path15.resolve(root, vfx.sprite) : recordedSprite;
12254
+ let sprite;
12255
+ if (spriteRef && isVfxReference(spriteRef)) {
12256
+ sprite = { reference: spriteRef };
12257
+ } else if (spriteRef) {
12258
+ const image = await optionalRevisionImage(spriteRef, `assets.${assetId}.vfx.sprite`);
12259
+ sprite = { file: spriteRef, sha256: image?.hash ?? null, width: image?.width ?? null, height: image?.height ?? null, format: image?.format ?? null };
12260
+ }
12261
+ return {
12262
+ ...vfx.preset ? { preset: vfx.preset } : {},
12263
+ ...recipe ? { recipe } : {},
12264
+ ...vfx.parts?.length ? { parts: vfx.parts } : {},
12265
+ ...sprite ? { sprite } : {},
12266
+ ...vfx.fps !== void 0 ? { fps: vfx.fps } : {},
12267
+ name: vfx.name ?? defaultName
12268
+ };
12269
+ }
11681
12270
  async function optionalRevisionImage(file, label, requiredFormat) {
11682
- if (!existsSync8(file)) return null;
11683
- const bytes = await readFile6(file);
12271
+ if (!existsSync9(file)) return null;
12272
+ const bytes = await readFile7(file);
11684
12273
  const metadata = imageMetadata(bytes);
11685
12274
  if (!metadata) throw new Error(`${label} is not a readable PNG or JPEG: ${file}`);
11686
12275
  if (requiredFormat && metadata.format !== requiredFormat) {
@@ -11689,8 +12278,8 @@ async function optionalRevisionImage(file, label, requiredFormat) {
11689
12278
  return { hash: sha256(bytes), ...metadata };
11690
12279
  }
11691
12280
  async function optionalSkeletonSet(file) {
11692
- if (!existsSync8(file)) return null;
11693
- const bytes = await readFile6(file);
12281
+ if (!existsSync9(file)) return null;
12282
+ const bytes = await readFile7(file);
11694
12283
  let json;
11695
12284
  try {
11696
12285
  json = JSON.parse(bytes.toString("utf8"));
@@ -11700,17 +12289,17 @@ async function optionalSkeletonSet(file) {
11700
12289
  return { hash: sha256(bytes), skeleton: parseSkeletonSet(json, file) };
11701
12290
  }
11702
12291
  async function revisionSourceMembers(stem, label) {
11703
- if (existsSync8(stem)) return null;
12292
+ if (existsSync9(stem)) return null;
11704
12293
  const at = (role) => memberPath(stem, role, 0, 2, MediaType.PNG);
11705
12294
  let roles = [];
11706
12295
  for (const set of [CHARACTER_DIRECTIONS_8, CHARACTER_DIRECTIONS_4]) {
11707
- if (set.every((direction) => existsSync8(at(direction)))) {
12296
+ if (set.every((direction) => existsSync9(at(direction)))) {
11708
12297
  roles = [...set];
11709
12298
  break;
11710
12299
  }
11711
12300
  }
11712
12301
  if (!roles.length) {
11713
- for (let index = 0; existsSync8(at(`frame-${String(index).padStart(2, "0")}`)); index++) {
12302
+ for (let index = 0; existsSync9(at(`frame-${String(index).padStart(2, "0")}`)); index++) {
11714
12303
  roles.push(`frame-${String(index).padStart(2, "0")}`);
11715
12304
  }
11716
12305
  }
@@ -11736,8 +12325,8 @@ async function resolveStyleImages(loaded, styleId) {
11736
12325
  if (!style) throw new Error(`Unknown style "${styleId}"`);
11737
12326
  const out = [];
11738
12327
  for (const img of style.styleImages) {
11739
- const file = path14.resolve(loaded.root, img.path);
11740
- const buf = await readFile6(file);
12328
+ const file = path15.resolve(loaded.root, img.path);
12329
+ const buf = await readFile7(file);
11741
12330
  const metadata = imageMetadata(buf);
11742
12331
  if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${file}`);
11743
12332
  if (metadata.width < 1 || metadata.height < 1) {
@@ -11764,7 +12353,7 @@ async function resolveCharacterShape(asset, style, kind, files) {
11764
12353
  };
11765
12354
  if (asset.concept) {
11766
12355
  const image = await files.load(asset.concept, "Concept image");
11767
- shape.concept = { path: path14.resolve(files.root, asset.concept), sha256: image.hash, width: image.width, height: image.height, format: image.format };
12356
+ shape.concept = { path: path15.resolve(files.root, asset.concept), sha256: image.hash, width: image.width, height: image.height, format: image.format };
11768
12357
  }
11769
12358
  if (asset.reference) {
11770
12359
  const byDirection = typeof asset.reference === "string" ? { south: asset.reference } : asset.reference;
@@ -11772,7 +12361,7 @@ async function resolveCharacterShape(asset, style, kind, files) {
11772
12361
  for (const [direction, rel] of Object.entries(byDirection)) {
11773
12362
  const image = await files.load(rel, `Reference sprite (${direction})`);
11774
12363
  shape.reference[direction] = {
11775
- path: path14.resolve(files.root, rel),
12364
+ path: path15.resolve(files.root, rel),
11776
12365
  sha256: image.hash,
11777
12366
  width: image.width,
11778
12367
  height: image.height,
@@ -11793,7 +12382,7 @@ async function resolveCharacterShape(asset, style, kind, files) {
11793
12382
  const animationMode = asset.animation.mode ?? (asset.animation.template ? "template" : "v3");
11794
12383
  const pose = async (rel, what) => {
11795
12384
  const image = await files.load(rel, what);
11796
- return { path: path14.resolve(files.root, rel), sha256: image.hash, width: image.width, height: image.height, format: image.format };
12385
+ return { path: path15.resolve(files.root, rel), sha256: image.hash, width: image.width, height: image.height, format: image.format };
11797
12386
  };
11798
12387
  shape.animation = {
11799
12388
  mode: animationMode,
@@ -11824,7 +12413,7 @@ async function resolveObjectProShape(assetId, asset, style, kind, files) {
11824
12413
  const south = typeof asset.reference === "string" ? asset.reference : asset.reference.south;
11825
12414
  if (south) {
11826
12415
  const image = await files.load(south, "Reference sprite");
11827
- shape.reference = { path: path14.resolve(files.root, south), sha256: image.hash, width: image.width, height: image.height, format: image.format };
12416
+ shape.reference = { path: path15.resolve(files.root, south), sha256: image.hash, width: image.width, height: image.height, format: image.format };
11828
12417
  }
11829
12418
  }
11830
12419
  if (asset.state) {
@@ -11852,7 +12441,7 @@ async function resolveObjectProShape(assetId, asset, style, kind, files) {
11852
12441
  }
11853
12442
  const pose = async (rel, what) => {
11854
12443
  const image = await files.load(rel, what);
11855
- return { path: path14.resolve(files.root, rel), sha256: image.hash, width: image.width, height: image.height, format: image.format };
12444
+ return { path: path15.resolve(files.root, rel), sha256: image.hash, width: image.width, height: image.height, format: image.format };
11856
12445
  };
11857
12446
  shape.animation = {
11858
12447
  mode: asset.animation.mode ?? "v3",
@@ -11869,9 +12458,9 @@ async function resolveObjectProShape(assetId, asset, style, kind, files) {
11869
12458
  }
11870
12459
 
11871
12460
  // src/pipeline/fetch.ts
11872
- import { existsSync as existsSync9, readFileSync as readFileSync4 } from "fs";
11873
- import { mkdir as mkdir4, readFile as readFile7, rename as rename4, rm as rm4, writeFile as writeFile4 } from "fs/promises";
11874
- import path15 from "path";
12461
+ import { existsSync as existsSync10, readFileSync as readFileSync4 } from "fs";
12462
+ import { mkdir as mkdir5, readFile as readFile8, rename as rename4, rm as rm4, writeFile as writeFile5 } from "fs/promises";
12463
+ import path16 from "path";
11875
12464
 
11876
12465
  // src/source-url.ts
11877
12466
  var SENSITIVE_QUERY_KEYS = /* @__PURE__ */ new Set([
@@ -11917,7 +12506,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
11917
12506
  const result = { downloaded: 0, skipped: 0, failed: 0, ...opts.refresh ? { unchanged: 0 } : {} };
11918
12507
  const specByKey = new Map(specs.map((s) => [lockKey(s.styleId, s.assetId), s]));
11919
12508
  normalizeLockOutputPaths(lock, specs);
11920
- const cacheDir = opts.cacheDir === false ? null : path15.resolve(opts.cacheDir ?? path15.join(path15.dirname(lockPath), ".pixelkiln", "cache"));
12509
+ const cacheDir = opts.cacheDir === false ? null : path16.resolve(opts.cacheDir ?? path16.join(path16.dirname(lockPath), ".pixelkiln", "cache"));
11921
12510
  const pending = Object.entries(lock.entries).filter(([key, e]) => {
11922
12511
  if (e.provider !== provider.id || !specByKey.has(key)) return false;
11923
12512
  if (specByKey.get(key)?.mirror) return false;
@@ -11928,14 +12517,14 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
11928
12517
  if (spec && e.specHash === spec.specHash && e.outputs.length && !postprocessCurrent(e, spec)) {
11929
12518
  return Boolean(opts.force) || e.outputs.every((output) => {
11930
12519
  const file = resolveOutputPath(output.path, spec.root);
11931
- return existsSync9(file) && sha256(readFileSync4(file)) === output.sha256;
12520
+ return existsSync10(file) && sha256(readFileSync4(file)) === output.sha256;
11932
12521
  });
11933
12522
  }
11934
12523
  if (!opts.repair) return false;
11935
12524
  if (e.outputs.length === 0 || !spec) return true;
11936
12525
  return e.outputs.some((output) => {
11937
12526
  const file = resolveOutputPath(output.path, spec.root);
11938
- if (!existsSync9(file)) return true;
12527
+ if (!existsSync10(file)) return true;
11939
12528
  const current = sha256(readFileSync4(file));
11940
12529
  if (current === output.sha256) return false;
11941
12530
  return Boolean(opts.force) || (e.supersededOutputs ?? []).some((old) => old.sha256 === current);
@@ -12019,21 +12608,21 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12019
12608
  if (!source.url) throw new Error(`no durable source URL to refresh ${source.role ?? "asset"} from`);
12020
12609
  fresh = await provider.download(source.url);
12021
12610
  if (recorded && sha256(fresh) === recordedRaw && !reapply) {
12022
- log2(` current ${path15.relative(process.cwd(), target)}`);
12611
+ log2(` current ${path16.relative(process.cwd(), target)}`);
12023
12612
  outputs.push({ ...recorded, path: portableOutputPath(target, spec.root) });
12024
12613
  continue;
12025
12614
  }
12026
12615
  }
12027
12616
  let raw = null;
12028
12617
  let final = null;
12029
- if (existsSync9(target)) {
12618
+ if (existsSync10(target)) {
12030
12619
  const currentHash = await sha256File(target);
12031
12620
  if (recorded && currentHash === recorded.sha256) {
12032
12621
  if (!fresh && !reapply) {
12033
12622
  if (cacheDir) {
12034
12623
  await cacheMedia(
12035
12624
  cacheDir,
12036
- await readFile7(target),
12625
+ await readFile8(target),
12037
12626
  recorded.mediaType ?? MediaType.PNG,
12038
12627
  recorded.sha256
12039
12628
  );
@@ -12042,10 +12631,10 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12042
12631
  continue;
12043
12632
  }
12044
12633
  if (reapply && !fresh) {
12045
- if (!recorded.raw) raw = await readFile7(target);
12046
- log2(` reapply ${path15.relative(process.cwd(), target)}`);
12634
+ if (!recorded.raw) raw = await readFile8(target);
12635
+ log2(` reapply ${path16.relative(process.cwd(), target)}`);
12047
12636
  } else {
12048
- log2(` changed ${path15.relative(process.cwd(), target)} (upstream)`);
12637
+ log2(` changed ${path16.relative(process.cwd(), target)} (upstream)`);
12049
12638
  }
12050
12639
  } else {
12051
12640
  const superseded = (entry.supersededOutputs ?? []).find(
@@ -12066,14 +12655,14 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12066
12655
  }
12067
12656
  if (outgoing && cacheDir) {
12068
12657
  try {
12069
- const oldBytes = await readFile7(target);
12658
+ const oldBytes = await readFile8(target);
12070
12659
  const oldMediaType = detectMediaType(oldBytes);
12071
12660
  if (oldMediaType) await cacheMedia(cacheDir, oldBytes, oldMediaType, currentHash);
12072
12661
  } catch {
12073
12662
  }
12074
12663
  }
12075
12664
  log2(
12076
- ` replace ${path15.relative(process.cwd(), target)}` + (opts.force ? " (--force)" : " (previous tracked generation)")
12665
+ ` replace ${path16.relative(process.cwd(), target)}` + (opts.force ? " (--force)" : " (previous tracked generation)")
12077
12666
  );
12078
12667
  }
12079
12668
  }
@@ -12082,7 +12671,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12082
12671
  } else if (!raw && recorded && cacheDir) {
12083
12672
  if (!reapply) final = await readCachedMedia(cacheDir, recorded.sha256, expectedMediaType);
12084
12673
  if (!final && recordedRaw) raw = await readCachedMedia(cacheDir, recordedRaw, expectedMediaType);
12085
- if (final || raw) log2(` cached ${path15.relative(process.cwd(), target)}`);
12674
+ if (final || raw) log2(` cached ${path16.relative(process.cwd(), target)}`);
12086
12675
  }
12087
12676
  if (!final && !raw) {
12088
12677
  if (!source.url) {
@@ -12109,13 +12698,13 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12109
12698
  buf = processed.bytes;
12110
12699
  if (processed.changed) {
12111
12700
  rawHash = sha256(raw);
12112
- log2(` snapped ${path15.relative(process.cwd(), target)} to ${wanted.palette.colors.length} colours`);
12701
+ log2(` snapped ${path16.relative(process.cwd(), target)} to ${wanted.palette.colors.length} colours`);
12113
12702
  }
12114
12703
  }
12115
12704
  if (cacheDir) await cacheMedia(cacheDir, buf, mediaType);
12116
- await mkdir4(path15.dirname(target), { recursive: true });
12705
+ await mkdir5(path16.dirname(target), { recursive: true });
12117
12706
  const tmp = `${target}.pixelkiln.tmp`;
12118
- await writeFile4(tmp, buf);
12707
+ await writeFile5(tmp, buf);
12119
12708
  outputs.push({
12120
12709
  path: portableOutputPath(target, spec.root),
12121
12710
  sha256: sha256(buf),
@@ -12127,7 +12716,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12127
12716
  await saveLock(lockPath, lock);
12128
12717
  await rename4(tmp, target);
12129
12718
  wrote = true;
12130
- log2(` wrote ${path15.relative(process.cwd(), target)}`);
12719
+ log2(` wrote ${path16.relative(process.cwd(), target)}`);
12131
12720
  }
12132
12721
  if (opts.refresh && !wrote) {
12133
12722
  if (rotated) {
@@ -12171,10 +12760,10 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12171
12760
  return result;
12172
12761
  }
12173
12762
  async function readCachedMedia(cacheDir, hash, mediaType) {
12174
- const file = path15.join(cacheDir, cacheFileName(hash, mediaType));
12175
- if (!existsSync9(file)) return null;
12763
+ const file = path16.join(cacheDir, cacheFileName(hash, mediaType));
12764
+ if (!existsSync10(file)) return null;
12176
12765
  try {
12177
- const buf = await readFile7(file);
12766
+ const buf = await readFile8(file);
12178
12767
  if (sha256(buf) !== hash) return null;
12179
12768
  validateMedia(buf, mediaType);
12180
12769
  return buf;
@@ -12185,15 +12774,15 @@ async function readCachedMedia(cacheDir, hash, mediaType) {
12185
12774
  async function cacheMedia(cacheDir, buf, mediaType, knownHash) {
12186
12775
  const hash = knownHash ?? sha256(buf);
12187
12776
  validateMedia(buf, mediaType);
12188
- const file = path15.join(cacheDir, cacheFileName(hash, mediaType));
12777
+ const file = path16.join(cacheDir, cacheFileName(hash, mediaType));
12189
12778
  if (await readCachedMedia(cacheDir, hash, mediaType)) return;
12190
- await mkdir4(cacheDir, { recursive: true });
12779
+ await mkdir5(cacheDir, { recursive: true });
12191
12780
  const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
12192
- await writeFile4(tmp, buf);
12781
+ await writeFile5(tmp, buf);
12193
12782
  try {
12194
12783
  await rename4(tmp, file);
12195
12784
  } catch (err) {
12196
- if (!existsSync9(file)) throw err;
12785
+ if (!existsSync10(file)) throw err;
12197
12786
  } finally {
12198
12787
  await rm4(tmp, { force: true });
12199
12788
  }
@@ -12213,7 +12802,7 @@ async function pushTags(provider, specs, lock, opts = {}) {
12213
12802
  for (const [key, entry] of Object.entries(lock.entries)) {
12214
12803
  const spec = specByKey.get(key);
12215
12804
  if (!spec || entry.provider !== provider.id || !entry.objectId) continue;
12216
- if (entry.generator === "pixflux") continue;
12805
+ if (entry.generator === "pixflux" || entry.generator === "vfx") continue;
12217
12806
  if (!provider.setTags) return tagged;
12218
12807
  try {
12219
12808
  await provider.setTags(entry.objectId, spec.tags, entry.generator);
@@ -12226,13 +12815,13 @@ async function pushTags(provider, specs, lock, opts = {}) {
12226
12815
  }
12227
12816
 
12228
12817
  // src/pipeline/adopt.ts
12229
- import { existsSync as existsSync11 } from "fs";
12230
- import { readFile as readFile9, writeFile as writeFile6 } from "fs/promises";
12231
- import path16 from "path";
12818
+ import { existsSync as existsSync12 } from "fs";
12819
+ import { readFile as readFile10, writeFile as writeFile7 } from "fs/promises";
12820
+ import path17 from "path";
12232
12821
 
12233
12822
  // src/cache.ts
12234
- import { mkdir as mkdir5, readFile as readFile8, rename as rename5, rm as rm5, writeFile as writeFile5 } from "fs/promises";
12235
- import { existsSync as existsSync10 } from "fs";
12823
+ import { mkdir as mkdir6, readFile as readFile9, rename as rename5, rm as rm5, writeFile as writeFile6 } from "fs/promises";
12824
+ import { existsSync as existsSync11 } from "fs";
12236
12825
  import pathModule from "path";
12237
12826
  import { z as z4 } from "zod";
12238
12827
  var HashCacheSchema = z4.object({
@@ -12247,10 +12836,10 @@ function isSha256Hash(value) {
12247
12836
  function parseCache(value) {
12248
12837
  return HashCacheSchema.parse(value);
12249
12838
  }
12250
- async function loadCache(path52) {
12251
- if (!existsSync10(path52)) return { version: 1, hashes: {} };
12839
+ async function loadCache(path53) {
12840
+ if (!existsSync11(path53)) return { version: 1, hashes: {} };
12252
12841
  try {
12253
- const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile8(path52, "utf8")));
12842
+ const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile9(path53, "utf8")));
12254
12843
  if (!parsed.success) return { version: 1, hashes: {} };
12255
12844
  return {
12256
12845
  version: 1,
@@ -12262,18 +12851,18 @@ async function loadCache(path52) {
12262
12851
  return { version: 1, hashes: {} };
12263
12852
  }
12264
12853
  }
12265
- async function saveCache(path52, cache) {
12854
+ async function saveCache(path53, cache) {
12266
12855
  const sorted = {};
12267
12856
  for (const key of Object.keys(cache.hashes).sort()) {
12268
12857
  const hash = cache.hashes[key];
12269
12858
  if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
12270
12859
  sorted[key] = hash;
12271
12860
  }
12272
- await mkdir5(pathModule.dirname(pathModule.resolve(path52)), { recursive: true });
12273
- const tmp = `${path52}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
12861
+ await mkdir6(pathModule.dirname(pathModule.resolve(path53)), { recursive: true });
12862
+ const tmp = `${path53}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
12274
12863
  try {
12275
- await writeFile5(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
12276
- await rename5(tmp, path52);
12864
+ await writeFile6(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
12865
+ await rename5(tmp, path53);
12277
12866
  } finally {
12278
12867
  await rm5(tmp, { force: true });
12279
12868
  }
@@ -12307,11 +12896,11 @@ async function adopt(provider, specs, lock, lockPath, opts = {}) {
12307
12896
  const localByHash = /* @__PURE__ */ new Map();
12308
12897
  const unmatchedLocal = [];
12309
12898
  for (const spec of specs) {
12310
- if (!existsSync11(spec.outFile)) {
12899
+ if (!existsSync12(spec.outFile)) {
12311
12900
  unmatchedLocal.push(`${spec.styleId}/${spec.assetId} (no file at ${spec.outFile})`);
12312
12901
  continue;
12313
12902
  }
12314
- const bytes = await readFile9(spec.outFile);
12903
+ const bytes = await readFile10(spec.outFile);
12315
12904
  try {
12316
12905
  decodePng(bytes);
12317
12906
  } catch (err) {
@@ -12431,7 +13020,7 @@ async function writePromptsBack(manifestPath, lock, opts = {}) {
12431
13020
  const log2 = opts.onProgress ?? (() => {
12432
13021
  });
12433
13022
  const selectedAssets = opts.assetIds ? new Set(opts.assetIds) : null;
12434
- const raw = JSON.parse(await readFile9(manifestPath, "utf8"));
13023
+ const raw = JSON.parse(await readFile10(manifestPath, "utf8"));
12435
13024
  const best = /* @__PURE__ */ new Map();
12436
13025
  for (const entry of Object.values(lock.entries)) {
12437
13026
  if (opts.provider && entry.provider !== opts.provider) continue;
@@ -12453,7 +13042,7 @@ async function writePromptsBack(manifestPath, lock, opts = {}) {
12453
13042
  stillEmpty.push(id);
12454
13043
  }
12455
13044
  }
12456
- await writeFile6(manifestPath, JSON.stringify(raw, null, 2) + "\n");
13045
+ await writeFile7(manifestPath, JSON.stringify(raw, null, 2) + "\n");
12457
13046
  log2(` filled ${filled} prompt(s) from adopted objects`);
12458
13047
  return { filled, stillEmpty };
12459
13048
  }
@@ -12464,9 +13053,9 @@ function formatUnmatchedRemote(objects, limit = 15) {
12464
13053
  }
12465
13054
 
12466
13055
  // src/pipeline/adopt-characters.ts
12467
- import { existsSync as existsSync12 } from "fs";
12468
- import { mkdir as mkdir6, readFile as readFile10, writeFile as writeFile7 } from "fs/promises";
12469
- import path17 from "path";
13056
+ import { existsSync as existsSync13 } from "fs";
13057
+ import { mkdir as mkdir7, readFile as readFile11, writeFile as writeFile8 } from "fs/promises";
13058
+ import path18 from "path";
12470
13059
  async function adoptCharacters(provider, specs, lock, lockPath, opts) {
12471
13060
  const log2 = opts.onProgress ?? (() => {
12472
13061
  });
@@ -12517,12 +13106,12 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
12517
13106
  const bytes = await download(source.url);
12518
13107
  const hash = sha256(bytes);
12519
13108
  const file = expectedOutputPath(spec, source.role, index, sources.length);
12520
- if (existsSync12(file)) {
12521
- if (sha256(await readFile10(file)) !== hash) result.differing.push(path17.relative(process.cwd(), file));
13109
+ if (existsSync13(file)) {
13110
+ if (sha256(await readFile11(file)) !== hash) result.differing.push(path18.relative(process.cwd(), file));
12522
13111
  } else {
12523
- await mkdir6(path17.dirname(file), { recursive: true });
12524
- await writeFile7(file, bytes);
12525
- result.written.push(path17.relative(process.cwd(), file));
13112
+ await mkdir7(path18.dirname(file), { recursive: true });
13113
+ await writeFile8(file, bytes);
13114
+ result.written.push(path18.relative(process.cwd(), file));
12526
13115
  }
12527
13116
  outputs.push({ path: portableOutputPath(file, spec.root), sha256: hash, ...source.role ? { role: source.role } : {} });
12528
13117
  }
@@ -12560,11 +13149,11 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
12560
13149
  if (!match) throw new Error(`remoteId ${spec.remoteId} is not a character on this account`);
12561
13150
  } else {
12562
13151
  const south = expectedOutputPath(spec, "south", 0, spec.character.directions);
12563
- if (!existsSync12(south)) {
12564
- result.unmatched.push(`${key}: no remoteId and no ${path17.relative(process.cwd(), south)} to match by`);
13152
+ if (!existsSync13(south)) {
13153
+ result.unmatched.push(`${key}: no remoteId and no ${path18.relative(process.cwd(), south)} to match by`);
12565
13154
  return false;
12566
13155
  }
12567
- const localHash = sha256(await readFile10(south));
13156
+ const localHash = sha256(await readFile11(south));
12568
13157
  for (const candidate of remote) {
12569
13158
  if (claimed.has(candidate.id)) continue;
12570
13159
  if (await southHash(candidate) === localHash) {
@@ -12573,7 +13162,7 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
12573
13162
  }
12574
13163
  }
12575
13164
  if (!match) {
12576
- result.unmatched.push(`${key}: no character's south rotation has the bytes of ${path17.relative(process.cwd(), south)}`);
13165
+ result.unmatched.push(`${key}: no character's south rotation has the bytes of ${path18.relative(process.cwd(), south)}`);
12577
13166
  return false;
12578
13167
  }
12579
13168
  }
@@ -12640,15 +13229,15 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
12640
13229
  if (!match) throw new Error(`remoteId ${spec.remoteId} names no ${animation.direction} animation of character ${parentId}`);
12641
13230
  } else {
12642
13231
  const first = expectedOutputPath(spec, "frame-00", 0, 2);
12643
- if (!existsSync12(first)) {
13232
+ if (!existsSync13(first)) {
12644
13233
  const byName = candidates.find((a) => a.name === characterAnimationName(spec));
12645
13234
  if (!byName) {
12646
- result.unmatched.push(`${key}: no remoteId and no ${path17.relative(process.cwd(), first)} to match by`);
13235
+ result.unmatched.push(`${key}: no remoteId and no ${path18.relative(process.cwd(), first)} to match by`);
12647
13236
  return false;
12648
13237
  }
12649
13238
  match = byName;
12650
13239
  } else {
12651
- const localHash = sha256(await readFile10(first));
13240
+ const localHash = sha256(await readFile11(first));
12652
13241
  for (const candidate of candidates) {
12653
13242
  if (sha256(await download(candidate.frames[0])) === localHash) {
12654
13243
  match = candidate;
@@ -12656,7 +13245,7 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
12656
13245
  }
12657
13246
  }
12658
13247
  if (!match) {
12659
- result.unmatched.push(`${key}: no ${animation.direction} animation of character ${parentId} starts with the bytes of ${path17.relative(process.cwd(), first)}`);
13248
+ result.unmatched.push(`${key}: no ${animation.direction} animation of character ${parentId} starts with the bytes of ${path18.relative(process.cwd(), first)}`);
12660
13249
  return false;
12661
13250
  }
12662
13251
  }
@@ -12716,19 +13305,19 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
12716
13305
  }
12717
13306
 
12718
13307
  // src/pipeline/salvage.ts
12719
- import { readFile as readFile11 } from "fs/promises";
12720
- import { existsSync as existsSync13 } from "fs";
12721
- import path18 from "path";
13308
+ import { readFile as readFile12 } from "fs/promises";
13309
+ import { existsSync as existsSync14 } from "fs";
13310
+ import path19 from "path";
12722
13311
  function providerClaimId(provider, objectId) {
12723
13312
  return `${encodeURIComponent(provider)}:${encodeURIComponent(objectId)}`;
12724
13313
  }
12725
13314
  async function loadClaims(lockPaths, opts = {}) {
12726
13315
  const claimed = /* @__PURE__ */ new Set();
12727
13316
  for (const p of lockPaths) {
12728
- if (!existsSync13(p)) throw new Error(`Claim lockfile not found: ${p}`);
13317
+ if (!existsSync14(p)) throw new Error(`Claim lockfile not found: ${p}`);
12729
13318
  let parsed;
12730
13319
  try {
12731
- parsed = parseLock(JSON.parse(await readFile11(p, "utf8")));
13320
+ parsed = parseLock(JSON.parse(await readFile12(p, "utf8")));
12732
13321
  } catch {
12733
13322
  throw new Error(`Claim lockfile is malformed: ${p}`);
12734
13323
  }
@@ -12895,20 +13484,20 @@ function matchStyleByPattern(prompt, manifest) {
12895
13484
  return null;
12896
13485
  }
12897
13486
  async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, claimPaths) {
12898
- const own = path18.resolve(ownManifestPath);
13487
+ const own = path19.resolve(ownManifestPath);
12899
13488
  const siblingManifestPaths = [
12900
13489
  .../* @__PURE__ */ new Set([
12901
13490
  ...workspaceManifestPaths,
12902
- ...claimPaths.map((c) => path18.join(path18.dirname(path18.resolve(c)), "pixelkiln.manifest.json"))
13491
+ ...claimPaths.map((c) => path19.join(path19.dirname(path19.resolve(c)), "pixelkiln.manifest.json"))
12903
13492
  ])
12904
13493
  ];
12905
13494
  const siblings = [];
12906
13495
  for (const siblingManifestPath of siblingManifestPaths) {
12907
- if (path18.resolve(siblingManifestPath) === own) continue;
12908
- if (!existsSync13(siblingManifestPath)) continue;
13496
+ if (path19.resolve(siblingManifestPath) === own) continue;
13497
+ if (!existsSync14(siblingManifestPath)) continue;
12909
13498
  try {
12910
13499
  const { manifest } = await loadManifest(siblingManifestPath);
12911
- siblings.push({ label: path18.basename(path18.dirname(siblingManifestPath)), manifest });
13500
+ siblings.push({ label: path19.basename(path19.dirname(siblingManifestPath)), manifest });
12912
13501
  } catch {
12913
13502
  }
12914
13503
  }
@@ -13028,9 +13617,9 @@ async function applyTags(provider, decisions, existing, opts = {}) {
13028
13617
  }
13029
13618
 
13030
13619
  // src/workspace.ts
13031
- import { mkdir as mkdir7, readFile as readFile12, rename as rename6, rm as rm6, writeFile as writeFile8 } from "fs/promises";
13032
- import { existsSync as existsSync14 } from "fs";
13033
- import path19 from "path";
13620
+ import { mkdir as mkdir8, readFile as readFile13, rename as rename6, rm as rm6, writeFile as writeFile9 } from "fs/promises";
13621
+ import { existsSync as existsSync15 } from "fs";
13622
+ import path20 from "path";
13034
13623
  import { z as z5 } from "zod";
13035
13624
  var WorkspaceProjectSchema = z5.object({
13036
13625
  id: z5.string().min(1),
@@ -13055,10 +13644,10 @@ ${parsed.error.issues.slice(0, 5).map((i) => ` ${i.path.join(".")}: ${i.message
13055
13644
  );
13056
13645
  }
13057
13646
  async function loadWorkspace(workspacePath) {
13058
- if (!existsSync14(workspacePath)) return { version: 1, projects: [] };
13647
+ if (!existsSync15(workspacePath)) return { version: 1, projects: [] };
13059
13648
  let raw;
13060
13649
  try {
13061
- raw = JSON.parse(await readFile12(workspacePath, "utf8"));
13650
+ raw = JSON.parse(await readFile13(workspacePath, "utf8"));
13062
13651
  } catch (err) {
13063
13652
  throw new ProjectError(
13064
13653
  `Workspace catalog at ${workspacePath} is malformed:
@@ -13072,22 +13661,22 @@ async function saveWorkspace(workspacePath, ws) {
13072
13661
  version: 1,
13073
13662
  projects: [...ws.projects].sort((a, b) => a.id.localeCompare(b.id))
13074
13663
  };
13075
- await mkdir7(path19.dirname(path19.resolve(workspacePath)), { recursive: true });
13664
+ await mkdir8(path20.dirname(path20.resolve(workspacePath)), { recursive: true });
13076
13665
  const tmp = `${workspacePath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
13077
13666
  try {
13078
- await writeFile8(tmp, JSON.stringify(sorted, null, 2) + "\n");
13667
+ await writeFile9(tmp, JSON.stringify(sorted, null, 2) + "\n");
13079
13668
  await rename6(tmp, workspacePath);
13080
13669
  } finally {
13081
13670
  await rm6(tmp, { force: true });
13082
13671
  }
13083
13672
  }
13084
13673
  function toPortablePath(dir, absolute) {
13085
- return path19.relative(dir, absolute).split(path19.sep).join("/");
13674
+ return path20.relative(dir, absolute).split(path20.sep).join("/");
13086
13675
  }
13087
13676
  function resolveProject(dir, project) {
13088
13677
  return {
13089
- manifestPath: path19.resolve(dir, project.manifest.split("/").join(path19.sep)),
13090
- lockPath: path19.resolve(dir, project.lock.split("/").join(path19.sep))
13678
+ manifestPath: path20.resolve(dir, project.manifest.split("/").join(path20.sep)),
13679
+ lockPath: path20.resolve(dir, project.lock.split("/").join(path20.sep))
13091
13680
  };
13092
13681
  }
13093
13682
  function validateWorkspace(ws, dir) {
@@ -13100,21 +13689,21 @@ function validateWorkspace(ws, dir) {
13100
13689
  const { manifestPath, lockPath } = resolveProject(dir, project);
13101
13690
  lockOwners.set(lockPath, [...lockOwners.get(lockPath) ?? [], project.id]);
13102
13691
  manifestOwners.set(manifestPath, [...manifestOwners.get(manifestPath) ?? [], project.id]);
13103
- if (path19.isAbsolute(project.manifest) || path19.isAbsolute(project.lock)) {
13692
+ if (path20.isAbsolute(project.manifest) || path20.isAbsolute(project.lock)) {
13104
13693
  diagnostics.push({
13105
13694
  id: "absolute-path",
13106
13695
  level: "warning",
13107
13696
  message: `project "${project.id}" stores an absolute path; the catalog will not resolve correctly if this tree is cloned or moved elsewhere`
13108
13697
  });
13109
13698
  }
13110
- if (!existsSync14(manifestPath)) {
13699
+ if (!existsSync15(manifestPath)) {
13111
13700
  diagnostics.push({
13112
13701
  id: "missing-manifest",
13113
13702
  level: "error",
13114
13703
  message: `project "${project.id}" manifest not found: ${manifestPath}`
13115
13704
  });
13116
13705
  }
13117
- if (!existsSync14(lockPath)) {
13706
+ if (!existsSync15(lockPath)) {
13118
13707
  diagnostics.push({
13119
13708
  id: "missing-lock",
13120
13709
  level: "error",
@@ -13161,8 +13750,8 @@ function validateWorkspace(ws, dir) {
13161
13750
  }
13162
13751
 
13163
13752
  // src/pick/salvage-server.ts
13164
- import { mkdir as mkdir8, writeFile as writeFile9, readFile as readFile14 } from "fs/promises";
13165
- import path20 from "path";
13753
+ import { mkdir as mkdir9, writeFile as writeFile10, readFile as readFile15 } from "fs/promises";
13754
+ import path21 from "path";
13166
13755
 
13167
13756
  // src/pick/salvage-sheet.ts
13168
13757
  var escapeHtml = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
@@ -13323,7 +13912,7 @@ refresh();
13323
13912
 
13324
13913
  // src/pick/review-server.ts
13325
13914
  import { createServer } from "http";
13326
- import { readFile as readFile13 } from "fs/promises";
13915
+ import { readFile as readFile14 } from "fs/promises";
13327
13916
 
13328
13917
  // src/open.ts
13329
13918
  import { spawn } from "child_process";
@@ -13350,7 +13939,7 @@ function serveReviewPage(opts) {
13350
13939
  if (req.method === "GET" && req.url && opts.assets?.has(req.url)) {
13351
13940
  try {
13352
13941
  const asset = opts.assets.get(req.url);
13353
- const bytes = await readFile13(asset.path);
13942
+ const bytes = await readFile14(asset.path);
13354
13943
  res.writeHead(200, {
13355
13944
  "Content-Type": asset.contentType,
13356
13945
  "Cache-Control": "no-store",
@@ -13427,7 +14016,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
13427
14016
  });
13428
14017
  const html = renderSalvageSheet(orphans, {
13429
14018
  styleId: ctx.styleId,
13430
- importDir: path20.relative(process.cwd(), ctx.importDir) || "."
14019
+ importDir: path21.relative(process.cwd(), ctx.importDir) || "."
13431
14020
  });
13432
14021
  const byId = new Map(orphans.map((o) => [o.id, o]));
13433
14022
  const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
@@ -13483,10 +14072,10 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
13483
14072
  if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
13484
14073
  decodePng(buf);
13485
14074
  const assetId = idFromPrompt(orphan.prompt, taken);
13486
- const rel = path20.join("_salvaged", `${assetId}.png`);
13487
- const outFile = path20.resolve(ctx.importDir, rel);
13488
- await mkdir8(path20.dirname(outFile), { recursive: true });
13489
- await writeFile9(outFile, buf);
14075
+ const rel = path21.join("_salvaged", `${assetId}.png`);
14076
+ const outFile = path21.resolve(ctx.importDir, rel);
14077
+ await mkdir9(path21.dirname(outFile), { recursive: true });
14078
+ await writeFile10(outFile, buf);
13490
14079
  ctx.manifest.assets[assetId] = {
13491
14080
  prompt: orphan.prompt,
13492
14081
  promptByStyle: {},
@@ -13518,7 +14107,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
13518
14107
  sourceUrl: durableSource,
13519
14108
  sourceUrls: durableSource ? [{ url: durableSource }] : [],
13520
14109
  outputs: [{
13521
- path: portableOutputPath(outFile, path20.dirname(ctx.manifestPath)),
14110
+ path: portableOutputPath(outFile, path21.dirname(ctx.manifestPath)),
13522
14111
  sha256: sha256(buf)
13523
14112
  }],
13524
14113
  submittedAt: orphan.createdAt,
@@ -13542,9 +14131,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
13542
14131
  }
13543
14132
  }
13544
14133
  await applyTags(provider, decisions, existingTags, { onProgress: log2, characters: characterMembers });
13545
- const raw = JSON.parse(await readFile14(ctx.manifestPath, "utf8"));
14134
+ const raw = JSON.parse(await readFile15(ctx.manifestPath, "utf8"));
13546
14135
  for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
13547
- await writeFile9(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
14136
+ await writeFile10(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
13548
14137
  await saveLock(ctx.lockPath, ctx.lock);
13549
14138
  if (characterAssetIds.length) result.characterAssetIds = characterAssetIds;
13550
14139
  return result;
@@ -13553,19 +14142,19 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
13553
14142
  }
13554
14143
 
13555
14144
  // src/cli/project.ts
13556
- import path22 from "path";
13557
- import { existsSync as existsSync15 } from "fs";
14145
+ import path23 from "path";
14146
+ import { existsSync as existsSync16 } from "fs";
13558
14147
 
13559
14148
  // src/project.ts
13560
- import path21 from "path";
14149
+ import path22 from "path";
13561
14150
  async function openProject(manifestPath = "pixelkiln.manifest.json", opts = {}) {
13562
- const absolute = path21.resolve(manifestPath);
13563
- const root = path21.dirname(absolute);
14151
+ const absolute = path22.resolve(manifestPath);
14152
+ const root = path22.dirname(absolute);
13564
14153
  if (opts.env !== false) {
13565
14154
  loadEnvFiles(root);
13566
- if (path21.resolve(process.cwd()) !== root) loadEnvFiles(process.cwd());
14155
+ if (path22.resolve(process.cwd()) !== root) loadEnvFiles(process.cwd());
13567
14156
  }
13568
- const lockPath = path21.resolve(opts.lockPath ?? path21.join(root, "pixelkiln.lock.json"));
14157
+ const lockPath = path22.resolve(opts.lockPath ?? path22.join(root, "pixelkiln.lock.json"));
13569
14158
  const loaded = await loadManifest(absolute);
13570
14159
  const specs = await resolveSpecs(loaded, { styles: opts.styles ?? [], assets: opts.assets ?? [] });
13571
14160
  const lock = await loadLock(lockPath);
@@ -13696,18 +14285,18 @@ async function workspaceStatus(ws, dir) {
13696
14285
 
13697
14286
  // src/cli/project.ts
13698
14287
  async function provenanceFile(id, file) {
13699
- const absolute = path22.resolve(file);
14288
+ const absolute = path23.resolve(file);
13700
14289
  return {
13701
14290
  id,
13702
14291
  path: absolute,
13703
- sha256: existsSync15(absolute) ? await sha256File(absolute) : null,
14292
+ sha256: existsSync16(absolute) ? await sha256File(absolute) : null,
13704
14293
  included: true
13705
14294
  };
13706
14295
  }
13707
14296
  function manifestSources(manifest, styleId) {
13708
14297
  const sources = {};
13709
14298
  for (const [assetId, asset] of Object.entries(manifest.assets)) {
13710
- if (asset.styles.length && !asset.styles.includes(styleId)) continue;
14299
+ if (!assetInStyle(asset, styleId, manifest.styles[styleId])) continue;
13711
14300
  const source = asset.sourceByStyle[styleId] ?? asset.source;
13712
14301
  if (source) sources[assetId] = source;
13713
14302
  }
@@ -13772,10 +14361,10 @@ function budgetsForPlan(plan, args) {
13772
14361
  return out;
13773
14362
  }
13774
14363
  async function requireCompleteWorkspaceClaims(workspacePath) {
13775
- if (!existsSync15(workspacePath)) {
14364
+ if (!existsSync16(workspacePath)) {
13776
14365
  throw new ProjectError(`Workspace catalog not found: ${workspacePath}`);
13777
14366
  }
13778
- const dir = path22.dirname(path22.resolve(workspacePath));
14367
+ const dir = path23.dirname(path23.resolve(workspacePath));
13779
14368
  const ws = await loadWorkspace(workspacePath);
13780
14369
  const diagnostics = validateWorkspace(ws, dir);
13781
14370
  const errors = diagnostics.filter((d) => d.level === "error");
@@ -13798,9 +14387,9 @@ async function openAccountProject(args) {
13798
14387
  return project;
13799
14388
  }
13800
14389
  async function open(args) {
13801
- if (!existsSync15(path22.resolve(args.manifest))) {
14390
+ if (!existsSync16(path23.resolve(args.manifest))) {
13802
14391
  throw new ProjectError(
13803
- `No manifest at ${path22.resolve(args.manifest)}. Pass --manifest, or run \`pixelkiln init --from <dir>\`.`
14392
+ `No manifest at ${path23.resolve(args.manifest)}. Pass --manifest, or run \`pixelkiln init --from <dir>\`.`
13804
14393
  );
13805
14394
  }
13806
14395
  const project = await openProject(args.manifest, { lockPath: args.lock, styles: args.styles, assets: args.assets });
@@ -13842,7 +14431,7 @@ function providerCache(mode2) {
13842
14431
 
13843
14432
  // src/cli/commands/account.ts
13844
14433
  async function runBalance(args) {
13845
- loadEnvFiles(path23.dirname(path23.resolve(args.manifest)));
14434
+ loadEnvFiles(path24.dirname(path24.resolve(args.manifest)));
13846
14435
  loadEnvFiles(process.cwd());
13847
14436
  const loaded = await loadManifest(args.manifest);
13848
14437
  const selectedProvider = accountProviderId(loaded.manifest, args.provider, "balance");
@@ -13915,12 +14504,12 @@ async function runAdopt(args) {
13915
14504
  tagged ${n} object(s) upstream`);
13916
14505
  }
13917
14506
  if (args.writePrompts) {
13918
- const { filled, stillEmpty } = await writePromptsBack(path23.resolve(args.manifest), lock, {
14507
+ const { filled, stillEmpty } = await writePromptsBack(path24.resolve(args.manifest), lock, {
13919
14508
  onProgress: log,
13920
14509
  provider: provider.id,
13921
14510
  assetIds: specs.map((spec) => spec.assetId)
13922
14511
  });
13923
- log(` recovered ${filled} prompt(s) into ${path23.relative(process.cwd(), args.manifest)}`);
14512
+ log(` recovered ${filled} prompt(s) into ${path24.relative(process.cwd(), args.manifest)}`);
13924
14513
  const reloaded = await loadManifest(args.manifest);
13925
14514
  const rebased = (await resolveSpecs(reloaded, {
13926
14515
  styles: args.styles,
@@ -13950,12 +14539,12 @@ async function runSalvage2(args) {
13950
14539
  const provider = providerFor(accountProvider);
13951
14540
  const jsonMode = args.dryRun && args.json;
13952
14541
  const diag = jsonMode ? (msg = "") => console.error(msg) : log;
13953
- const ownLock = path23.resolve(args.lock);
14542
+ const ownLock = path24.resolve(args.lock);
13954
14543
  let workspaceProjects = [];
13955
14544
  let workspaceDir = "";
13956
14545
  if (args.workspace) {
13957
- const workspacePath = path23.resolve(args.workspace);
13958
- workspaceDir = path23.dirname(workspacePath);
14546
+ const workspacePath = path24.resolve(args.workspace);
14547
+ workspaceDir = path24.dirname(workspacePath);
13959
14548
  const complete = await requireCompleteWorkspaceClaims(workspacePath);
13960
14549
  workspaceProjects = complete.ws.projects;
13961
14550
  for (const d of complete.diagnostics) diag(` WARN ${d.id}: ${d.message}`);
@@ -13964,12 +14553,12 @@ async function runSalvage2(args) {
13964
14553
  const lockPaths = [
13965
14554
  .../* @__PURE__ */ new Set([
13966
14555
  ...workspaceLockPaths,
13967
- ...existsSync16(ownLock) ? [ownLock] : [],
13968
- ...args.claims.map((c) => path23.resolve(c))
14556
+ ...existsSync17(ownLock) ? [ownLock] : [],
14557
+ ...args.claims.map((c) => path24.resolve(c))
13969
14558
  ])
13970
14559
  ];
13971
14560
  diag(` claim set (${lockPaths.length} lockfile(s)):`);
13972
- for (const p of lockPaths) diag(` ${path23.relative(process.cwd(), p)}`);
14561
+ for (const p of lockPaths) diag(` ${path24.relative(process.cwd(), p)}`);
13973
14562
  if (!args.claims.length && !args.workspace) {
13974
14563
  diag(
13975
14564
  `
@@ -13977,7 +14566,7 @@ async function runSalvage2(args) {
13977
14566
  pass every other project's lockfile via --claims a.json,b.json, or
13978
14567
  register every project in a workspace catalog and pass --workspace.`
13979
14568
  );
13980
- } else if (args.workspace && !workspaceProjects.some((p) => resolveProject(workspaceDir, p).manifestPath === path23.resolve(args.manifest))) {
14569
+ } else if (args.workspace && !workspaceProjects.some((p) => resolveProject(workspaceDir, p).manifestPath === path24.resolve(args.manifest))) {
13981
14570
  diag(
13982
14571
  `
13983
14572
  This project's manifest is not registered in the workspace catalog. Its own
@@ -14096,7 +14685,7 @@ async function runSalvage2(args) {
14096
14685
  manifestPath: loaded.path,
14097
14686
  manifest: accountManifest,
14098
14687
  styleId,
14099
- importDir: path23.resolve(loaded.root, style.outDir),
14688
+ importDir: path24.resolve(loaded.root, style.outDir),
14100
14689
  lock,
14101
14690
  lockPath: args.lock
14102
14691
  },
@@ -14223,20 +14812,20 @@ async function runTag(args) {
14223
14812
  }
14224
14813
 
14225
14814
  // src/cli/commands/edit.ts
14226
- import path26 from "path";
14815
+ import path27 from "path";
14227
14816
 
14228
14817
  // src/pipeline/hand-edit.ts
14229
- import { existsSync as existsSync17 } from "fs";
14230
- import { copyFile, mkdir as mkdir9, readFile as readFile16, rename as rename8, rm as rm8, writeFile as writeFile11 } from "fs/promises";
14231
- import path25 from "path";
14818
+ import { existsSync as existsSync18 } from "fs";
14819
+ import { copyFile, mkdir as mkdir10, readFile as readFile17, rename as rename8, rm as rm8, writeFile as writeFile12 } from "fs/promises";
14820
+ import path26 from "path";
14232
14821
  import { z as z7 } from "zod";
14233
14822
 
14234
14823
  // src/manifest-edit.ts
14235
- import { readFile as readFile15, rename as rename7, rm as rm7, writeFile as writeFile10 } from "fs/promises";
14236
- import path24 from "path";
14824
+ import { readFile as readFile16, rename as rename7, rm as rm7, writeFile as writeFile11 } from "fs/promises";
14825
+ import path25 from "path";
14237
14826
  import { z as z6 } from "zod";
14238
14827
  var HexSha = z6.string().regex(/^[0-9a-f]{64}$/);
14239
- var ProjectFileSchema = z6.string().min(1).refine((file) => !path24.isAbsolute(file) && !file.split(/[\\/]/).includes(".."), "must be a path inside the project");
14828
+ var ProjectFileSchema = z6.string().min(1).refine((file) => !path25.isAbsolute(file) && !file.split(/[\\/]/).includes(".."), "must be a path inside the project");
14240
14829
  var AssetPatchSchema = z6.object({
14241
14830
  prompt: z6.string().optional(),
14242
14831
  /** Per-style override for the style being viewed; null removes it. */
@@ -14655,8 +15244,8 @@ function applyEdit(raw, edit) {
14655
15244
  }
14656
15245
  }
14657
15246
  async function withCandidate(manifestPath, edit, use, unchanged) {
14658
- const absolute = path24.resolve(manifestPath);
14659
- const original = await readFile15(absolute, "utf8");
15247
+ const absolute = path25.resolve(manifestPath);
15248
+ const original = await readFile16(absolute, "utf8");
14660
15249
  if (sha256(original) !== edit.expectedSha256) throw new ManifestDriftError(absolute);
14661
15250
  let raw;
14662
15251
  try {
@@ -14669,12 +15258,12 @@ async function withCandidate(manifestPath, edit, use, unchanged) {
14669
15258
  applyEdit(raw, edit);
14670
15259
  const next = serializeLike(original, raw);
14671
15260
  if (next === original) return unchanged(absolute, original);
14672
- const tmp = path24.join(
14673
- path24.dirname(absolute),
14674
- `.${path24.basename(absolute)}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`
15261
+ const tmp = path25.join(
15262
+ path25.dirname(absolute),
15263
+ `.${path25.basename(absolute)}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`
14675
15264
  );
14676
15265
  try {
14677
- await writeFile10(tmp, next);
15266
+ await writeFile11(tmp, next);
14678
15267
  let specs;
14679
15268
  try {
14680
15269
  specs = await resolveSpecs(await loadManifest(tmp));
@@ -14744,11 +15333,11 @@ async function priceManifestEdit(manifestPath, edit, opts = {}) {
14744
15333
  var HAND_EDIT_DIR = "edits";
14745
15334
  function handEditPath(loaded, spec) {
14746
15335
  const style = loaded.manifest.styles[spec.styleId];
14747
- const outDir = path25.resolve(loaded.root, style?.outDir ?? ".");
14748
- const relative = path25.relative(outDir, spec.outFile);
14749
- const inside = relative && !relative.startsWith("..") && !path25.isAbsolute(relative);
14750
- const file = inside ? relative : path25.basename(spec.outFile);
14751
- return path25.join(outDir, HAND_EDIT_DIR, file.replace(/\.gif$/i, ".png"));
15336
+ const outDir = path26.resolve(loaded.root, style?.outDir ?? ".");
15337
+ const relative = path26.relative(outDir, spec.outFile);
15338
+ const inside = relative && !relative.startsWith("..") && !path26.isAbsolute(relative);
15339
+ const file = inside ? relative : path26.basename(spec.outFile);
15340
+ return path26.join(outDir, HAND_EDIT_DIR, file.replace(/\.gif$/i, ".png"));
14752
15341
  }
14753
15342
  function handEditBases(spec, lock) {
14754
15343
  const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
@@ -14761,10 +15350,10 @@ function handEditBases(spec, lock) {
14761
15350
  }));
14762
15351
  if (members.some((member) => member.mediaType && member.mediaType !== "image/png")) return null;
14763
15352
  if (entry.outputs.length === 1 && !primaryOutput(entry)) return null;
14764
- if (!members.every((member) => existsSync17(member.path))) return null;
15353
+ if (!members.every((member) => existsSync18(member.path))) return null;
14765
15354
  return members.map(({ role, path: file }) => ({ role, path: file }));
14766
15355
  }
14767
- return existsSync17(spec.outFile) ? [{ role: null, path: spec.outFile }] : null;
15356
+ return existsSync18(spec.outFile) ? [{ role: null, path: spec.outFile }] : null;
14768
15357
  }
14769
15358
  function handEditMembers(loaded, spec, lock, editPath) {
14770
15359
  const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
@@ -14790,18 +15379,18 @@ async function startHandEdit(loaded, lock, spec, opts = {}) {
14790
15379
  `${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`
14791
15380
  );
14792
15381
  }
14793
- const existing = spec.source ? path25.resolve(loaded.root, spec.source) : null;
15382
+ const existing = spec.source ? path26.resolve(loaded.root, spec.source) : null;
14794
15383
  const editPath = existing ?? handEditPath(loaded, spec);
14795
15384
  const members = handEditMembers(loaded, spec, lock, editPath);
14796
15385
  let created = false;
14797
15386
  for (const member of members) {
14798
- if (existsSync17(member.path)) continue;
15387
+ if (existsSync18(member.path)) continue;
14799
15388
  if (!member.base) {
14800
15389
  throw new ManifestEditError(
14801
15390
  `${spec.styleId}/${spec.assetId} has no generated PNG on disk to start from; generate or restore it first`
14802
15391
  );
14803
15392
  }
14804
- await mkdir9(path25.dirname(member.path), { recursive: true });
15393
+ await mkdir10(path26.dirname(member.path), { recursive: true });
14805
15394
  await copyFile(member.base, member.path);
14806
15395
  created = true;
14807
15396
  }
@@ -14880,7 +15469,7 @@ function handEditProjectPath(editPath) {
14880
15469
  async function readHandEditCompanion(editPath) {
14881
15470
  let text;
14882
15471
  try {
14883
- text = await readFile16(handEditCompanionPath(editPath), "utf8");
15472
+ text = await readFile17(handEditCompanionPath(editPath), "utf8");
14884
15473
  } catch {
14885
15474
  return null;
14886
15475
  }
@@ -14923,7 +15512,7 @@ async function saveHandEdit(loaded, lock, spec, input) {
14923
15512
  }
14924
15513
  });
14925
15514
  const bases = handEditBases(spec, lock);
14926
- const expectedSize = bases?.[0] ? decodePng(await readFile16(bases[0].path)) : { width: spec.width, height: spec.height };
15515
+ const expectedSize = bases?.[0] ? decodePng(await readFile17(bases[0].path)) : { width: spec.width, height: spec.height };
14927
15516
  for (const [index, image] of decoded.entries()) {
14928
15517
  if (image.width !== expectedSize.width || image.height !== expectedSize.height) {
14929
15518
  const which = memberSet ? `member ${incoming[index].role ?? index}` : "the edit";
@@ -14939,7 +15528,7 @@ async function saveHandEdit(loaded, lock, spec, input) {
14939
15528
  editor: input.editor,
14940
15529
  protocol: input.protocol,
14941
15530
  savedAt: (input.now ?? /* @__PURE__ */ new Date()).toISOString(),
14942
- project: projectPath ? path25.basename(projectPath) : null,
15531
+ project: projectPath ? path26.basename(projectPath) : null,
14943
15532
  outputs: incoming.map((frame, index) => ({
14944
15533
  role: frame.role,
14945
15534
  basedOn: basedOnHash(entry, index),
@@ -14969,7 +15558,7 @@ function basedOnHash(entry, index) {
14969
15558
  async function replaceFile(file, bytes) {
14970
15559
  const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
14971
15560
  try {
14972
- await writeFile11(tmp, bytes);
15561
+ await writeFile12(tmp, bytes);
14973
15562
  await rename8(tmp, file);
14974
15563
  } finally {
14975
15564
  await rm8(tmp, { force: true });
@@ -14999,7 +15588,7 @@ async function runEdit(args) {
14999
15588
  const set = started.members.length > 1;
15000
15589
  const kind = spec.generator === "frames" ? "frames" : "members";
15001
15590
  log(` ${started.created ? "created" : "found"} ${started.source}${set ? ` (${started.members.length} ${kind})` : ""}${started.declared ? " and declared it as the asset's source" : ""}`);
15002
- if (set) for (const member of started.members) log(` ${member.role ?? ""} ${path26.relative(process.cwd(), member.path)}`);
15591
+ if (set) for (const member of started.members) log(` ${member.role ?? ""} ${path27.relative(process.cwd(), member.path)}`);
15003
15592
  if (args.noOpen) {
15004
15593
  log(` edit ${set ? "them" : "it"} with your own tool, then run pixelkiln plan; mount and pack place ${set ? "them" : "it"} in place of the generated art`);
15005
15594
  } else {
@@ -15009,24 +15598,24 @@ async function runEdit(args) {
15009
15598
  }
15010
15599
 
15011
15600
  // src/cli/commands/gallery.ts
15012
- import path36 from "path";
15013
- import { existsSync as existsSync27 } from "fs";
15601
+ import path37 from "path";
15602
+ import { existsSync as existsSync28 } from "fs";
15014
15603
 
15015
15604
  // src/gallery/snapshot.ts
15016
15605
  import { createHash as createHash4 } from "crypto";
15017
- import { existsSync as existsSync22 } from "fs";
15018
- import { readFile as readFile20, stat as stat2 } from "fs/promises";
15019
- import path32 from "path";
15606
+ import { existsSync as existsSync23 } from "fs";
15607
+ import { readFile as readFile21, stat as stat2 } from "fs/promises";
15608
+ import path33 from "path";
15020
15609
 
15021
15610
  // src/gallery/edit.ts
15022
- import { readFile as readFile19, rename as rename9, writeFile as writeFile15 } from "fs/promises";
15023
- import path30 from "path";
15611
+ import { readFile as readFile20, rename as rename9, writeFile as writeFile16 } from "fs/promises";
15612
+ import path31 from "path";
15024
15613
  import { z as z11 } from "zod";
15025
15614
 
15026
15615
  // src/gallery/upload.ts
15027
- import { existsSync as existsSync18 } from "fs";
15028
- import { mkdir as mkdir10, writeFile as writeFile12 } from "fs/promises";
15029
- import path27 from "path";
15616
+ import { existsSync as existsSync19 } from "fs";
15617
+ import { mkdir as mkdir11, writeFile as writeFile13 } from "fs/promises";
15618
+ import path28 from "path";
15030
15619
  import { z as z8 } from "zod";
15031
15620
  var MAX_UPLOAD_BYTES = 8 * 1024 * 1024;
15032
15621
  var UploadImageSchema = z8.object({
@@ -15039,12 +15628,12 @@ var UploadImageSchema = z8.object({
15039
15628
  overwrite: z8.boolean().optional()
15040
15629
  }).strict();
15041
15630
  async function saveProjectImage(root, request) {
15042
- const file = path27.resolve(root, request.path);
15043
- const relative = path27.relative(root, file);
15044
- if (relative.startsWith("..") || path27.isAbsolute(relative)) {
15631
+ const file = path28.resolve(root, request.path);
15632
+ const relative = path28.relative(root, file);
15633
+ if (relative.startsWith("..") || path28.isAbsolute(relative)) {
15045
15634
  throw new ManifestEditError("an upload must stay inside the project");
15046
15635
  }
15047
- const extension = path27.extname(file).toLowerCase();
15636
+ const extension = path28.extname(file).toLowerCase();
15048
15637
  const expected = extension === ".png" ? "png" : extension === ".jpg" || extension === ".jpeg" ? "jpeg" : null;
15049
15638
  if (!expected) throw new ManifestEditError("an upload must be a .png, .jpg, or .jpeg path");
15050
15639
  const bytes = Buffer.from(request.base64, "base64");
@@ -15056,18 +15645,18 @@ async function saveProjectImage(root, request) {
15056
15645
  if (metadata.format !== expected) {
15057
15646
  throw new ManifestEditError(`the upload is a ${metadata.format.toUpperCase()}; name it ${metadata.format === "png" ? ".png" : ".jpg"}`);
15058
15647
  }
15059
- if (existsSync18(file) && !request.overwrite) {
15060
- throw new ManifestEditError(`${relative.split(path27.sep).join("/")} already exists; choose another name, or replace it`);
15648
+ if (existsSync19(file) && !request.overwrite) {
15649
+ throw new ManifestEditError(`${relative.split(path28.sep).join("/")} already exists; choose another name, or replace it`);
15061
15650
  }
15062
- await mkdir10(path27.dirname(file), { recursive: true });
15063
- await writeFile12(file, bytes);
15064
- return relative.split(path27.sep).join("/");
15651
+ await mkdir11(path28.dirname(file), { recursive: true });
15652
+ await writeFile13(file, bytes);
15653
+ return relative.split(path28.sep).join("/");
15065
15654
  }
15066
15655
 
15067
15656
  // src/gallery/image-revision.ts
15068
- import { existsSync as existsSync19 } from "fs";
15069
- import { readFile as readFile17, rm as rm9, writeFile as writeFile13 } from "fs/promises";
15070
- import path28 from "path";
15657
+ import { existsSync as existsSync20 } from "fs";
15658
+ import { readFile as readFile18, rm as rm9, writeFile as writeFile14 } from "fs/promises";
15659
+ import path29 from "path";
15071
15660
  import { z as z9 } from "zod";
15072
15661
  var InpaintRevisionRequestSchema = z9.object({
15073
15662
  action: z9.literal("create-inpaint-revision"),
@@ -15086,15 +15675,15 @@ var InpaintRevisionRequestSchema = z9.object({
15086
15675
  overwrite: z9.boolean().optional()
15087
15676
  }).strict();
15088
15677
  async function createInpaintRevision(manifestPath, request) {
15089
- const root = path28.dirname(path28.resolve(manifestPath));
15090
- if (path28.extname(request.maskPath).toLowerCase() !== ".png") {
15678
+ const root = path29.dirname(path29.resolve(manifestPath));
15679
+ if (path29.extname(request.maskPath).toLowerCase() !== ".png") {
15091
15680
  throw new ManifestEditError("the mask must be a .png path inside the project");
15092
15681
  }
15093
15682
  const bytes = Buffer.from(request.maskBase64, "base64");
15094
15683
  const metadata = imageMetadata(bytes);
15095
15684
  if (!metadata || metadata.format !== "png") throw new ManifestEditError("the mask is not a readable PNG");
15096
- const file = path28.resolve(root, request.maskPath);
15097
- const previous = existsSync19(file) ? await readFile17(file) : null;
15685
+ const file = path29.resolve(root, request.maskPath);
15686
+ const previous = existsSync20(file) ? await readFile18(file) : null;
15098
15687
  const mask = await saveProjectImage(root, {
15099
15688
  action: "upload-image",
15100
15689
  path: request.maskPath,
@@ -15113,7 +15702,7 @@ async function createInpaintRevision(manifestPath, request) {
15113
15702
  }
15114
15703
  });
15115
15704
  } catch (error) {
15116
- if (previous) await writeFile13(file, previous);
15705
+ if (previous) await writeFile14(file, previous);
15117
15706
  else await rm9(file, { force: true });
15118
15707
  throw error;
15119
15708
  }
@@ -15121,9 +15710,9 @@ async function createInpaintRevision(manifestPath, request) {
15121
15710
  }
15122
15711
 
15123
15712
  // src/gallery/skeleton.ts
15124
- import { existsSync as existsSync20 } from "fs";
15125
- import { mkdir as mkdir11, readFile as readFile18, rm as rm10, writeFile as writeFile14 } from "fs/promises";
15126
- import path29 from "path";
15713
+ import { existsSync as existsSync21 } from "fs";
15714
+ import { mkdir as mkdir12, readFile as readFile19, rm as rm10, writeFile as writeFile15 } from "fs/promises";
15715
+ import path30 from "path";
15127
15716
  import { z as z10 } from "zod";
15128
15717
  var SkeletonEstimateRequestSchema = z10.object({
15129
15718
  project: z10.string().min(1).optional(),
@@ -15162,8 +15751,8 @@ var SkeletonKeypointsUpdateSchema = z10.object({
15162
15751
  }).strict();
15163
15752
  var DEFAULT_ESTIMATE_LIMIT = 10;
15164
15753
  function skeletonSourceFile(spec) {
15165
- if (spec.source) return path29.resolve(spec.root, spec.source);
15166
- if (spec.quality && existsSync20(spec.quality.outFile)) return spec.quality.outFile;
15754
+ if (spec.source) return path30.resolve(spec.root, spec.source);
15755
+ if (spec.quality && existsSync21(spec.quality.outFile)) return spec.quality.outFile;
15167
15756
  return spec.outFile;
15168
15757
  }
15169
15758
  function createGallerySkeletonHandlers(opts) {
@@ -15184,10 +15773,10 @@ function createGallerySkeletonHandlers(opts) {
15184
15773
  const spec = ctx.specs.find((candidate) => lockKey(candidate.styleId, candidate.assetId) === key);
15185
15774
  if (!spec) throw new ManifestEditError(`"${key}" is not declared by the manifest`);
15186
15775
  const file = skeletonSourceFile(spec);
15187
- if (!existsSync20(file)) throw new ManifestEditError(`${key} has no image on disk to estimate from`);
15188
- const bytes = await readFile18(file);
15776
+ if (!existsSync21(file)) throw new ManifestEditError(`${key} has no image on disk to estimate from`);
15777
+ const bytes = await readFile19(file);
15189
15778
  const metadata = imageMetadata(bytes);
15190
- if (!metadata) throw new ManifestEditError(`${path29.basename(file)} is not a readable PNG or JPEG`);
15779
+ if (!metadata) throw new ManifestEditError(`${path30.basename(file)} is not a readable PNG or JPEG`);
15191
15780
  const problem = estimateSkeletonSizeProblem(metadata.width, metadata.height);
15192
15781
  if (problem) throw new ManifestEditError(`${key} is ${problem}`);
15193
15782
  if (used >= limit) {
@@ -15204,7 +15793,7 @@ function createGallerySkeletonHandlers(opts) {
15204
15793
  log2(` estimated a skeleton for ${key} (PixelLab estimate-skeleton, ${used}/${limit} this session${billed ? `, billed ${billed.amount} ${billed.unit}` : ""})`);
15205
15794
  return {
15206
15795
  set: scaffoldSkeletonSet(res.keypoints, request.frames ?? DEFAULT_SCAFFOLD_FRAMES),
15207
- source: path29.relative(ctx.loaded.root, file).split(path29.sep).join("/"),
15796
+ source: path30.relative(ctx.loaded.root, file).split(path30.sep).join("/"),
15208
15797
  session: status()
15209
15798
  };
15210
15799
  }
@@ -15218,21 +15807,21 @@ function billedAmount(usage) {
15218
15807
  return typeof amount === "number" && Number.isFinite(amount) ? { unit: type, amount } : null;
15219
15808
  }
15220
15809
  async function createSkeletonAnimation(manifestPath, request) {
15221
- const root = path29.dirname(path29.resolve(manifestPath));
15222
- const file = path29.resolve(root, request.keypointsFile);
15223
- const relative = path29.relative(root, file);
15224
- if (!request.keypointsFile.endsWith(".json") || relative.startsWith("..") || path29.isAbsolute(relative)) {
15810
+ const root = path30.dirname(path30.resolve(manifestPath));
15811
+ const file = path30.resolve(root, request.keypointsFile);
15812
+ const relative = path30.relative(root, file);
15813
+ if (!request.keypointsFile.endsWith(".json") || relative.startsWith("..") || path30.isAbsolute(relative)) {
15225
15814
  throw new ManifestEditError("the keypoints file must be a .json path inside the project");
15226
15815
  }
15227
- const existed = existsSync20(file);
15816
+ const existed = existsSync21(file);
15228
15817
  let previous = null;
15229
15818
  if (request.set) {
15230
15819
  if (existed && !request.overwrite) {
15231
15820
  throw new ManifestEditError(`${request.keypointsFile} already exists; choose another path, or replace it`);
15232
15821
  }
15233
- if (existed) previous = await readFile18(file);
15234
- await mkdir11(path29.dirname(file), { recursive: true });
15235
- await writeFile14(file, JSON.stringify(request.set, null, 2) + "\n");
15822
+ if (existed) previous = await readFile19(file);
15823
+ await mkdir12(path30.dirname(file), { recursive: true });
15824
+ await writeFile15(file, JSON.stringify(request.set, null, 2) + "\n");
15236
15825
  } else if (!existed) {
15237
15826
  throw new ManifestEditError(`${request.keypointsFile} does not exist; paste poses or estimate them from the sprite`);
15238
15827
  }
@@ -15247,7 +15836,7 @@ async function createSkeletonAnimation(manifestPath, request) {
15247
15836
  revision: {
15248
15837
  mode: "animate-skeleton",
15249
15838
  from: request.from,
15250
- keypointsFile: relative.split(path29.sep).join("/"),
15839
+ keypointsFile: relative.split(path30.sep).join("/"),
15251
15840
  direction: request.direction,
15252
15841
  ...request.description ? { description: request.description } : {},
15253
15842
  ...request.skeletonTemplate ? { skeletonTemplate: request.skeletonTemplate } : {}
@@ -15256,7 +15845,7 @@ async function createSkeletonAnimation(manifestPath, request) {
15256
15845
  });
15257
15846
  } catch (error) {
15258
15847
  if (request.set) {
15259
- if (previous) await writeFile14(file, previous);
15848
+ if (previous) await writeFile15(file, previous);
15260
15849
  else await rm10(file, { force: true });
15261
15850
  }
15262
15851
  throw error;
@@ -15268,19 +15857,19 @@ async function updateSkeletonKeypoints(ctx, request) {
15268
15857
  if (!spec) throw new ManifestEditError(`"${key}" is not declared by the manifest`);
15269
15858
  const file = spec.revision?.mode === "animate-skeleton" ? spec.revision.keypointsFile : void 0;
15270
15859
  if (!file) throw new ManifestEditError(`${key} is not an animate-skeleton revision`);
15271
- const relative = path29.relative(ctx.loaded.root, file);
15272
- if (relative.startsWith("..") || path29.isAbsolute(relative)) {
15860
+ const relative = path30.relative(ctx.loaded.root, file);
15861
+ if (relative.startsWith("..") || path30.isAbsolute(relative)) {
15273
15862
  throw new ManifestEditError(`${key}'s keypoints file is outside the project; edit it by hand`);
15274
15863
  }
15275
- const current = existsSync20(file) ? sha256(await readFile18(file)) : null;
15864
+ const current = existsSync21(file) ? sha256(await readFile19(file)) : null;
15276
15865
  if (current !== request.expectedSha256) {
15277
15866
  throw Object.assign(
15278
- new Error(`${relative.split(path29.sep).join("/")} changed on disk since this page loaded it; press Refresh and edit again`),
15867
+ new Error(`${relative.split(path30.sep).join("/")} changed on disk since this page loaded it; press Refresh and edit again`),
15279
15868
  { status: 409 }
15280
15869
  );
15281
15870
  }
15282
- await writeFile14(file, JSON.stringify(request.set, null, 2) + "\n");
15283
- return relative.split(path29.sep).join("/");
15871
+ await writeFile15(file, JSON.stringify(request.set, null, 2) + "\n");
15872
+ return relative.split(path30.sep).join("/");
15284
15873
  }
15285
15874
 
15286
15875
  // src/gallery/edit.ts
@@ -15334,7 +15923,7 @@ function createGalleryEditHandler(opts) {
15334
15923
  const history = /* @__PURE__ */ new Map();
15335
15924
  const manifestOf = async (project) => {
15336
15925
  try {
15337
- return path30.resolve(await opts.manifestFor(typeof project === "string" ? project : void 0));
15926
+ return path31.resolve(await opts.manifestFor(typeof project === "string" ? project : void 0));
15338
15927
  } catch {
15339
15928
  return null;
15340
15929
  }
@@ -15344,7 +15933,7 @@ function createGalleryEditHandler(opts) {
15344
15933
  if (undo.success) {
15345
15934
  const manifestPath2 = await manifestOf(undo.data.project);
15346
15935
  if (!manifestPath2) throw new ManifestEditError("no manifest to undo an edit in");
15347
- const current = await readFile19(manifestPath2, "utf8");
15936
+ const current = await readFile20(manifestPath2, "utf8");
15348
15937
  if (sha256(current) !== undo.data.expectedSha256) throw new ManifestDriftError(manifestPath2);
15349
15938
  const stack = history.get(manifestPath2) ?? [];
15350
15939
  const last = stack[stack.length - 1];
@@ -15354,17 +15943,17 @@ function createGalleryEditHandler(opts) {
15354
15943
  throw Object.assign(new Error("the manifest changed outside the gallery since its last edit; undo would discard that change"), { status: 409 });
15355
15944
  }
15356
15945
  const tmp = manifestPath2 + ".pixelkiln-undo";
15357
- await writeFile15(tmp, last.before);
15946
+ await writeFile16(tmp, last.before);
15358
15947
  await rename9(tmp, manifestPath2);
15359
15948
  stack.pop();
15360
- log2(` manifest edit undone in ${path30.relative(process.cwd(), manifestPath2) || manifestPath2}`);
15949
+ log2(` manifest edit undone in ${path31.relative(process.cwd(), manifestPath2) || manifestPath2}`);
15361
15950
  return opts.reload();
15362
15951
  }
15363
15952
  const manifestPath = await manifestOf(body?.project);
15364
- const before = manifestPath ? await readFile19(manifestPath, "utf8").catch(() => null) : null;
15953
+ const before = manifestPath ? await readFile20(manifestPath, "utf8").catch(() => null) : null;
15365
15954
  const build = await apply(body);
15366
15955
  if (manifestPath && before !== null) {
15367
- const after = await readFile19(manifestPath, "utf8").catch(() => null);
15956
+ const after = await readFile20(manifestPath, "utf8").catch(() => null);
15368
15957
  if (after !== null && after !== before) {
15369
15958
  const stack = history.get(manifestPath) ?? [];
15370
15959
  stack.push({ before, afterSha: sha256(after) });
@@ -15419,7 +16008,7 @@ function createEditApplier(opts, log2) {
15419
16008
  } catch (error) {
15420
16009
  throw new ManifestEditError(error instanceof Error ? error.message : String(error));
15421
16010
  }
15422
- const written = await saveProjectImage(path30.dirname(path30.resolve(manifestPath2)), upload.data);
16011
+ const written = await saveProjectImage(path31.dirname(path31.resolve(manifestPath2)), upload.data);
15423
16012
  log2(` image added: ${written}`);
15424
16013
  return opts.reload();
15425
16014
  }
@@ -15486,7 +16075,7 @@ function createEditApplier(opts, log2) {
15486
16075
  }
15487
16076
  const result = await applyManifestEdit(manifestPath, parsed.data);
15488
16077
  if (result.changed) {
15489
- const relative = path30.relative(process.cwd(), result.manifestPath);
16078
+ const relative = path31.relative(process.cwd(), result.manifestPath);
15490
16079
  const shown = !relative || relative.startsWith("..") ? result.manifestPath : relative;
15491
16080
  log2(` manifest edited: ${describeEdit(parsed.data)} in ${shown}`);
15492
16081
  }
@@ -15537,8 +16126,8 @@ function createGalleryPriceHandler(opts) {
15537
16126
  }
15538
16127
 
15539
16128
  // src/pipeline/history.ts
15540
- import { existsSync as existsSync21 } from "fs";
15541
- import path31 from "path";
16129
+ import { existsSync as existsSync22 } from "fs";
16130
+ import path32 from "path";
15542
16131
  var HISTORY_ENV = "PIXELKILN_HISTORY";
15543
16132
  function historyLimit(manifest, env = process.env) {
15544
16133
  if (manifest?.history !== void 0) return manifest.history;
@@ -15625,10 +16214,10 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
15625
16214
  }
15626
16215
  for (let i = 0; i < entry.outputs.length; i++) {
15627
16216
  const file = currentEntryOutputPath(entry, spec, i);
15628
- if (!existsSync21(file)) continue;
16217
+ if (!existsSync22(file)) continue;
15629
16218
  if (await sha256File(file) !== entry.outputs[i].sha256 && !opts.force) {
15630
16219
  throw new OverwriteRefusedError(
15631
- `${path31.relative(process.cwd(), file)} was changed after download; keep it with pixelkiln edit, or pass --force to replace it`,
16220
+ `${path32.relative(process.cwd(), file)} was changed after download; keep it with pixelkiln edit, or pass --force to replace it`,
15632
16221
  [file]
15633
16222
  );
15634
16223
  }
@@ -15681,15 +16270,20 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
15681
16270
  // src/gallery/snapshot.ts
15682
16271
  var PROJECT_FILE_TYPE = "application/octet-stream";
15683
16272
  function galleryMediaId(absolutePath) {
15684
- return createHash4("sha256").update(path32.resolve(absolutePath)).digest("hex").slice(0, 24);
16273
+ return createHash4("sha256").update(path33.resolve(absolutePath)).digest("hex").slice(0, 24);
15685
16274
  }
15686
16275
  function galleryMediaRoute(id) {
15687
16276
  return `/media/${id}`;
15688
16277
  }
16278
+ function renderedSize(entry) {
16279
+ const vfx = entry?.providerMetadata?.[entry.provider]?.vfx;
16280
+ return typeof vfx?.width === "number" && typeof vfx.height === "number" ? { width: vfx.width, height: vfx.height } : null;
16281
+ }
15689
16282
  function isDeclared(loaded, styleId, assetId) {
15690
16283
  const asset = loaded.manifest.assets[assetId];
15691
- if (!asset || !loaded.manifest.styles[styleId]) return false;
15692
- return asset.styles.length === 0 || asset.styles.includes(styleId);
16284
+ const style = loaded.manifest.styles[styleId];
16285
+ if (!asset || !style) return false;
16286
+ return assetInStyle(asset, styleId, style);
15693
16287
  }
15694
16288
  function matchesFilter(filter, styleId, assetId) {
15695
16289
  if (filter?.styles?.length && !filter.styles.includes(styleId)) return false;
@@ -15700,9 +16294,9 @@ function describeHistory(media, entry, cacheDir) {
15700
16294
  return (entry.history ?? []).map((generation, i) => {
15701
16295
  const outputs = generation.outputs.map((output) => {
15702
16296
  const mediaType = output.mediaType ?? MediaType.PNG;
15703
- const file = path32.join(cacheDir, cacheFileName(output.sha256, mediaType));
16297
+ const file = path33.join(cacheDir, cacheFileName(output.sha256, mediaType));
15704
16298
  let url = null;
15705
- if (existsSync22(file)) {
16299
+ if (existsSync23(file)) {
15706
16300
  const id = galleryMediaId(file);
15707
16301
  media.set(id, { path: file, contentType: mediaType });
15708
16302
  url = `${galleryMediaRoute(id)}?v=${output.sha256.slice(0, 24)}`;
@@ -15734,7 +16328,7 @@ function describeHistory(media, entry, cacheDir) {
15734
16328
  var MAX_COMPARED_BYTES = 8 * 1024 * 1024;
15735
16329
  async function samePixels(a, b) {
15736
16330
  try {
15737
- const [left, right] = await Promise.all([readFile20(a), readFile20(b)]);
16331
+ const [left, right] = await Promise.all([readFile21(a), readFile21(b)]);
15738
16332
  if (left.length > MAX_COMPARED_BYTES || right.length > MAX_COMPARED_BYTES) return false;
15739
16333
  const x = decodePng(left);
15740
16334
  const y = decodePng(right);
@@ -15832,10 +16426,10 @@ async function describeOutput(media, root, absolutePath, recorded = {}) {
15832
16426
  async function describeQuality(media, root, inspection) {
15833
16427
  const outputPaths = inspection.outputs ?? [inspection.output];
15834
16428
  const outputs = await Promise.all(
15835
- outputPaths.map((file) => describeOutput(media, root, path32.resolve(root, file)))
16429
+ outputPaths.map((file) => describeOutput(media, root, path33.resolve(root, file)))
15836
16430
  );
15837
- const recordPath = path32.resolve(root, inspection.record);
15838
- const recordExists = existsSync22(recordPath);
16431
+ const recordPath = path33.resolve(root, inspection.record);
16432
+ const recordExists = existsSync23(recordPath);
15839
16433
  let options = null;
15840
16434
  let check = null;
15841
16435
  if (recordExists) {
@@ -15854,7 +16448,7 @@ async function describeQuality(media, root, inspection) {
15854
16448
  return {
15855
16449
  state: inspection.state,
15856
16450
  reason: inspection.reason,
15857
- output: portableOutputPath(path32.resolve(root, inspection.output), root),
16451
+ output: portableOutputPath(path33.resolve(root, inspection.output), root),
15858
16452
  record: portableOutputPath(recordPath, root),
15859
16453
  recordExists,
15860
16454
  outputs,
@@ -15892,9 +16486,9 @@ async function buildGallerySnapshot(opts) {
15892
16486
  const media = /* @__PURE__ */ new Map();
15893
16487
  const plan = await buildPlan(specs, lock);
15894
16488
  const items = [];
15895
- const manifestText = await readFile20(loaded.path, "utf8");
16489
+ const manifestText = await readFile21(loaded.path, "utf8");
15896
16490
  const manifestSha256 = sha256(manifestText);
15897
- const cacheDir = path32.resolve(path32.dirname(opts.lockPath), ".pixelkiln", "cache");
16491
+ const cacheDir = path33.resolve(path33.dirname(opts.lockPath), ".pixelkiln", "cache");
15898
16492
  let rawStyles = {};
15899
16493
  try {
15900
16494
  const raw = JSON.parse(manifestText);
@@ -15918,12 +16512,12 @@ async function buildGallerySnapshot(opts) {
15918
16512
  )
15919
16513
  );
15920
16514
  if (spec.source) {
15921
- const editPath = path32.resolve(root, spec.source);
16515
+ const editPath = path33.resolve(root, spec.source);
15922
16516
  const memberCount = sourceIsStem(spec.source, entry, root) ? entry.outputs.length : 1;
15923
16517
  const editShas = await Promise.all(
15924
16518
  Array.from({ length: memberCount }, async (_, index) => {
15925
16519
  const file = sourceOutputPath(spec.source, entry, index, root);
15926
- return { file, sha256: existsSync22(file) ? await sha256File(file) : null };
16520
+ return { file, sha256: existsSync23(file) ? await sha256File(file) : null };
15927
16521
  })
15928
16522
  );
15929
16523
  edits = await Promise.all(editShas.map(({ file, sha256: editSha }) => describeOutput(media, root, file, { sha256: editSha })));
@@ -15931,7 +16525,7 @@ async function buildGallerySnapshot(opts) {
15931
16525
  const companion = await readHandEditCompanion(editPath);
15932
16526
  if (companion) {
15933
16527
  const projectPath = handEditProjectPath(editPath);
15934
- const project = companion.project && existsSync22(projectPath) ? await fileInfo(projectPath) : null;
16528
+ const project = companion.project && existsSync23(projectPath) ? await fileInfo(projectPath) : null;
15935
16529
  let projectUrl = null;
15936
16530
  if (project) {
15937
16531
  const id = galleryMediaId(projectPath);
@@ -15967,8 +16561,8 @@ async function buildGallerySnapshot(opts) {
15967
16561
  editStatus = edits.some((member) => !member.exists) ? "missing" : editChanged.every((changed) => !changed) ? "same" : regenerated ? "regenerated-since" : "edited";
15968
16562
  }
15969
16563
  } else if (spec.source) {
15970
- outputs = [await describeOutput(media, root, path32.resolve(root, spec.source))];
15971
- } else if (!entry && existsSync22(spec.outFile)) {
16564
+ outputs = [await describeOutput(media, root, path33.resolve(root, spec.source))];
16565
+ } else if (!entry && existsSync23(spec.outFile)) {
15972
16566
  outputs = [await describeOutput(media, root, spec.outFile)];
15973
16567
  } else {
15974
16568
  outputs = [];
@@ -15989,8 +16583,8 @@ async function buildGallerySnapshot(opts) {
15989
16583
  generator: entry?.generator ?? spec.generator,
15990
16584
  prompt: entry?.prompt ?? spec.prompt,
15991
16585
  currentPrompt: entry && entry.prompt !== spec.prompt ? spec.prompt : null,
15992
- width: entry?.width ?? spec.width,
15993
- height: entry?.height ?? spec.height,
16586
+ width: renderedSize(entry)?.width ?? entry?.width ?? spec.width,
16587
+ height: renderedSize(entry)?.height ?? entry?.height ?? spec.height,
15994
16588
  tileFeature: entry?.tileFeature ?? spec.tileFeature ?? null,
15995
16589
  fps: quality?.frameSet?.fps ?? metadataFps(entry) ?? spec.quality?.fps ?? null,
15996
16590
  cost: entry?.cost ?? 0,
@@ -16171,7 +16765,7 @@ async function buildGallerySnapshot(opts) {
16171
16765
  id: loaded.manifest.name,
16172
16766
  name: loaded.manifest.name,
16173
16767
  manifest: loaded.path,
16174
- lock: path32.resolve(opts.lockPath),
16768
+ lock: path33.resolve(opts.lockPath),
16175
16769
  root,
16176
16770
  provider: loaded.manifest.provider,
16177
16771
  account: null,
@@ -16197,7 +16791,7 @@ async function buildGallerySnapshot(opts) {
16197
16791
  return { snapshot, media };
16198
16792
  }
16199
16793
  async function buildWorkspaceGallerySnapshot(opts) {
16200
- const dir = path32.dirname(path32.resolve(opts.workspacePath));
16794
+ const dir = path33.dirname(path33.resolve(opts.workspacePath));
16201
16795
  const media = /* @__PURE__ */ new Map();
16202
16796
  const projects = [];
16203
16797
  const items = [];
@@ -16240,11 +16834,11 @@ async function buildWorkspaceGallerySnapshot(opts) {
16240
16834
  id: project.id,
16241
16835
  name: loaded.manifest.name,
16242
16836
  manifest: manifestPath,
16243
- lock: path32.resolve(lockPath),
16837
+ lock: path33.resolve(lockPath),
16244
16838
  root: loaded.root,
16245
16839
  provider: loaded.manifest.provider,
16246
16840
  account: project.account ?? null,
16247
- manifestSha256: manifestSha256 ?? sha256(await readFile20(manifestPath, "utf8")),
16841
+ manifestSha256: manifestSha256 ?? sha256(await readFile21(manifestPath, "utf8")),
16248
16842
  historyLimit: historyLimit(loaded.manifest),
16249
16843
  entries: Object.keys(lock.entries).length,
16250
16844
  items: projectItems.length,
@@ -16256,8 +16850,8 @@ async function buildWorkspaceGallerySnapshot(opts) {
16256
16850
  id: project.id,
16257
16851
  name: project.id,
16258
16852
  manifest: manifestPath,
16259
- lock: path32.resolve(lockPath),
16260
- root: path32.dirname(manifestPath),
16853
+ lock: path33.resolve(lockPath),
16854
+ root: path33.dirname(manifestPath),
16261
16855
  provider: project.provider,
16262
16856
  account: project.account ?? null,
16263
16857
  manifestSha256: null,
@@ -16284,7 +16878,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
16284
16878
  version: 1,
16285
16879
  generatedAt: (opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
16286
16880
  project: null,
16287
- workspace: { path: path32.resolve(opts.workspacePath), dir, projects },
16881
+ workspace: { path: path33.resolve(opts.workspacePath), dir, projects },
16288
16882
  filter,
16289
16883
  totals: {
16290
16884
  entries: projects.reduce((sum, project) => sum + project.entries, 0),
@@ -16303,10 +16897,10 @@ async function buildWorkspaceGallerySnapshot(opts) {
16303
16897
  // src/gallery/server.ts
16304
16898
  import { randomBytes } from "crypto";
16305
16899
  import { createServer as createServer2 } from "http";
16306
- import { readFile as readFile21 } from "fs/promises";
16900
+ import { readFile as readFile22 } from "fs/promises";
16307
16901
 
16308
16902
  // src/gallery/page.ts
16309
- import { existsSync as existsSync23, readFileSync as readFileSync5 } from "fs";
16903
+ import { existsSync as existsSync24, readFileSync as readFileSync5 } from "fs";
16310
16904
 
16311
16905
  // src/gallery/client-bundle.ts
16312
16906
  import { createRequire } from "module";
@@ -16347,7 +16941,7 @@ function clientAssets() {
16347
16941
  const prebuilt = new URL("gallery.js", CLIENT_DIR);
16348
16942
  client ??= {
16349
16943
  css: readFileSync5(new URL("gallery.css", CLIENT_DIR), "utf8"),
16350
- js: existsSync23(prebuilt) ? readFileSync5(prebuilt, "utf8") : bundleGalleryClient()
16944
+ js: existsSync24(prebuilt) ? readFileSync5(prebuilt, "utf8") : bundleGalleryClient()
16351
16945
  };
16352
16946
  return client;
16353
16947
  }
@@ -16618,7 +17212,7 @@ async function serveGallery(opts) {
16618
17212
  const asset = reviewAssets.get(jobId)?.get(url2.pathname);
16619
17213
  if (!asset) return fail(404, "no such review asset");
16620
17214
  try {
16621
- const bytes = await readFile21(asset.path);
17215
+ const bytes = await readFile22(asset.path);
16622
17216
  res.writeHead(200, {
16623
17217
  "Content-Type": asset.contentType,
16624
17218
  "Cache-Control": "no-store",
@@ -16701,7 +17295,7 @@ async function serveGallery(opts) {
16701
17295
  if (!asset) return fail(404, "no such gallery media");
16702
17296
  let bytes;
16703
17297
  try {
16704
- bytes = await readFile21(asset.path);
17298
+ bytes = await readFile22(asset.path);
16705
17299
  } catch {
16706
17300
  return fail(404, "gallery media is no longer on disk; refresh the page");
16707
17301
  }
@@ -17074,6 +17668,8 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
17074
17668
  const anchor = spec.character?.styleAnchor;
17075
17669
  const anchorEntry = anchor ? lock.entries[lockKey(anchor.spec.styleId, anchor.spec.assetId)] : void 0;
17076
17670
  const replacedMetadata = previousEntry?.providerMetadata?.[provider.id];
17671
+ const lastGeneration = previousEntry?.status === "downloaded" ? previousEntry : previousEntry?.history?.[0];
17672
+ const lastGenerationMetadata = lastGeneration?.providerMetadata?.[provider.id];
17077
17673
  const parentOutputs = spec.character?.kind === "outfit" && parentEntry ? parentEntry.outputs.map((output, index) => ({
17078
17674
  path: currentEntryOutputPath(parentEntry, spec.character.parentSpec, index),
17079
17675
  role: output.role
@@ -17086,6 +17682,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
17086
17682
  ...anchorEntry?.objectId ? { styleObjectId: anchorEntry.objectId } : {},
17087
17683
  ...parentOutputs ? { parentOutputs } : {},
17088
17684
  ...replacedMetadata ? { replacedMetadata } : {},
17685
+ ...lastGenerationMetadata ? { lastGenerationMetadata } : {},
17089
17686
  ...previousMetadata ? { previousMetadata } : {},
17090
17687
  checkpoint: async (checkpoint) => {
17091
17688
  if (!checkpoint.jobId) throw new Error("Provider checkpoint returned an empty job id");
@@ -17161,13 +17758,13 @@ function ownedReferenceImageId(spec, lock, providerId) {
17161
17758
  }
17162
17759
 
17163
17760
  // src/pick/server.ts
17164
- import { existsSync as existsSync25 } from "fs";
17165
- import { readFile as readFile23 } from "fs/promises";
17761
+ import { existsSync as existsSync26 } from "fs";
17762
+ import { readFile as readFile24 } from "fs/promises";
17166
17763
 
17167
17764
  // src/pipeline/init.ts
17168
- import { readdir, readFile as readFile22, writeFile as writeFile16 } from "fs/promises";
17169
- import { existsSync as existsSync24 } from "fs";
17170
- import path33 from "path";
17765
+ import { readdir, readFile as readFile23, writeFile as writeFile17 } from "fs/promises";
17766
+ import { existsSync as existsSync25 } from "fs";
17767
+ import path34 from "path";
17171
17768
  function pngSize(buf) {
17172
17769
  if (buf.length < 24) return null;
17173
17770
  const isPng = buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834;
@@ -17181,7 +17778,7 @@ function slugify(value) {
17181
17778
  async function walk(dir, exclude) {
17182
17779
  const out = [];
17183
17780
  for (const entry of await readdir(dir, { withFileTypes: true })) {
17184
- const full = path33.join(dir, entry.name);
17781
+ const full = path34.join(dir, entry.name);
17185
17782
  if (exclude.some((x) => full.includes(x))) continue;
17186
17783
  if (entry.isDirectory()) out.push(...await walk(full, exclude));
17187
17784
  else if (entry.name.toLowerCase().endsWith(".png")) out.push(full);
@@ -17195,14 +17792,14 @@ async function scanAssets(root, opts = {}) {
17195
17792
  const skipped = [];
17196
17793
  const seen = /* @__PURE__ */ new Map();
17197
17794
  for (const file of files.sort()) {
17198
- const size = pngSize(await readFile22(file));
17795
+ const size = pngSize(await readFile23(file));
17199
17796
  if (!size) {
17200
17797
  skipped.push(`${file} (not a readable PNG)`);
17201
17798
  continue;
17202
17799
  }
17203
- const rel = path33.relative(root, file);
17204
- const category = path33.dirname(rel) === "." ? "" : path33.dirname(rel);
17205
- let id = slugify(path33.basename(rel));
17800
+ const rel = path34.relative(root, file);
17801
+ const category = path34.dirname(rel) === "." ? "" : path34.dirname(rel);
17802
+ let id = slugify(path34.basename(rel));
17206
17803
  if (seen.has(id)) {
17207
17804
  const n = seen.get(id) + 1;
17208
17805
  seen.set(id, n);
@@ -17260,10 +17857,10 @@ function mode(values) {
17260
17857
  return [...counts.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? 64;
17261
17858
  }
17262
17859
  async function writeManifestFile(target, manifest) {
17263
- if (existsSync24(target)) {
17860
+ if (existsSync25(target)) {
17264
17861
  throw new Error(`${target} already exists; refusing to overwrite an existing manifest.`);
17265
17862
  }
17266
- await writeFile16(target, JSON.stringify(manifest, null, 2) + "\n");
17863
+ await writeFile17(target, JSON.stringify(manifest, null, 2) + "\n");
17267
17864
  }
17268
17865
 
17269
17866
  // src/pick/sheet.ts
@@ -17676,13 +18273,13 @@ async function currentArt(entry, spec) {
17676
18273
  const previous = primaryOutput({ ...entry, outputs: entry.supersededOutputs });
17677
18274
  if (!previous) return null;
17678
18275
  const file = resolveOutputPath(previous.path, spec.root);
17679
- if (!existsSync25(file)) return null;
18276
+ if (!existsSync26(file)) return null;
17680
18277
  const contentType = previous.mediaType ?? mediaTypeFromExtension(file);
17681
18278
  if (!contentType) return null;
17682
18279
  let width = entry.width;
17683
18280
  let height = entry.height;
17684
18281
  if (contentType === "image/png") {
17685
- const size = pngSize(await readFile23(file));
18282
+ const size = pngSize(await readFile24(file));
17686
18283
  if (size) ({ width, height } = size);
17687
18284
  }
17688
18285
  return { path: file, contentType, width, height };
@@ -17690,7 +18287,7 @@ async function currentArt(entry, spec) {
17690
18287
  async function cachedFrame(url) {
17691
18288
  if (!url.startsWith("file://")) return null;
17692
18289
  const file = url.slice("file://".length);
17693
- const contentType = mediaTypeFromExtension(file) ?? detectMediaType(await readFile23(file).catch(() => Buffer.alloc(0))) ?? "application/octet-stream";
18290
+ const contentType = mediaTypeFromExtension(file) ?? detectMediaType(await readFile24(file).catch(() => Buffer.alloc(0))) ?? "application/octet-stream";
17694
18291
  return { path: file, contentType };
17695
18292
  }
17696
18293
  async function prepareReview(provider, lock, opts = {}) {
@@ -18236,15 +18833,15 @@ function createGenerateHandlers(opts) {
18236
18833
  }
18237
18834
 
18238
18835
  // src/gallery/editor.ts
18239
- import { readFile as readFile25 } from "fs/promises";
18240
- import path35 from "path";
18836
+ import { readFile as readFile26 } from "fs/promises";
18837
+ import path36 from "path";
18241
18838
 
18242
18839
  // src/editor/install.ts
18243
18840
  import { createHash as createHash5 } from "crypto";
18244
- import { existsSync as existsSync26 } from "fs";
18245
- import { mkdir as mkdir12, readFile as readFile24, rename as rename10, rm as rm11, stat as stat3, writeFile as writeFile17 } from "fs/promises";
18841
+ import { existsSync as existsSync27 } from "fs";
18842
+ import { mkdir as mkdir13, readFile as readFile25, rename as rename10, rm as rm11, stat as stat3, writeFile as writeFile18 } from "fs/promises";
18246
18843
  import os3 from "os";
18247
- import path34 from "path";
18844
+ import path35 from "path";
18248
18845
 
18249
18846
  // tools/pixelorama-bridge/pin.json
18250
18847
  var pin_default = {
@@ -18308,15 +18905,15 @@ var EditorInstallError = class extends Error {
18308
18905
  };
18309
18906
  function editorToolsRoot() {
18310
18907
  const override = process.env.PIXELKILN_TOOLS_DIR?.trim();
18311
- if (override) return path34.resolve(override);
18312
- if (process.platform === "darwin") return path34.join(os3.homedir(), "Library", "Caches", "pixelkiln", "tools");
18908
+ if (override) return path35.resolve(override);
18909
+ if (process.platform === "darwin") return path35.join(os3.homedir(), "Library", "Caches", "pixelkiln", "tools");
18313
18910
  if (process.platform === "win32") {
18314
- return path34.join(process.env.LOCALAPPDATA ?? path34.join(os3.homedir(), "AppData", "Local"), "pixelkiln", "tools");
18911
+ return path35.join(process.env.LOCALAPPDATA ?? path35.join(os3.homedir(), "AppData", "Local"), "pixelkiln", "tools");
18315
18912
  }
18316
- return path34.join(process.env.XDG_CACHE_HOME ?? path34.join(os3.homedir(), ".cache"), "pixelkiln", "tools");
18913
+ return path35.join(process.env.XDG_CACHE_HOME ?? path35.join(os3.homedir(), ".cache"), "pixelkiln", "tools");
18317
18914
  }
18318
18915
  function editorDir(pin = EDITOR_PIN, root = editorToolsRoot()) {
18319
- return path34.join(root, "pixelorama", pin.release ?? "unpublished");
18916
+ return path35.join(root, "pixelorama", pin.release ?? "unpublished");
18320
18917
  }
18321
18918
  function editorBaseUrl(pin = EDITOR_PIN) {
18322
18919
  const override = process.env.PIXELKILN_EDITOR_URL?.trim();
@@ -18326,7 +18923,7 @@ function editorBaseUrl(pin = EDITOR_PIN) {
18326
18923
  }
18327
18924
  async function sha256Of(file) {
18328
18925
  try {
18329
- return createHash5("sha256").update(await readFile24(file)).digest("hex");
18926
+ return createHash5("sha256").update(await readFile25(file)).digest("hex");
18330
18927
  } catch {
18331
18928
  return null;
18332
18929
  }
@@ -18339,8 +18936,8 @@ async function editorStatus(opts = {}) {
18339
18936
  let totalBytes = 0;
18340
18937
  for (const [name, expected] of Object.entries(pin.files)) {
18341
18938
  totalBytes += expected.bytes;
18342
- const file = path34.join(dir, name);
18343
- const info = existsSync26(file) ? await stat3(file).catch(() => null) : null;
18939
+ const file = path35.join(dir, name);
18940
+ const info = existsSync27(file) ? await stat3(file).catch(() => null) : null;
18344
18941
  if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
18345
18942
  installedBytes += expected.bytes;
18346
18943
  } else {
@@ -18369,7 +18966,7 @@ async function installEditor(opts = {}) {
18369
18966
  );
18370
18967
  }
18371
18968
  if (!baseUrl) throw new EditorInstallError("no download location for the editor build");
18372
- await mkdir12(dir, { recursive: true });
18969
+ await mkdir13(dir, { recursive: true });
18373
18970
  const downloaded = [];
18374
18971
  const skipped = Object.keys(pin.files).filter((name) => !before.missing.includes(name));
18375
18972
  let fetchedBytes = before.installedBytes;
@@ -18397,10 +18994,10 @@ async function installEditor(opts = {}) {
18397
18994
  if (digest2 !== expected.sha256) {
18398
18995
  throw new EditorInstallError(`${name}: sha256 ${digest2.slice(0, 12)}\u2026 does not match the pinned ${expected.sha256.slice(0, 12)}\u2026`, name);
18399
18996
  }
18400
- const final = path34.join(dir, name);
18997
+ const final = path35.join(dir, name);
18401
18998
  const tmp = `${final}.${process.pid}.part`;
18402
18999
  try {
18403
- await writeFile17(tmp, bytes);
19000
+ await writeFile18(tmp, bytes);
18404
19001
  await rename10(tmp, final);
18405
19002
  } finally {
18406
19003
  await rm11(tmp, { force: true });
@@ -18443,7 +19040,7 @@ var EDITOR_CONTENT_TYPES = {
18443
19040
  ".json": "application/json; charset=utf-8"
18444
19041
  };
18445
19042
  function editorContentType(name) {
18446
- return EDITOR_CONTENT_TYPES[path34.extname(name).toLowerCase()] ?? null;
19043
+ return EDITOR_CONTENT_TYPES[path35.extname(name).toLowerCase()] ?? null;
18447
19044
  }
18448
19045
 
18449
19046
  // src/gallery/editor.ts
@@ -18519,7 +19116,7 @@ function createGalleryEditorHandlers(opts = {}) {
18519
19116
  if (!status.installed) return null;
18520
19117
  let bytes;
18521
19118
  try {
18522
- bytes = await readFile25(path35.join(dir, name));
19119
+ bytes = await readFile26(path36.join(dir, name));
18523
19120
  } catch {
18524
19121
  verified = null;
18525
19122
  return null;
@@ -18559,7 +19156,7 @@ async function serveUntilStopped(initial, reload, args, access2) {
18559
19156
  };
18560
19157
  const loadProject = async (project) => {
18561
19158
  const ctx = await access2.loadProject(project);
18562
- loadProjectEnv(path36.dirname(ctx.loaded.path));
19159
+ loadProjectEnv(path37.dirname(ctx.loaded.path));
18563
19160
  return ctx;
18564
19161
  };
18565
19162
  const loadProjectEnv = (dir) => {
@@ -18568,7 +19165,7 @@ async function serveUntilStopped(initial, reload, args, access2) {
18568
19165
  for (const [name, value] of Object.entries(declared)) {
18569
19166
  if (credentialNames.has(name) && name in process.env && process.env[name] !== value) {
18570
19167
  throw new Error(
18571
- `${path36.relative(process.cwd(), dir) || "."} sets ${name} to a different value than the one this gallery already loaded; run a separate gallery for that project so its work uses its own account.`
19168
+ `${path37.relative(process.cwd(), dir) || "."} sets ${name} to a different value than the one this gallery already loaded; run a separate gallery for that project so its work uses its own account.`
18572
19169
  );
18573
19170
  }
18574
19171
  }
@@ -18576,7 +19173,7 @@ async function serveUntilStopped(initial, reload, args, access2) {
18576
19173
  };
18577
19174
  const quoters = /* @__PURE__ */ new Map();
18578
19175
  const quoteFor = async (project) => {
18579
- loadProjectEnv(path36.dirname(path36.resolve(await access2.manifestFor(project))));
19176
+ loadProjectEnv(path37.dirname(path37.resolve(await access2.manifestFor(project))));
18580
19177
  const key = process.env.PIXELLAB_API_KEY;
18581
19178
  if (!key) return null;
18582
19179
  let quoter = quoters.get(key);
@@ -18627,8 +19224,8 @@ async function serveUntilStopped(initial, reload, args, access2) {
18627
19224
  }
18628
19225
  async function runGallery(args) {
18629
19226
  if (args.workspace) {
18630
- const workspacePath = path36.resolve(args.workspace);
18631
- if (!existsSync27(workspacePath)) {
19227
+ const workspacePath = path37.resolve(args.workspace);
19228
+ if (!existsSync28(workspacePath)) {
18632
19229
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
18633
19230
  }
18634
19231
  const filter2 = { styles: args.styles, assets: args.assets };
@@ -18648,7 +19245,7 @@ async function runGallery(args) {
18648
19245
  const registered = async (projectId) => {
18649
19246
  const project = (await loadWorkspace(workspacePath)).projects.find((candidate) => candidate.id === projectId);
18650
19247
  if (!projectId || !project) throw new Error(`unknown workspace project "${projectId ?? ""}"`);
18651
- return resolveProject(path36.dirname(workspacePath), project);
19248
+ return resolveProject(path37.dirname(workspacePath), project);
18652
19249
  };
18653
19250
  await serveUntilStopped(initial, build2, args, {
18654
19251
  manifestFor: async (projectId) => (await registered(projectId)).manifestPath,
@@ -18673,7 +19270,7 @@ async function runGallery(args) {
18673
19270
  return buildGallerySnapshot({ ...fresh, filter });
18674
19271
  };
18675
19272
  await serveUntilStopped(await build(), reload, args, {
18676
- manifestFor: () => path36.resolve(args.manifest),
19273
+ manifestFor: () => path37.resolve(args.manifest),
18677
19274
  loadProject: () => openProject(args.manifest, { lockPath: args.lock, env: false })
18678
19275
  });
18679
19276
  }
@@ -18916,7 +19513,7 @@ async function runLifecycle(args, wave, spent) {
18916
19513
  log(` Packaging stays blocked until each refined PNG has a current human approval.`);
18917
19514
  }
18918
19515
  }
18919
- return args.command === "gen" && submitted > 0 && total.downloaded > 0 && !total.failed && specs.some((spec) => spec.character?.parentSpec || spec.character?.styleAnchor || spec.revision || spec.mirror);
19516
+ return args.command === "gen" && submitted > 0 && total.downloaded > 0 && !total.failed && specs.some((spec) => spec.character?.parentSpec || spec.character?.styleAnchor || spec.revision || spec.mirror || spec.scene || spec.vfx?.sprite && "file" in spec.vfx.sprite);
18920
19517
  }
18921
19518
  return false;
18922
19519
  }
@@ -18982,18 +19579,18 @@ async function runRestoreGeneration(args) {
18982
19579
  }
18983
19580
 
18984
19581
  // src/cli/commands/housekeeping.ts
18985
- import { existsSync as existsSync29 } from "fs";
19582
+ import { existsSync as existsSync30 } from "fs";
18986
19583
 
18987
19584
  // src/pipeline/cache-health.ts
18988
- import { existsSync as existsSync28 } from "fs";
18989
- import { readFile as readFile26, readdir as readdir2, rm as rm12 } from "fs/promises";
18990
- import path37 from "path";
19585
+ import { existsSync as existsSync29 } from "fs";
19586
+ import { readFile as readFile27, readdir as readdir2, rm as rm12 } from "fs/promises";
19587
+ import path38 from "path";
18991
19588
  async function inspectCaches(lock, lockPath, options = {}) {
18992
- if (options.prune && !existsSync28(lockPath)) {
18993
- throw new Error(`Refusing to prune without an existing lockfile at ${path37.resolve(lockPath)}`);
19589
+ if (options.prune && !existsSync29(lockPath)) {
19590
+ throw new Error(`Refusing to prune without an existing lockfile at ${path38.resolve(lockPath)}`);
18994
19591
  }
18995
- const contentDir = path37.resolve(path37.dirname(lockPath), ".pixelkiln", "cache");
18996
- const remotePath = path37.resolve(cachePathFor(lockPath));
19592
+ const contentDir = path38.resolve(path38.dirname(lockPath), ".pixelkiln", "cache");
19593
+ const remotePath = path38.resolve(cachePathFor(lockPath));
18997
19594
  const referenced = referencedHashes(lock);
18998
19595
  let content = await inspectContentCache(contentDir, referenced);
18999
19596
  let remoteHashes = await inspectRemoteHashCache(remotePath);
@@ -19005,7 +19602,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
19005
19602
  ]);
19006
19603
  for (const name of names) {
19007
19604
  try {
19008
- await rm12(path37.join(contentDir, name), { force: true });
19605
+ await rm12(path38.join(contentDir, name), { force: true });
19009
19606
  removed.contentFiles++;
19010
19607
  } catch {
19011
19608
  }
@@ -19014,7 +19611,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
19014
19611
  await saveCache(remotePath, { version: 1, hashes: {} });
19015
19612
  removed.resetRemoteHashCache = true;
19016
19613
  } else if (remoteHashes.invalidIds.length) {
19017
- const cache = parseCache(JSON.parse(await readFile26(remotePath, "utf8")));
19614
+ const cache = parseCache(JSON.parse(await readFile27(remotePath, "utf8")));
19018
19615
  for (const id of remoteHashes.invalidIds) delete cache.hashes[id];
19019
19616
  removed.remoteHashEntries = remoteHashes.invalidIds.length;
19020
19617
  await saveCache(remotePath, cache);
@@ -19032,7 +19629,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
19032
19629
  async function inspectContentCache(contentDir, referenced) {
19033
19630
  const report = {
19034
19631
  path: contentDir,
19035
- exists: existsSync28(contentDir),
19632
+ exists: existsSync29(contentDir),
19036
19633
  files: 0,
19037
19634
  bytes: 0,
19038
19635
  valid: 0,
@@ -19053,12 +19650,12 @@ async function inspectContentCache(contentDir, referenced) {
19053
19650
  continue;
19054
19651
  }
19055
19652
  report.files++;
19056
- const file = path37.join(contentDir, entry.name);
19653
+ const file = path38.join(contentDir, entry.name);
19057
19654
  const mediaType = mediaTypeFromExtension(entry.name);
19058
19655
  const expected = mediaType ? entry.name.slice(0, -4) : "";
19059
19656
  let bytes;
19060
19657
  try {
19061
- bytes = await readFile26(file);
19658
+ bytes = await readFile27(file);
19062
19659
  report.bytes += bytes.length;
19063
19660
  } catch (err) {
19064
19661
  report.invalid.push({
@@ -19096,7 +19693,7 @@ async function inspectContentCache(contentDir, referenced) {
19096
19693
  async function inspectRemoteHashCache(remotePath) {
19097
19694
  const report = {
19098
19695
  path: remotePath,
19099
- exists: existsSync28(remotePath),
19696
+ exists: existsSync29(remotePath),
19100
19697
  entries: 0,
19101
19698
  valid: 0,
19102
19699
  invalidIds: [],
@@ -19105,7 +19702,7 @@ async function inspectRemoteHashCache(remotePath) {
19105
19702
  if (!report.exists) return report;
19106
19703
  let cache;
19107
19704
  try {
19108
- cache = parseCache(JSON.parse(await readFile26(remotePath, "utf8")));
19705
+ cache = parseCache(JSON.parse(await readFile27(remotePath, "utf8")));
19109
19706
  } catch (err) {
19110
19707
  report.error = err instanceof Error ? err.message : String(err);
19111
19708
  return report;
@@ -19169,7 +19766,7 @@ async function runAccept(args) {
19169
19766
  let intact = true;
19170
19767
  for (const output of entry.outputs) {
19171
19768
  const file = resolveOutputPath(output.path, item.spec.root);
19172
- if (!existsSync29(file) || await sha256File(file) !== output.sha256) {
19769
+ if (!existsSync30(file) || await sha256File(file) !== output.sha256) {
19173
19770
  intact = false;
19174
19771
  break;
19175
19772
  }
@@ -19224,42 +19821,42 @@ This drops their provenance from the lockfile. The art on disk is`);
19224
19821
  }
19225
19822
 
19226
19823
  // src/cli/commands/init.ts
19227
- import path38 from "path";
19228
- import { existsSync as existsSync30 } from "fs";
19824
+ import path39 from "path";
19825
+ import { existsSync as existsSync31 } from "fs";
19229
19826
  async function runInit(args) {
19230
19827
  if (!args.from) throw new Error("init needs --from <dir> pointing at your existing PNGs.");
19231
- const root = path38.resolve(args.from);
19232
- if (!existsSync30(root)) throw new Error(`No directory at ${root}`);
19828
+ const root = path39.resolve(args.from);
19829
+ if (!existsSync31(root)) throw new Error(`No directory at ${root}`);
19233
19830
  const generator = args.generator ?? "map";
19234
19831
  if (generator !== "1dir" && generator !== "map") {
19235
19832
  throw new Error(`--generator must be "1dir" or "map", got "${args.generator}".`);
19236
19833
  }
19237
- const target = path38.resolve(args.out ?? "pixelkiln.manifest.json");
19834
+ const target = path39.resolve(args.out ?? "pixelkiln.manifest.json");
19238
19835
  const { assets, skipped } = await scanAssets(root, { exclude: args.exclude });
19239
19836
  if (!assets.length) throw new Error(`No PNGs found under ${root}`);
19240
19837
  const manifest = buildManifest(
19241
- args.name ?? path38.basename(path38.dirname(target)),
19838
+ args.name ?? path39.basename(path39.dirname(target)),
19242
19839
  args.styles[0] ?? "base",
19243
19840
  generator,
19244
- path38.relative(path38.dirname(target), root) || ".",
19841
+ path39.relative(path39.dirname(target), root) || ".",
19245
19842
  assets
19246
19843
  );
19247
19844
  await writeManifestFile(target, manifest);
19248
- log(` scanned ${assets.length} PNG(s) under ${path38.relative(process.cwd(), root)}`);
19845
+ log(` scanned ${assets.length} PNG(s) under ${path39.relative(process.cwd(), root)}`);
19249
19846
  if (skipped.length) log(` skipped ${skipped.length} unreadable file(s)`);
19250
- log(` wrote ${path38.relative(process.cwd(), target)}`);
19847
+ log(` wrote ${path39.relative(process.cwd(), target)}`);
19251
19848
  log(`
19252
19849
  Prompts are intentionally empty. To recover the real ones from your`);
19253
19850
  log(` PixelLab account instead of inventing them:`);
19254
19851
  log(`
19255
- pixelkiln adopt --manifest ${path38.relative(process.cwd(), target)} --write-prompts
19852
+ pixelkiln adopt --manifest ${path39.relative(process.cwd(), target)} --write-prompts
19256
19853
  `);
19257
19854
  }
19258
19855
 
19259
19856
  // src/pipeline/doctor.ts
19260
- import { constants, existsSync as existsSync31 } from "fs";
19857
+ import { constants, existsSync as existsSync32 } from "fs";
19261
19858
  import { access, stat as stat4 } from "fs/promises";
19262
- import path39 from "path";
19859
+ import path40 from "path";
19263
19860
  async function doctor(loaded, specs, lock, lockPath, opts = {}) {
19264
19861
  const checks = [];
19265
19862
  const add = (id, level, message6) => checks.push({ id, level, message: message6 });
@@ -19270,8 +19867,8 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
19270
19867
  );
19271
19868
  add("lock", "ok", `${Object.keys(lock.entries).length} valid v2 lock entr(ies)`);
19272
19869
  const dirs = [...new Set(specs.flatMap((spec) => [
19273
- path39.dirname(spec.outFile),
19274
- ...spec.quality ? [path39.dirname(spec.quality.outFile)] : []
19870
+ path40.dirname(spec.outFile),
19871
+ ...spec.quality ? [path40.dirname(spec.quality.outFile)] : []
19275
19872
  ]))];
19276
19873
  const unwritable = [];
19277
19874
  for (const dir of dirs) {
@@ -19294,7 +19891,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
19294
19891
  for (let index = 0; index < entry.outputs.length; index++) {
19295
19892
  const output = entry.outputs[index];
19296
19893
  const spec = specByKey.get(key);
19297
- const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, path39.dirname(lockPath));
19894
+ const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, path40.dirname(lockPath));
19298
19895
  const owner = outputOwners.get(absolute);
19299
19896
  if (owner && owner !== key) duplicateOutputs.push(`${output.path} (${owner}, ${key})`);
19300
19897
  outputOwners.set(absolute, key);
@@ -19309,7 +19906,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
19309
19906
  } else {
19310
19907
  add("lock-outputs", "ok", `${outputOwners.size} recorded output path(s) are uniquely owned`);
19311
19908
  }
19312
- const cacheDir = path39.join(path39.dirname(lockPath), ".pixelkiln", "cache");
19909
+ const cacheDir = path40.join(path40.dirname(lockPath), ".pixelkiln", "cache");
19313
19910
  const stranded = [];
19314
19911
  const invalidState = [];
19315
19912
  const staleMapUrls = [];
@@ -19325,11 +19922,11 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
19325
19922
  const hasSource = Boolean(entry.sourceUrl || entry.sourceUrls?.length);
19326
19923
  let hasCache = false;
19327
19924
  for (const output of entry.outputs) {
19328
- const cached = path39.join(
19925
+ const cached = path40.join(
19329
19926
  cacheDir,
19330
19927
  cacheFileName(output.sha256, output.mediaType ?? MediaType.PNG)
19331
19928
  );
19332
- if (existsSync31(cached) && await sha256File(cached) === output.sha256) {
19929
+ if (existsSync32(cached) && await sha256File(cached) === output.sha256) {
19333
19930
  hasCache = true;
19334
19931
  break;
19335
19932
  }
@@ -19425,16 +20022,16 @@ async function nearestExistingDirectory(start) {
19425
20022
  if ((await stat4(current)).isDirectory()) return current;
19426
20023
  } catch {
19427
20024
  }
19428
- const parent = path39.dirname(current);
20025
+ const parent = path40.dirname(current);
19429
20026
  if (parent === current) return current;
19430
20027
  current = parent;
19431
20028
  }
19432
20029
  }
19433
20030
 
19434
20031
  // src/pipeline/audit.ts
19435
- import { readFile as readFile27 } from "fs/promises";
19436
- import { existsSync as existsSync32 } from "fs";
19437
- import path40 from "path";
20032
+ import { readFile as readFile28 } from "fs/promises";
20033
+ import { existsSync as existsSync33 } from "fs";
20034
+ import path41 from "path";
19438
20035
  function paletteDistance(asset, reference) {
19439
20036
  if (!asset.length || !reference.length) return 0;
19440
20037
  let total = 0;
@@ -19470,12 +20067,12 @@ async function auditStyle(loaded, specs, styleId, lock) {
19470
20067
  const unreadable = [];
19471
20068
  for (const spec of mine) {
19472
20069
  for (const output of resolveSpecOutputs(spec, lock, loaded.root)) {
19473
- if (!existsSync32(output.absolutePath)) {
20070
+ if (!existsSync33(output.absolutePath)) {
19474
20071
  missing.push(output.id);
19475
20072
  continue;
19476
20073
  }
19477
20074
  try {
19478
- const png = decodePng(await readFile27(output.absolutePath));
20075
+ const png = decodePng(await readFile28(output.absolutePath));
19479
20076
  const palette = extractPalette(png, 12);
19480
20077
  assets.push({
19481
20078
  assetId: spec.assetId,
@@ -19499,10 +20096,10 @@ async function auditStyle(loaded, specs, styleId, lock) {
19499
20096
  let referenceFromStyleImages = false;
19500
20097
  const refPalettes = [];
19501
20098
  for (const img of style.styleImages) {
19502
- const abs = path40.resolve(loaded.root, img.path);
19503
- if (!existsSync32(abs)) continue;
20099
+ const abs = path41.resolve(loaded.root, img.path);
20100
+ if (!existsSync33(abs)) continue;
19504
20101
  try {
19505
- refPalettes.push(extractPalette(decodePng(await readFile27(abs)), 12));
20102
+ refPalettes.push(extractPalette(decodePng(await readFile28(abs)), 12));
19506
20103
  } catch {
19507
20104
  }
19508
20105
  }
@@ -19730,24 +20327,24 @@ async function runAudit(args) {
19730
20327
  }
19731
20328
 
19732
20329
  // src/cli/commands/pack.ts
19733
- import path42 from "path";
19734
- import { readFile as readFile28 } from "fs/promises";
20330
+ import path43 from "path";
20331
+ import { readFile as readFile29 } from "fs/promises";
19735
20332
 
19736
20333
  // src/pipeline/pack.ts
19737
20334
  import { readFileSync as readFileSync6 } from "fs";
19738
- import path41 from "path";
20335
+ import path42 from "path";
19739
20336
  var DEFAULT_FRAME_SET_FPS = 12;
19740
20337
  function resolvePackInputs(raw, inputsFilePath) {
19741
20338
  if (!Array.isArray(raw) || !raw.length) {
19742
20339
  throw new Error("--inputs must be a non-empty JSON array of { id, path }");
19743
20340
  }
19744
- const baseDir = path41.dirname(path41.resolve(inputsFilePath));
20341
+ const baseDir = path42.dirname(path42.resolve(inputsFilePath));
19745
20342
  return raw.map((entry, i) => {
19746
20343
  const e = entry;
19747
20344
  if (typeof e.id !== "string" || typeof e.path !== "string") {
19748
20345
  throw new Error(`--inputs[${i}] needs string "id" and "path"`);
19749
20346
  }
19750
- return { id: e.id, path: path41.resolve(baseDir, e.path) };
20347
+ return { id: e.id, path: path42.resolve(baseDir, e.path) };
19751
20348
  });
19752
20349
  }
19753
20350
  function packSprites(inputs, options = {}) {
@@ -19831,7 +20428,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
19831
20428
  }
19832
20429
  const inputs2 = Object.entries(options.sourceOverrides).map(([id, source]) => ({
19833
20430
  id,
19834
- path: path41.resolve(source)
20431
+ path: path42.resolve(source)
19835
20432
  }));
19836
20433
  if (!inputs2.length) throw new Error(`No approved quality outputs for style "${styleId}".`);
19837
20434
  const packed2 = packSprites(inputs2, options);
@@ -19855,7 +20452,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
19855
20452
  let outputs = resolveEntryOutputs(entry, id, manifestDir);
19856
20453
  const source = sources[id];
19857
20454
  if (source && outputs.length <= 1) {
19858
- inputs.push({ id, path: path41.resolve(manifestDir, source) });
20455
+ inputs.push({ id, path: path42.resolve(manifestDir, source) });
19859
20456
  continue;
19860
20457
  }
19861
20458
  if (source && sourceIsStem(source, entry, manifestDir)) {
@@ -19884,12 +20481,13 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
19884
20481
  id,
19885
20482
  kind: isFrames ? "frames" : "members",
19886
20483
  ...fps ? { fps } : {},
20484
+ ...isFrames && frameSetLoop(entry) === false ? { loop: false } : {},
19887
20485
  frames: selected.map((output) => output.id)
19888
20486
  });
19889
20487
  }
19890
20488
  }
19891
20489
  for (const [id, source] of Object.entries(sources)) {
19892
- if (!locked.has(id)) inputs.push({ id, path: path41.resolve(manifestDir, source) });
20490
+ if (!locked.has(id)) inputs.push({ id, path: path42.resolve(manifestDir, source) });
19893
20491
  }
19894
20492
  if (!inputs.length) {
19895
20493
  throw new Error(`No readable sprites for style "${styleId}"; ${noOutput.length} skipped.`);
@@ -19997,7 +20595,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
19997
20595
  const source = sources[id];
19998
20596
  const entry = lock.entries[`${styleId}/${id}`];
19999
20597
  if (source && !(entry && sourceIsStem(source, entry, manifestDir))) {
20000
- placements.push({ id, path: path41.resolve(manifestDir, source), cell });
20598
+ placements.push({ id, path: path42.resolve(manifestDir, source), cell });
20001
20599
  continue;
20002
20600
  }
20003
20601
  if (source && entry) {
@@ -20030,7 +20628,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
20030
20628
  `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}`
20031
20629
  );
20032
20630
  }
20033
- const basePng = mount.base ? readFileSync6(path41.resolve(manifestDir, mount.base)) : void 0;
20631
+ const basePng = mount.base ? readFileSync6(path42.resolve(manifestDir, mount.base)) : void 0;
20034
20632
  const mounted = mountSprites(placements, {
20035
20633
  cellWidth: mount.cellWidth,
20036
20634
  cellHeight: mount.cellHeight,
@@ -20040,7 +20638,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
20040
20638
  ...mounted,
20041
20639
  sources: mount.base ? [{
20042
20640
  id: "$base",
20043
- path: path41.resolve(manifestDir, mount.base),
20641
+ path: path42.resolve(manifestDir, mount.base),
20044
20642
  sha256: sha256(basePng),
20045
20643
  included: true
20046
20644
  }, ...mounted.sources] : mounted.sources,
@@ -20422,7 +21020,7 @@ function renderGodotSpriteFrames(atlas, opts) {
20422
21020
  );
20423
21021
  };
20424
21022
  for (const set of atlas.sets ?? []) {
20425
- if (set.kind === "frames") animation(set.id, set.frames, set.fps ?? GODOT_DEFAULT_SPEED, true);
21023
+ if (set.kind === "frames") animation(set.id, set.frames, set.fps ?? GODOT_DEFAULT_SPEED, set.loop ?? true);
20426
21024
  else for (const id of set.frames) animation(id, [id], GODOT_DEFAULT_SPEED, false);
20427
21025
  }
20428
21026
  for (const frame of atlas.frames) {
@@ -20444,12 +21042,12 @@ async function runPack(args) {
20444
21042
  if (args.primaryOnly || args.outputRoles.length) {
20445
21043
  throw new Error("--primary-only and --output-role require manifest-driven pack");
20446
21044
  }
20447
- const raw = JSON.parse(await readFile28(path42.resolve(args.inputs), "utf8"));
21045
+ const raw = JSON.parse(await readFile29(path43.resolve(args.inputs), "utf8"));
20448
21046
  const inputs = resolvePackInputs(raw, args.inputs);
20449
21047
  const { png, atlas, skipped, sources } = packSprites(inputs, { columns: args.columns });
20450
21048
  const format = sheetFormat(args);
20451
- const base = path42.resolve(args.out.replace(/\.(?:png|json|tres)$/i, ""));
20452
- const { extension, document } = renderSheetDocument(format, atlas, { imageName: path42.basename(`${base}.png`) });
21049
+ const base = path43.resolve(args.out.replace(/\.(?:png|json|tres)$/i, ""));
21050
+ const { extension, document } = renderSheetDocument(format, atlas, { imageName: path43.basename(`${base}.png`) });
20453
21051
  const outputs = [
20454
21052
  { path: `${base}.png`, data: png },
20455
21053
  { path: `${base}${extension}`, data: document }
@@ -20462,7 +21060,7 @@ async function runPack(args) {
20462
21060
  log(
20463
21061
  ` ${atlas.frames.length} sprite(s), ${atlas.sheet.width}x${atlas.sheet.height} in ${atlas.columns} column(s), ${(png.length / 1024).toFixed(1)} KB (${format})`
20464
21062
  );
20465
- log(` ${path42.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
21063
+ log(` ${path43.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
20466
21064
  for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
20467
21065
  return;
20468
21066
  }
@@ -20470,7 +21068,7 @@ async function runPack(args) {
20470
21068
  if (args.primaryOnly && args.outputRoles.length) {
20471
21069
  throw new Error("pack accepts either --primary-only or --output-role, not both");
20472
21070
  }
20473
- const manifestDir = path42.dirname(path42.resolve(args.manifest));
21071
+ const manifestDir = path43.dirname(path43.resolve(args.manifest));
20474
21072
  const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
20475
21073
  for (const styleId of styleIds) {
20476
21074
  const style = loaded.manifest.styles[styleId];
@@ -20485,9 +21083,9 @@ async function runPack(args) {
20485
21083
  sourceOverrides: qualitySources,
20486
21084
  sources: manifestSources(loaded.manifest, styleId)
20487
21085
  });
20488
- const base = args.out ? path42.resolve(args.out.replace(/\.(?:png|json|tres)$/i, "")) : path42.resolve(manifestDir, style.outDir, `${styleId}-sheet`);
21086
+ const base = args.out ? path43.resolve(args.out.replace(/\.(?:png|json|tres)$/i, "")) : path43.resolve(manifestDir, style.outDir, `${styleId}-sheet`);
20489
21087
  const format = sheetFormat(args);
20490
- const { extension, document } = renderSheetDocument(format, atlas, { imageName: path42.basename(`${base}.png`) });
21088
+ const { extension, document } = renderSheetDocument(format, atlas, { imageName: path43.basename(`${base}.png`) });
20491
21089
  const outputs = [
20492
21090
  { path: `${base}.png`, data: png },
20493
21091
  { path: `${base}${extension}`, data: document }
@@ -20519,13 +21117,13 @@ async function runPack(args) {
20519
21117
  log(
20520
21118
  ` ${styleId}: ${atlas.frames.length} sprite(s), ${atlas.sheet.width}x${atlas.sheet.height} in ${atlas.columns} column(s)` + (atlas.sets?.length ? `, ${atlas.sets.length} set(s)` : "") + ` (${format})`
20521
21119
  );
20522
- log(` ${path42.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
21120
+ log(` ${path43.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
20523
21121
  for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
20524
21122
  }
20525
21123
  }
20526
21124
  async function runMount(args) {
20527
21125
  const { loaded, lock } = await openProject2(args);
20528
- const manifestDir = path42.dirname(path42.resolve(args.manifest));
21126
+ const manifestDir = path43.dirname(path43.resolve(args.manifest));
20529
21127
  const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
20530
21128
  for (const styleId of styleIds) {
20531
21129
  const style = loaded.manifest.styles[styleId];
@@ -20564,9 +21162,9 @@ async function runMount(args) {
20564
21162
  sources,
20565
21163
  outputRoles
20566
21164
  );
20567
- const out = path42.resolve(manifestDir, style.mount.out);
21165
+ const out = path43.resolve(manifestDir, style.mount.out);
20568
21166
  const format = sheetFormat(args);
20569
- const { extension, document } = renderSheetDocument(format, atlas, { imageName: path42.basename(out) });
21167
+ const { extension, document } = renderSheetDocument(format, atlas, { imageName: path43.basename(out) });
20570
21168
  const metadata = out.replace(/\.png$/, "") + extension;
20571
21169
  const companion = out.replace(/\.png$/, "") + ".pixelkiln.json";
20572
21170
  const outputs = [
@@ -20586,7 +21184,7 @@ async function runMount(args) {
20586
21184
  await provenanceFile("$lock", args.lock),
20587
21185
  ...qualityRecords,
20588
21186
  ...artifactSources.filter(
20589
- (source) => source.id !== "$base" || path42.resolve(source.path) !== out
21187
+ (source) => source.id !== "$base" || path43.resolve(source.path) !== out
20590
21188
  )
20591
21189
  ],
20592
21190
  options: {
@@ -20600,14 +21198,14 @@ async function runMount(args) {
20600
21198
  log(
20601
21199
  ` ${styleId}: ${atlas.frames.length} cell(s) into ${atlas.sheet.width}x${atlas.sheet.height}` + (overBase ? ` over ${style.mount.base}` : " (new sheet)")
20602
21200
  );
20603
- log(` ${path42.relative(process.cwd(), out)} + atlas/provenance JSON`);
21201
+ log(` ${path43.relative(process.cwd(), out)} + atlas/provenance JSON`);
20604
21202
  for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
20605
21203
  }
20606
21204
  }
20607
21205
  async function runExport(args) {
20608
21206
  const { loaded, specs, lock } = await openProject2(args);
20609
21207
  const format = args.format ?? "generic";
20610
- const manifestDir = path42.dirname(path42.resolve(args.manifest));
21208
+ const manifestDir = path43.dirname(path43.resolve(args.manifest));
20611
21209
  const selected = specs.filter((spec) => {
20612
21210
  if (spec.generator !== "tiles" && spec.generator !== "terrain") return false;
20613
21211
  if (args.styles.length && !args.styles.includes(spec.styleId)) return false;
@@ -20623,12 +21221,12 @@ async function runExport(args) {
20623
21221
  for (const spec of selected) {
20624
21222
  const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
20625
21223
  const style = loaded.manifest.styles[spec.styleId];
20626
- const defaultBase = path42.resolve(manifestDir, style.outDir, `${spec.assetId}-tileset`);
20627
- const base = args.out ? path42.resolve(args.out.replace(/\.(?:png|json|tsj|tres)$/i, "")) : defaultBase;
21224
+ const defaultBase = path43.resolve(manifestDir, style.outDir, `${spec.assetId}-tileset`);
21225
+ const base = args.out ? path43.resolve(args.out.replace(/\.(?:png|json|tsj|tres)$/i, "")) : defaultBase;
20628
21226
  const result = exportTileset(entry, spec, {
20629
21227
  format,
20630
21228
  manifestDir,
20631
- imageName: path42.basename(`${base}.png`),
21229
+ imageName: path43.basename(`${base}.png`),
20632
21230
  columns: args.columns
20633
21231
  });
20634
21232
  const outputs = [
@@ -20646,7 +21244,7 @@ async function runExport(args) {
20646
21244
  asset: spec.assetId,
20647
21245
  columns: args.columns ?? null,
20648
21246
  format,
20649
- image: path42.basename(`${base}.png`),
21247
+ image: path43.basename(`${base}.png`),
20650
21248
  providerRules: result.generic.providerRules,
20651
21249
  style: spec.styleId,
20652
21250
  tileType: spec.tileType ?? null
@@ -20656,7 +21254,7 @@ async function runExport(args) {
20656
21254
  ` ${spec.styleId}/${spec.assetId}: ${result.generic.tiles.length} tile(s), ${result.generic.sheet.width}x${result.generic.sheet.height} (${format})`
20657
21255
  );
20658
21256
  log(
20659
- ` ${path42.relative(process.cwd(), base)}.png + ${path42.basename(base)}${result.extension} + .pixelkiln.json`
21257
+ ` ${path43.relative(process.cwd(), base)}.png + ${path43.basename(base)}${result.extension} + .pixelkiln.json`
20660
21258
  );
20661
21259
  }
20662
21260
  }
@@ -20669,23 +21267,23 @@ function sheetFormat(args) {
20669
21267
  }
20670
21268
 
20671
21269
  // src/cli/commands/pixellab-utils.ts
20672
- import path44 from "path";
21270
+ import path45 from "path";
20673
21271
 
20674
21272
  // src/pixellab-utils.ts
20675
- import { existsSync as existsSync33 } from "fs";
20676
- import { mkdir as mkdir13, readFile as readFile29, writeFile as writeFile18 } from "fs/promises";
20677
- import path43 from "path";
21273
+ import { existsSync as existsSync34 } from "fs";
21274
+ import { mkdir as mkdir14, readFile as readFile30, writeFile as writeFile19 } from "fs/promises";
21275
+ import path44 from "path";
20678
21276
  var UNZOOM_MIN_SIDE = 256;
20679
21277
  var UNZOOM_MAX_AREA = 2048 * 2048;
20680
21278
  function defaultUnzoomOut(input) {
20681
- const ext = path43.extname(input);
21279
+ const ext = path44.extname(input);
20682
21280
  return `${ext ? input.slice(0, -ext.length) : input}.unzoomed.png`;
20683
21281
  }
20684
21282
  async function unzoomFile(client2, input, opts = {}) {
20685
- const out = path43.resolve(opts.out ?? defaultUnzoomOut(input));
20686
- if (path43.resolve(input) === out) throw new Error("unzoom will not overwrite its own input; pass a different --out");
20687
- if (existsSync33(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
20688
- const bytes = await readFile29(input);
21283
+ const out = path44.resolve(opts.out ?? defaultUnzoomOut(input));
21284
+ if (path44.resolve(input) === out) throw new Error("unzoom will not overwrite its own input; pass a different --out");
21285
+ if (existsSync34(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
21286
+ const bytes = await readFile30(input);
20689
21287
  const metadata = imageMetadata(bytes);
20690
21288
  if (!metadata) throw new Error(`${input} is not a readable PNG or JPEG`);
20691
21289
  const { width, height, format } = metadata;
@@ -20698,22 +21296,22 @@ async function unzoomFile(client2, input, opts = {}) {
20698
21296
  throw new Error(`${input} is ${width}x${height}; PixelLab's unzoom takes at most 2048x2048 worth of pixels`);
20699
21297
  }
20700
21298
  const res = await client2.unzoom({ image: { base64: bytes.toString("base64"), format }, quantize: opts.quantize });
20701
- await mkdir13(path43.dirname(out), { recursive: true });
20702
- await writeFile18(out, res.png);
21299
+ await mkdir14(path44.dirname(out), { recursive: true });
21300
+ await writeFile19(out, res.png);
20703
21301
  return { out, original: res.originalSize, unzoomed: res.unzoomedSize, zoomFactor: res.zoomFactor, usage: res.usage };
20704
21302
  }
20705
21303
  var FONT_WEIGHTS = ["Bold", "Regular"];
20706
21304
  var FONT_GLYPH_SIZES = [8, 16, 32, 64];
20707
21305
  var FONT_COST_GENERATIONS = 15;
20708
21306
  function fontOutputPaths(out) {
20709
- const base = path43.resolve(out.replace(/\.(ttf|png)$/i, ""));
21307
+ const base = path44.resolve(out.replace(/\.(ttf|png)$/i, ""));
20710
21308
  return { ttf: `${base}.ttf`, atlas: `${base}.png` };
20711
21309
  }
20712
21310
  async function generateFont(client2, opts) {
20713
21311
  const { ttf, atlas } = fontOutputPaths(opts.out);
20714
21312
  if (!opts.force) {
20715
21313
  for (const file of [ttf, atlas]) {
20716
- if (existsSync33(file)) throw new Error(`${file} already exists; pass --force to replace it`);
21314
+ if (existsSync34(file)) throw new Error(`${file} already exists; pass --force to replace it`);
20717
21315
  }
20718
21316
  }
20719
21317
  const sleep3 = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
@@ -20740,9 +21338,9 @@ async function generateFont(client2, opts) {
20740
21338
  client2.download(job.download_ttf_url),
20741
21339
  client2.download(job.download_atlas_url)
20742
21340
  ]);
20743
- await mkdir13(path43.dirname(ttf), { recursive: true });
20744
- await writeFile18(ttf, ttfBytes);
20745
- await writeFile18(atlas, atlasBytes);
21341
+ await mkdir14(path44.dirname(ttf), { recursive: true });
21342
+ await writeFile19(ttf, ttfBytes);
21343
+ await writeFile19(atlas, atlasBytes);
20746
21344
  return { jobId, ttf, atlas, glyphPx: job.glyph_px ?? null, usage: job.usage };
20747
21345
  }
20748
21346
  if (Date.now() - started > timeout) {
@@ -20756,14 +21354,14 @@ async function generateFont(client2, opts) {
20756
21354
  var PIXELATE_COST_GENERATIONS = 6;
20757
21355
  var PIXELATE_MAX_SIDE = 2048;
20758
21356
  function defaultPixelateOut(input) {
20759
- const ext = path43.extname(input);
21357
+ const ext = path44.extname(input);
20760
21358
  return `${ext ? input.slice(0, -ext.length) : input}.pixel.png`;
20761
21359
  }
20762
21360
  async function pixelateFile(client2, input, opts = {}) {
20763
- const out = path43.resolve(opts.out ?? defaultPixelateOut(input));
20764
- if (path43.resolve(input) === out) throw new Error("pixelate will not overwrite its own input; pass a different --out");
20765
- if (existsSync33(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
20766
- const bytes = await readFile29(input);
21361
+ const out = path44.resolve(opts.out ?? defaultPixelateOut(input));
21362
+ if (path44.resolve(input) === out) throw new Error("pixelate will not overwrite its own input; pass a different --out");
21363
+ if (existsSync34(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
21364
+ const bytes = await readFile30(input);
20767
21365
  const metadata = imageMetadata(bytes);
20768
21366
  if (!metadata) throw new Error(`${input} is not a readable PNG or JPEG`);
20769
21367
  const { width, height, format } = metadata;
@@ -20789,8 +21387,8 @@ async function pixelateFile(client2, input, opts = {}) {
20789
21387
  }
20790
21388
  const png = Buffer.from(first.base64, "base64");
20791
21389
  const size = imageMetadata(png);
20792
- await mkdir13(path43.dirname(out), { recursive: true });
20793
- await writeFile18(out, png);
21390
+ await mkdir14(path44.dirname(out), { recursive: true });
21391
+ await writeFile19(out, png);
20794
21392
  return {
20795
21393
  jobId,
20796
21394
  out,
@@ -20811,7 +21409,7 @@ async function pixelateFile(client2, input, opts = {}) {
20811
21409
 
20812
21410
  // src/cli/commands/pixellab-utils.ts
20813
21411
  function loadKeys(args) {
20814
- loadEnvFiles(path44.dirname(path44.resolve(args.manifest)));
21412
+ loadEnvFiles(path45.dirname(path45.resolve(args.manifest)));
20815
21413
  loadEnvFiles(process.cwd());
20816
21414
  }
20817
21415
  async function runUnzoom(args) {
@@ -20908,21 +21506,76 @@ async function runPixelate(args) {
20908
21506
  ` ${res.original.width}x${res.original.height}${scaled} \u2192 ${res.pixelated.width}x${res.pixelated.height}; wrote ${res.out}`
20909
21507
  );
20910
21508
  }
21509
+ async function runVfx(args) {
21510
+ if (args.subcommand === "presets" || args.subcommand === "parts") {
21511
+ if (args.dryRun) {
21512
+ log(` would list PixelLab's VFX ${args.subcommand}; nothing sent`);
21513
+ return;
21514
+ }
21515
+ loadKeys(args);
21516
+ log(await describeVfx(mcpClientFromEnv(), args.subcommand));
21517
+ return;
21518
+ }
21519
+ if (!args.out) throw new UsageError("vfx needs --out <base>: <base>.png, .gif, .json, and .vfx.json are written");
21520
+ const starts = [args.preset, args.recipe, args.effect].filter(Boolean);
21521
+ if (starts.length > 1) throw new UsageError("vfx takes one of --preset, --recipe, or --effect, not several");
21522
+ if (starts.length === 0 && args.parts.length === 0) {
21523
+ throw new UsageError("vfx needs something to start from: --preset <name>, --recipe <file>, --effect <id>, or --part <name>");
21524
+ }
21525
+ const paths = vfxOutputPaths(args.out);
21526
+ const fromRecipe = args.recipe ? await readVfxRecipe(args.recipe) : void 0;
21527
+ const sprite = args.sprite ?? fromRecipe?.sprite;
21528
+ const recipeName = typeof fromRecipe?.recipe.name === "string" ? fromRecipe.recipe.name : void 0;
21529
+ const name = args.name ?? (args.effect ? void 0 : recipeName ?? args.preset ?? path45.basename(paths.sheet, ".png"));
21530
+ const from = args.preset ? `preset ${args.preset}` : args.recipe ? `recipe ${args.recipe}` : args.effect ? `saved effect ${args.effect}` : "nothing";
21531
+ log(
21532
+ ` vfx ${name ? `"${name}"` : "(keeping its saved name)"} from ${from}${args.parts.length ? ` + ${args.parts.join(", ")}` : ""}${sprite ? ` around ${sprite}` : ""} \u2192 ${paths.sheet} (+ .gif, .json, .vfx.json${args.gamePack ? ", -game.zip" : ""})`
21533
+ );
21534
+ log(" cost: free on a PixelLab subscription (a renderer, not a generation); saved to your My effects");
21535
+ if (args.dryRun) {
21536
+ log(" dry run: nothing sent");
21537
+ return;
21538
+ }
21539
+ if (fromRecipe?.sprite && !args.sprite) log(` using the sprite the record names: ${fromRecipe.sprite}`);
21540
+ loadKeys(args);
21541
+ const res = await makeVfx(mcpClientFromEnv(), {
21542
+ preset: args.preset,
21543
+ recipe: fromRecipe?.recipe,
21544
+ effect: args.effect,
21545
+ parts: args.parts.map((part) => ({ name: part })),
21546
+ sprite,
21547
+ fps: args.fps,
21548
+ name: args.name,
21549
+ out: args.out,
21550
+ gamePack: args.gamePack,
21551
+ force: args.force,
21552
+ onProgress: (message6) => log(` ${message6}\u2026`)
21553
+ });
21554
+ if (args.json) {
21555
+ log(JSON.stringify({ version: 1, ...res }, null, 2));
21556
+ return;
21557
+ }
21558
+ log(` ${res.width}x${res.height}, ${res.frames} frames @ ${res.fps} fps, ${res.loop ? "loops" : "plays once"}`);
21559
+ for (const file of [res.paths.sheet, res.paths.gif, res.paths.atlas, res.paths.gamePack, res.paths.provenance]) {
21560
+ if (file) log(` wrote ${file}`);
21561
+ }
21562
+ if (res.editUrl) log(` tweak it with sliders: ${res.editUrl} (then: pixelkiln vfx --effect ${res.effectId} --out ${args.out} --force)`);
21563
+ }
20911
21564
 
20912
21565
  // src/cli/commands/quality.ts
20913
- import path46 from "path";
20914
- import { readFile as readFile31 } from "fs/promises";
21566
+ import path47 from "path";
21567
+ import { readFile as readFile32 } from "fs/promises";
20915
21568
 
20916
21569
  // src/pipeline/quality-regression.ts
20917
- import path45 from "path";
20918
- import { existsSync as existsSync34 } from "fs";
20919
- import { readFile as readFile30 } from "fs/promises";
21570
+ import path46 from "path";
21571
+ import { existsSync as existsSync35 } from "fs";
21572
+ import { readFile as readFile31 } from "fs/promises";
20920
21573
  import { z as z13 } from "zod";
20921
21574
  var SHA256_RE = /^[0-9a-f]{64}$/;
20922
21575
  var HEX_RE = /^#[0-9a-f]{6}$/;
20923
21576
  var MAX_REDMEAN_DISTANCE = 765;
20924
21577
  var QualityPathSchema = z13.string().min(1).refine(
20925
- (value) => !path45.posix.isAbsolute(value) && !value.includes("\\"),
21578
+ (value) => !path46.posix.isAbsolute(value) && !value.includes("\\"),
20926
21579
  "expected a portable relative path"
20927
21580
  );
20928
21581
  var DEFAULT_QUALITY_TOLERANCES = {
@@ -21069,10 +21722,10 @@ function measureDecodedImageQuality(png, digest2) {
21069
21722
  };
21070
21723
  }
21071
21724
  async function measureImageQuality(file) {
21072
- const absolute = path45.resolve(file);
21725
+ const absolute = path46.resolve(file);
21073
21726
  let bytes;
21074
21727
  try {
21075
- bytes = await readFile30(absolute);
21728
+ bytes = await readFile31(absolute);
21076
21729
  } catch (error) {
21077
21730
  throw new Error(`Cannot read quality image ${absolute}: ${message4(error)}`, { cause: error });
21078
21731
  }
@@ -21091,11 +21744,11 @@ function resolveQualityInputs(raw, inputsFilePath) {
21091
21744
  }
21092
21745
  const ids = parsed.data.map((entry) => entry.id);
21093
21746
  if (new Set(ids).size !== ids.length) throw new Error("--inputs contains a duplicate quality case id.");
21094
- const root = path45.dirname(path45.resolve(inputsFilePath));
21747
+ const root = path46.dirname(path46.resolve(inputsFilePath));
21095
21748
  return parsed.data.map((entry) => ({
21096
21749
  id: entry.id,
21097
- file: path45.resolve(root, entry.path),
21098
- ...entry.record ? { record: path45.resolve(root, entry.record) } : {},
21750
+ file: path46.resolve(root, entry.path),
21751
+ ...entry.record ? { record: path46.resolve(root, entry.record) } : {},
21099
21752
  tolerances: QualityToleranceSchema.parse({
21100
21753
  ...DEFAULT_QUALITY_TOLERANCES,
21101
21754
  ...entry.tolerances
@@ -21103,7 +21756,7 @@ function resolveQualityInputs(raw, inputsFilePath) {
21103
21756
  }));
21104
21757
  }
21105
21758
  function portableRelative2(from, to) {
21106
- return path45.relative(from, path45.resolve(to)).split(path45.sep).join("/") || ".";
21759
+ return path46.relative(from, path46.resolve(to)).split(path46.sep).join("/") || ".";
21107
21760
  }
21108
21761
  async function verifyRecordForImage(recordPath, imagePath) {
21109
21762
  const verification = await verifyArtifactBundle(recordPath);
@@ -21118,20 +21771,20 @@ async function verifyRecordForImage(recordPath, imagePath) {
21118
21771
  if (record.kind !== "refine" || options === null || typeof options !== "object" || options.schema !== "pixelkiln-quality") {
21119
21772
  throw new Error(`Quality record ${recordPath} is not a PixelKiln refinement record.`);
21120
21773
  }
21121
- const outputs = record.outputs.map((output) => path45.resolve(path45.dirname(recordPath), output.path));
21122
- if (!outputs.includes(path45.resolve(imagePath))) {
21774
+ const outputs = record.outputs.map((output) => path46.resolve(path46.dirname(recordPath), output.path));
21775
+ if (!outputs.includes(path46.resolve(imagePath))) {
21123
21776
  throw new Error(`Quality record ${recordPath} does not own ${imagePath}.`);
21124
21777
  }
21125
21778
  }
21126
21779
  async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
21127
21780
  if (!inputs.length) throw new Error("Cannot snapshot an empty quality baseline.");
21128
- const absolute = path45.resolve(baselinePath);
21781
+ const absolute = path46.resolve(baselinePath);
21129
21782
  for (const input of inputs) {
21130
- if (path45.resolve(input.file) === absolute || input.record && path45.resolve(input.record) === absolute) {
21783
+ if (path46.resolve(input.file) === absolute || input.record && path46.resolve(input.record) === absolute) {
21131
21784
  throw new Error(`Quality baseline output would overwrite an input: ${absolute}.`);
21132
21785
  }
21133
21786
  }
21134
- const root = path45.dirname(absolute);
21787
+ const root = path46.dirname(absolute);
21135
21788
  const cases = [];
21136
21789
  for (const input of [...inputs].sort((a, b) => a.id.localeCompare(b.id))) {
21137
21790
  const expected = await measureImageQuality(input.file);
@@ -21158,8 +21811,8 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
21158
21811
  cases
21159
21812
  });
21160
21813
  const data = Buffer.from(JSON.stringify(baseline, null, 2) + "\n");
21161
- if (existsSync34(absolute) && !options.force) {
21162
- const current = await readFile30(absolute);
21814
+ if (existsSync35(absolute) && !options.force) {
21815
+ const current = await readFile31(absolute);
21163
21816
  if (!current.equals(data)) {
21164
21817
  throw new OverwriteRefusedError(
21165
21818
  `Quality baseline already exists with different content: ${absolute}. Pass --force to replace it.`,
@@ -21171,10 +21824,10 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
21171
21824
  return { baseline, path: absolute, changed: result.changed.length > 0 };
21172
21825
  }
21173
21826
  async function readQualityBaseline(baselinePath) {
21174
- const absolute = path45.resolve(baselinePath);
21827
+ const absolute = path46.resolve(baselinePath);
21175
21828
  let raw;
21176
21829
  try {
21177
- raw = JSON.parse(await readFile30(absolute, "utf8"));
21830
+ raw = JSON.parse(await readFile31(absolute, "utf8"));
21178
21831
  } catch (error) {
21179
21832
  throw new Error(`Could not read quality baseline ${absolute}: ${message4(error)}`, { cause: error });
21180
21833
  }
@@ -21244,12 +21897,12 @@ function regressionViolations(expected, actual, tolerances) {
21244
21897
  return { violations, warnings };
21245
21898
  }
21246
21899
  async function checkQualityBaseline(baselinePath) {
21247
- const absolute = path45.resolve(baselinePath);
21900
+ const absolute = path46.resolve(baselinePath);
21248
21901
  const baseline = await readQualityBaseline(absolute);
21249
- const root = path45.dirname(absolute);
21902
+ const root = path46.dirname(absolute);
21250
21903
  const cases = [];
21251
21904
  for (const entry of baseline.cases) {
21252
- const file = path45.resolve(root, entry.file);
21905
+ const file = path46.resolve(root, entry.file);
21253
21906
  let actual = null;
21254
21907
  const violations = [];
21255
21908
  const warnings = [];
@@ -21262,7 +21915,7 @@ async function checkQualityBaseline(baselinePath) {
21262
21915
  violations.push(message4(error));
21263
21916
  }
21264
21917
  if (entry.record) {
21265
- const recordPath = path45.resolve(root, entry.record.path);
21918
+ const recordPath = path46.resolve(root, entry.record.path);
21266
21919
  try {
21267
21920
  const recordHash = await sha256File(recordPath);
21268
21921
  if (recordHash !== entry.record.sha256) violations.push("quality record hash changed");
@@ -21304,15 +21957,15 @@ async function runQuality(args) {
21304
21957
  if (!args.out) throw new Error("quality snapshot needs --out <pixelkiln.quality.json>.");
21305
21958
  let raw;
21306
21959
  try {
21307
- raw = JSON.parse(await readFile31(path46.resolve(args.inputs), "utf8"));
21960
+ raw = JSON.parse(await readFile32(path47.resolve(args.inputs), "utf8"));
21308
21961
  } catch (error) {
21309
21962
  throw new Error(
21310
- `Could not read quality inputs ${path46.resolve(args.inputs)}: ${error instanceof Error ? error.message : String(error)}`,
21963
+ `Could not read quality inputs ${path47.resolve(args.inputs)}: ${error instanceof Error ? error.message : String(error)}`,
21311
21964
  { cause: error }
21312
21965
  );
21313
21966
  }
21314
21967
  const inputs = resolveQualityInputs(raw, args.inputs);
21315
- if (path46.resolve(args.inputs) === path46.resolve(args.out)) {
21968
+ if (path47.resolve(args.inputs) === path47.resolve(args.out)) {
21316
21969
  throw new Error("quality snapshot --out must not overwrite its --inputs file.");
21317
21970
  }
21318
21971
  const result = await snapshotQualityBaseline(inputs, args.out, { force: args.force });
@@ -21325,7 +21978,7 @@ async function runQuality(args) {
21325
21978
  }, null, 2));
21326
21979
  } else {
21327
21980
  log(
21328
- ` ${result.changed ? "wrote" : "unchanged"} ${path46.relative(process.cwd(), result.path)} (${result.baseline.cases.length} case(s))`
21981
+ ` ${result.changed ? "wrote" : "unchanged"} ${path47.relative(process.cwd(), result.path)} (${result.baseline.cases.length} case(s))`
21329
21982
  );
21330
21983
  log(` Review the tolerances, commit the baseline, then run quality check in CI.`);
21331
21984
  }
@@ -21351,12 +22004,12 @@ async function runQuality(args) {
21351
22004
  }
21352
22005
 
21353
22006
  // src/cli/commands/recipe.ts
21354
- import path48 from "path";
22007
+ import path49 from "path";
21355
22008
 
21356
22009
  // src/recipes.ts
21357
- import path47 from "path";
21358
- import { existsSync as existsSync35 } from "fs";
21359
- import { readdir as readdir3, readFile as readFile32 } from "fs/promises";
22010
+ import path48 from "path";
22011
+ import { existsSync as existsSync36 } from "fs";
22012
+ import { readdir as readdir3, readFile as readFile33 } from "fs/promises";
21360
22013
  import { fileURLToPath as fileURLToPath2 } from "url";
21361
22014
  import { z as z14 } from "zod";
21362
22015
  var SHA256_RE2 = /^[0-9a-f]{64}$/;
@@ -21365,7 +22018,7 @@ var VERSION_RE = /^\d+\.\d+\.\d+$/;
21365
22018
  var RECIPE_FILE = "pixelkiln.recipe.json";
21366
22019
  var RECIPE_DIR_TOKEN = "{{recipeDir}}";
21367
22020
  var PortablePathSchema = z14.string().min(1).refine((value) => {
21368
- if (value.includes("\\") || path47.posix.isAbsolute(value)) return false;
22021
+ if (value.includes("\\") || path48.posix.isAbsolute(value)) return false;
21369
22022
  const parts = value.split("/");
21370
22023
  return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
21371
22024
  }, "expected a portable relative path without '.', '..', or backslashes");
@@ -21541,10 +22194,10 @@ function message5(error) {
21541
22194
  return error instanceof Error ? error.message : String(error);
21542
22195
  }
21543
22196
  async function readRecipeFile(recipePath, bundled) {
21544
- const absolute = path47.resolve(recipePath);
22197
+ const absolute = path48.resolve(recipePath);
21545
22198
  let raw;
21546
22199
  try {
21547
- raw = JSON.parse(await readFile32(absolute, "utf8"));
22200
+ raw = JSON.parse(await readFile33(absolute, "utf8"));
21548
22201
  } catch (error) {
21549
22202
  throw new Error(`Could not read recipe ${absolute}: ${message5(error)}`, { cause: error });
21550
22203
  }
@@ -21555,13 +22208,13 @@ async function readRecipeFile(recipePath, bundled) {
21555
22208
  ` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
21556
22209
  );
21557
22210
  }
21558
- return { recipe: parsed.data, authored: raw, path: absolute, dir: path47.dirname(absolute), bundled };
22211
+ return { recipe: parsed.data, authored: raw, path: absolute, dir: path48.dirname(absolute), bundled };
21559
22212
  }
21560
22213
  async function walkRecipeFiles(root) {
21561
- if (!existsSync35(root)) return [];
22214
+ if (!existsSync36(root)) return [];
21562
22215
  const out = [];
21563
22216
  for (const entry of await readdir3(root, { withFileTypes: true })) {
21564
- const resolved = path47.join(root, entry.name);
22217
+ const resolved = path48.join(root, entry.name);
21565
22218
  if (entry.isDirectory()) out.push(...await walkRecipeFiles(resolved));
21566
22219
  else if (entry.isFile() && entry.name === RECIPE_FILE) out.push(resolved);
21567
22220
  }
@@ -21572,7 +22225,7 @@ function bundledRecipeRoot() {
21572
22225
  }
21573
22226
  async function listBundledRecipes(root = bundledRecipeRoot()) {
21574
22227
  const recipes = await Promise.all(
21575
- (await walkRecipeFiles(path47.resolve(root))).map((file) => readRecipeFile(file, true))
22228
+ (await walkRecipeFiles(path48.resolve(root))).map((file) => readRecipeFile(file, true))
21576
22229
  );
21577
22230
  return recipes.sort(
21578
22231
  (a, b) => a.recipe.id.localeCompare(b.recipe.id) || compareVersions(b.recipe.version, a.recipe.version)
@@ -21595,9 +22248,9 @@ function parseSelector(selector) {
21595
22248
  return { id: match[1], version: match[2] };
21596
22249
  }
21597
22250
  async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
21598
- const local = path47.resolve(target);
21599
- if (existsSync35(local)) {
21600
- const recipePath = path47.basename(local) === RECIPE_FILE ? local : path47.join(local, RECIPE_FILE);
22251
+ const local = path48.resolve(target);
22252
+ if (existsSync36(local)) {
22253
+ const recipePath = path48.basename(local) === RECIPE_FILE ? local : path48.join(local, RECIPE_FILE);
21601
22254
  return readRecipeFile(recipePath, false);
21602
22255
  }
21603
22256
  if (target.startsWith(".") || target.startsWith("/") || target.includes("\\")) {
@@ -21611,8 +22264,8 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
21611
22264
  return matches[0];
21612
22265
  }
21613
22266
  async function verifyFile(root, file) {
21614
- const absolute = path47.resolve(root, ...file.path.split("/"));
21615
- if (!existsSync35(absolute)) {
22267
+ const absolute = path48.resolve(root, ...file.path.split("/"));
22268
+ if (!existsSync36(absolute)) {
21616
22269
  return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
21617
22270
  }
21618
22271
  const actualSha256 = await sha256File(absolute);
@@ -21627,7 +22280,7 @@ async function verifyRecipe(target, options = {}) {
21627
22280
  const loaded = await resolveRecipe(target, options.bundledRoot);
21628
22281
  const actualIntegrity = recipeDigest(loaded.authored);
21629
22282
  const files = await Promise.all(loaded.recipe.files.map((file) => verifyFile(loaded.dir, file)));
21630
- const modelRoot = options.modelRoot ? path47.resolve(options.modelRoot) : null;
22283
+ const modelRoot = options.modelRoot ? path48.resolve(options.modelRoot) : null;
21631
22284
  const models = modelRoot ? await Promise.all(loaded.recipe.models.map(async (model) => ({
21632
22285
  ...await verifyFile(modelRoot, model),
21633
22286
  source: model.source,
@@ -21661,7 +22314,7 @@ async function verifyRecipe(target, options = {}) {
21661
22314
  };
21662
22315
  }
21663
22316
  function renderStyle(style, destination, cwd) {
21664
- const portableDir = path47.relative(cwd, destination).split(path47.sep).join("/") || ".";
22317
+ const portableDir = path48.relative(cwd, destination).split(path48.sep).join("/") || ".";
21665
22318
  const replace = (value) => {
21666
22319
  if (typeof value === "string") return value.replaceAll(RECIPE_DIR_TOKEN, portableDir);
21667
22320
  if (Array.isArray(value)) return value.map(replace);
@@ -21673,25 +22326,25 @@ function renderStyle(style, destination, cwd) {
21673
22326
  return replace(style);
21674
22327
  }
21675
22328
  async function installRecipe(target, options = {}) {
21676
- const cwd = path47.resolve(options.cwd ?? process.cwd());
22329
+ const cwd = path48.resolve(options.cwd ?? process.cwd());
21677
22330
  const loaded = await resolveRecipe(target, options.bundledRoot);
21678
22331
  const verification = await verifyRecipe(loaded.path);
21679
22332
  if (!verification.ok) {
21680
22333
  throw new Error(`Recipe ${loaded.recipe.id}@${loaded.recipe.version} failed integrity verification.`);
21681
22334
  }
21682
- const destination = options.out ? path47.resolve(cwd, options.out) : path47.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
22335
+ const destination = options.out ? path48.resolve(cwd, options.out) : path48.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
21683
22336
  const sources = [
21684
22337
  { path: RECIPE_FILE, source: loaded.path },
21685
- ...loaded.recipe.files.map((file) => ({ path: file.path, source: path47.join(loaded.dir, ...file.path.split("/")) }))
22338
+ ...loaded.recipe.files.map((file) => ({ path: file.path, source: path48.join(loaded.dir, ...file.path.split("/")) }))
21686
22339
  ];
21687
22340
  const files = await Promise.all(sources.map(async (file) => ({
21688
- path: path47.join(destination, ...file.path.split("/")),
21689
- data: await readFile32(file.source)
22341
+ path: path48.join(destination, ...file.path.split("/")),
22342
+ data: await readFile33(file.source)
21690
22343
  })));
21691
22344
  if (!options.force) {
21692
22345
  for (const file of files) {
21693
- if (!existsSync35(file.path)) continue;
21694
- const current = await readFile32(file.path);
22346
+ if (!existsSync36(file.path)) continue;
22347
+ const current = await readFile33(file.path);
21695
22348
  if (!current.equals(file.data)) {
21696
22349
  throw new OverwriteRefusedError(
21697
22350
  `Recipe destination has local changes: ${file.path}. Choose another --out or pass --force to replace declared recipe files.`,
@@ -21785,14 +22438,14 @@ async function runRecipe(args) {
21785
22438
  }, null, 2));
21786
22439
  } else {
21787
22440
  log(` installed ${result.recipe.id}@${result.recipe.version}`);
21788
- log(` ${path48.relative(process.cwd(), result.destination) || "."}`);
22441
+ log(` ${path49.relative(process.cwd(), result.destination) || "."}`);
21789
22442
  log(`
21790
22443
  Add this entry under your manifest's styles object:`);
21791
22444
  log(JSON.stringify({ [result.styleId]: result.style }, null, 2));
21792
22445
  if (result.recipe.models.length) {
21793
22446
  log(`
21794
22447
  Models are not downloaded automatically. Verify them with:`);
21795
- log(` pixelkiln recipe verify ${path48.relative(process.cwd(), result.destination)} --model-root <ComfyUI/models>`);
22448
+ log(` pixelkiln recipe verify ${path49.relative(process.cwd(), result.destination)} --model-root <ComfyUI/models>`);
21796
22449
  }
21797
22450
  }
21798
22451
  return;
@@ -21800,7 +22453,7 @@ async function runRecipe(args) {
21800
22453
  }
21801
22454
 
21802
22455
  // src/cli/commands/refine.ts
21803
- import path49 from "path";
22456
+ import path50 from "path";
21804
22457
  async function runRefine(args) {
21805
22458
  if (args.from || args.subcommand === "approve") {
21806
22459
  if (!args.from) {
@@ -21830,12 +22483,12 @@ async function runRefine(args) {
21830
22483
  ` recovered ${result3.detection.columns}x${result3.detection.rows} native grid (${result3.detection.confidence}: ${result3.detection.consensus})`
21831
22484
  );
21832
22485
  log(` enforced ${result3.palette.length}-color palette without dithering`);
21833
- log(` wrote ${path49.relative(process.cwd(), result3.output)}`);
21834
- log(` quality record: ${path49.relative(process.cwd(), result3.record)}`);
22486
+ log(` wrote ${path50.relative(process.cwd(), result3.output)}`);
22487
+ log(` quality record: ${path50.relative(process.cwd(), result3.record)}`);
21835
22488
  log(`
21836
22489
  Automated checks passed. Human 1\xD7 review is still required:`);
21837
22490
  log(
21838
- ` pixelkiln refine approve --from ${path49.relative(process.cwd(), result3.record)} --reviewer "Your Name"`
22491
+ ` pixelkiln refine approve --from ${path50.relative(process.cwd(), result3.record)} --reviewer "Your Name"`
21839
22492
  );
21840
22493
  }
21841
22494
  return;
@@ -21863,7 +22516,7 @@ async function runRefine(args) {
21863
22516
  if (args.json) log(JSON.stringify(result3, null, 2));
21864
22517
  else {
21865
22518
  log(
21866
- result3.options.frameSet ? ` approved ${result3.outputs.length} frames in ${path49.relative(process.cwd(), result3.record)} by ${args.reviewer.trim()}` : ` approved ${path49.relative(process.cwd(), result3.output)} by ${args.reviewer.trim()}`
22519
+ result3.options.frameSet ? ` approved ${result3.outputs.length} frames in ${path50.relative(process.cwd(), result3.record)} by ${args.reviewer.trim()}` : ` approved ${path50.relative(process.cwd(), result3.output)} by ${args.reviewer.trim()}`
21867
22520
  );
21868
22521
  }
21869
22522
  return;
@@ -21872,7 +22525,7 @@ async function runRefine(args) {
21872
22525
  if (args.json) {
21873
22526
  log(JSON.stringify(result2, null, 2));
21874
22527
  } else {
21875
- log(` ${result2.safe ? "release-ready" : "not release-ready"}: ${path49.relative(process.cwd(), result2.output || result2.record)}`);
22528
+ log(` ${result2.safe ? "release-ready" : "not release-ready"}: ${path50.relative(process.cwd(), result2.output || result2.record)}`);
21876
22529
  for (const reason of result2.reasons) log(` ${reason}`);
21877
22530
  }
21878
22531
  if (!result2.safe) process.exitCode = 1;
@@ -21918,7 +22571,7 @@ async function runRefine(args) {
21918
22571
  log(` ${item.state.padEnd(16)} ${item.key} ${item.reason}`);
21919
22572
  if (item.state === "needs-approval") {
21920
22573
  log(
21921
- ` pixelkiln refine approve --from ${path49.relative(process.cwd(), item.record)} --reviewer "Your Name"`
22574
+ ` pixelkiln refine approve --from ${path50.relative(process.cwd(), item.record)} --reviewer "Your Name"`
21922
22575
  );
21923
22576
  }
21924
22577
  }
@@ -21929,9 +22582,9 @@ async function runRefine(args) {
21929
22582
  }
21930
22583
 
21931
22584
  // src/cli/commands/skeleton.ts
21932
- import path50 from "path";
21933
- import { existsSync as existsSync36 } from "fs";
21934
- import { readFile as readFile33, writeFile as writeFile19 } from "fs/promises";
22585
+ import path51 from "path";
22586
+ import { existsSync as existsSync37 } from "fs";
22587
+ import { readFile as readFile34, writeFile as writeFile20 } from "fs/promises";
21935
22588
 
21936
22589
  // src/skeleton-preview.ts
21937
22590
  var RIGHT = [255, 140, 40, 255];
@@ -22042,12 +22695,12 @@ var Canvas = class {
22042
22695
  async function runEstimateSkeleton(args) {
22043
22696
  const image = args.target;
22044
22697
  if (!image) throw new UsageError("estimate-skeleton needs an image path: pixelkiln estimate-skeleton <image> [--out keypoints.json]");
22045
- if (args.out && existsSync36(path50.resolve(args.out)) && !args.force) {
22698
+ if (args.out && existsSync37(path51.resolve(args.out)) && !args.force) {
22046
22699
  throw new UsageError(`${args.out} already exists; pass --force to replace it`);
22047
22700
  }
22048
- loadEnvFiles(path50.dirname(path50.resolve(args.manifest)));
22701
+ loadEnvFiles(path51.dirname(path51.resolve(args.manifest)));
22049
22702
  loadEnvFiles(process.cwd());
22050
- const bytes = await readFile33(path50.resolve(image));
22703
+ const bytes = await readFile34(path51.resolve(image));
22051
22704
  const metadata = imageMetadata(bytes);
22052
22705
  if (!metadata) throw new UsageError(`${image} is not a readable PNG or JPEG`);
22053
22706
  const sizeProblem = estimateSkeletonSizeProblem(metadata.width, metadata.height);
@@ -22059,7 +22712,7 @@ async function runEstimateSkeleton(args) {
22059
22712
  const frames = args.frames ?? DEFAULT_SCAFFOLD_FRAMES;
22060
22713
  const json = JSON.stringify(scaffoldSkeletonSet(res.keypoints, frames), null, 2);
22061
22714
  if (args.out) {
22062
- await writeFile19(path50.resolve(args.out), json + "\n");
22715
+ await writeFile20(path51.resolve(args.out), json + "\n");
22063
22716
  log(` wrote ${args.out}: the estimated pose and ${frames} frames to edit`);
22064
22717
  log(` preview it with: pixelkiln skeleton-preview ${args.out} --from ${image}`);
22065
22718
  } else {
@@ -22072,8 +22725,8 @@ async function runSkeletonPreview(args) {
22072
22725
  let sourceFile;
22073
22726
  let label;
22074
22727
  if (target.endsWith(".json")) {
22075
- keypointsFile = path50.resolve(target);
22076
- sourceFile = args.from ? path50.resolve(args.from) : void 0;
22728
+ keypointsFile = path51.resolve(target);
22729
+ sourceFile = args.from ? path51.resolve(args.from) : void 0;
22077
22730
  label = target;
22078
22731
  } else {
22079
22732
  const loaded = await loadManifest(args.manifest);
@@ -22085,31 +22738,31 @@ async function runSkeletonPreview(args) {
22085
22738
  );
22086
22739
  }
22087
22740
  keypointsFile = spec.revision.keypointsFile;
22088
- sourceFile = args.from ? path50.resolve(args.from) : spec.revision.sourceFile;
22741
+ sourceFile = args.from ? path51.resolve(args.from) : spec.revision.sourceFile;
22089
22742
  label = `${spec.styleId}/${spec.assetId}`;
22090
22743
  }
22091
- if (!existsSync36(keypointsFile)) throw new UsageError(`Keypoints file does not exist yet: ${keypointsFile}`);
22744
+ if (!existsSync37(keypointsFile)) throw new UsageError(`Keypoints file does not exist yet: ${keypointsFile}`);
22092
22745
  let json;
22093
22746
  try {
22094
- json = JSON.parse(await readFile33(keypointsFile, "utf8"));
22747
+ json = JSON.parse(await readFile34(keypointsFile, "utf8"));
22095
22748
  } catch (error) {
22096
22749
  throw new UsageError(`${keypointsFile} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
22097
22750
  }
22098
22751
  const set = parseSkeletonSet(json, keypointsFile);
22099
22752
  let source;
22100
22753
  if (sourceFile) {
22101
- if (!existsSync36(sourceFile)) {
22754
+ if (!existsSync37(sourceFile)) {
22102
22755
  log(` source image does not exist yet, drawing on a plain ground: ${sourceFile}`);
22103
22756
  } else {
22104
- const bytes = await readFile33(sourceFile);
22757
+ const bytes = await readFile34(sourceFile);
22105
22758
  if (imageMetadata(bytes)?.format !== "png") throw new UsageError(`${sourceFile} must be a PNG to preview over`);
22106
22759
  source = decodePng(bytes);
22107
22760
  }
22108
22761
  }
22109
- const out = path50.resolve(args.out ?? keypointsFile.replace(/\.json$/, "") + ".preview.png");
22110
- await writeFile19(out, renderSkeletonSheet(set, { source }));
22762
+ const out = path51.resolve(args.out ?? keypointsFile.replace(/\.json$/, "") + ".preview.png");
22763
+ await writeFile20(out, renderSkeletonSheet(set, { source }));
22111
22764
  log(` ${label}: ${set.frames.length} frames after the starting pose`);
22112
- const shown = path50.relative(process.cwd(), out);
22765
+ const shown = path51.relative(process.cwd(), out);
22113
22766
  log(` wrote ${shown.startsWith("..") ? out : shown}`);
22114
22767
  }
22115
22768
 
@@ -22155,15 +22808,15 @@ async function runTools(args) {
22155
22808
  }
22156
22809
 
22157
22810
  // src/cli/commands/workspace.ts
22158
- import path51 from "path";
22159
- import { existsSync as existsSync37 } from "fs";
22811
+ import path52 from "path";
22812
+ import { existsSync as existsSync38 } from "fs";
22160
22813
  async function runWorkspace(args) {
22161
- const workspacePath = path51.resolve(args.workspace ?? "pixelkiln.workspace.json");
22162
- const dir = path51.dirname(workspacePath);
22814
+ const workspacePath = path52.resolve(args.workspace ?? "pixelkiln.workspace.json");
22815
+ const dir = path52.dirname(workspacePath);
22163
22816
  if (args.subcommand === "add") {
22164
- const manifestPath = path51.resolve(args.target);
22817
+ const manifestPath = path52.resolve(args.target);
22165
22818
  const loadedTarget = await loadManifest(manifestPath);
22166
- const lockPath = args.explicitLock ? path51.resolve(args.explicitLock) : path51.join(path51.dirname(manifestPath), "pixelkiln.lock.json");
22819
+ const lockPath = args.explicitLock ? path52.resolve(args.explicitLock) : path52.join(path52.dirname(manifestPath), "pixelkiln.lock.json");
22167
22820
  const ws = await loadWorkspace(workspacePath);
22168
22821
  const id = args.name ?? loadedTarget.manifest.name;
22169
22822
  if (ws.projects.some((p) => p.id === id)) {
@@ -22183,10 +22836,10 @@ async function runWorkspace(args) {
22183
22836
  ...args.account ? { account: args.account } : {}
22184
22837
  };
22185
22838
  await saveWorkspace(workspacePath, { version: 1, projects: [...ws.projects, project] });
22186
- log(` registered "${id}" in ${path51.relative(process.cwd(), workspacePath)}`);
22839
+ log(` registered "${id}" in ${path52.relative(process.cwd(), workspacePath)}`);
22187
22840
  log(` manifest: ${project.manifest}`);
22188
22841
  log(` lock: ${project.lock}`);
22189
- if (!existsSync37(lockPath)) {
22842
+ if (!existsSync38(lockPath)) {
22190
22843
  log(
22191
22844
  ` warning: no lockfile there yet; this project contributes no claims until one is generated`
22192
22845
  );
@@ -22195,7 +22848,7 @@ async function runWorkspace(args) {
22195
22848
  }
22196
22849
  if (args.subcommand === "remove") {
22197
22850
  const ws = await loadWorkspace(workspacePath);
22198
- const resolvedTarget = path51.resolve(args.target);
22851
+ const resolvedTarget = path52.resolve(args.target);
22199
22852
  const match = ws.projects.find(
22200
22853
  (p) => p.id === args.target || resolveProject(dir, p).manifestPath === resolvedTarget
22201
22854
  );
@@ -22206,11 +22859,11 @@ async function runWorkspace(args) {
22206
22859
  version: 1,
22207
22860
  projects: ws.projects.filter((p) => p !== match)
22208
22861
  });
22209
- log(` removed "${match.id}" from ${path51.relative(process.cwd(), workspacePath)}`);
22862
+ log(` removed "${match.id}" from ${path52.relative(process.cwd(), workspacePath)}`);
22210
22863
  return;
22211
22864
  }
22212
22865
  if (args.subcommand === "list") {
22213
- if (!existsSync37(workspacePath)) {
22866
+ if (!existsSync38(workspacePath)) {
22214
22867
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
22215
22868
  }
22216
22869
  const ws = await loadWorkspace(workspacePath);
@@ -22218,9 +22871,9 @@ async function runWorkspace(args) {
22218
22871
  if (args.json) {
22219
22872
  log(JSON.stringify({ version: 1, workspace: workspacePath, projects: ws.projects, diagnostics }, null, 2));
22220
22873
  } else if (!ws.projects.length) {
22221
- log(` no projects registered in ${path51.relative(process.cwd(), workspacePath)}`);
22874
+ log(` no projects registered in ${path52.relative(process.cwd(), workspacePath)}`);
22222
22875
  } else {
22223
- log(` ${ws.projects.length} project(s) in ${path51.relative(process.cwd(), workspacePath)}:`);
22876
+ log(` ${ws.projects.length} project(s) in ${path52.relative(process.cwd(), workspacePath)}:`);
22224
22877
  for (const p of ws.projects) {
22225
22878
  log(` ${p.id.padEnd(24)} ${p.manifest.padEnd(40)} (${p.provider}${p.account ? `, ${p.account}` : ""})`);
22226
22879
  }
@@ -22230,7 +22883,7 @@ async function runWorkspace(args) {
22230
22883
  return;
22231
22884
  }
22232
22885
  if (args.subcommand === "status") {
22233
- if (!existsSync37(workspacePath)) {
22886
+ if (!existsSync38(workspacePath)) {
22234
22887
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
22235
22888
  }
22236
22889
  const ws = await loadWorkspace(workspacePath);
@@ -22238,7 +22891,7 @@ async function runWorkspace(args) {
22238
22891
  if (args.json) {
22239
22892
  log(JSON.stringify({ ...report, workspace: workspacePath }, null, 2));
22240
22893
  } else {
22241
- log(` workspace: ${path51.relative(process.cwd(), workspacePath)}`);
22894
+ log(` workspace: ${path52.relative(process.cwd(), workspacePath)}`);
22242
22895
  for (const p of report.projects) {
22243
22896
  if (p.error) {
22244
22897
  log(`
@@ -22292,8 +22945,8 @@ async function runHelp() {
22292
22945
  async function runVersion() {
22293
22946
  for (const up of ["../package.json", "../../package.json"]) {
22294
22947
  const url = new URL(up, import.meta.url);
22295
- if (!existsSync38(url)) continue;
22296
- const pkg = JSON.parse(await readFile34(url, "utf8"));
22948
+ if (!existsSync39(url)) continue;
22949
+ const pkg = JSON.parse(await readFile35(url, "utf8"));
22297
22950
  if (pkg.name === "pixelkiln") {
22298
22951
  log(`${pkg.name} ${pkg.version}`);
22299
22952
  return;
@@ -22341,6 +22994,7 @@ var RUNNERS = {
22341
22994
  "skeleton-preview": runSkeletonPreview,
22342
22995
  unzoom: runUnzoom,
22343
22996
  pixelate: runPixelate,
22997
+ vfx: runVfx,
22344
22998
  font: runFont
22345
22999
  };
22346
23000
  async function main(argv = process.argv.slice(2)) {