pixelkiln 0.86.3 → 0.88.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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",
@@ -162,6 +169,8 @@ var COMMANDS = [
162
169
  "estimate-skeleton",
163
170
  "skeleton-preview",
164
171
  "unzoom",
172
+ "pixelate",
173
+ "vfx",
165
174
  "font",
166
175
  "help",
167
176
  "--help",
@@ -176,6 +185,7 @@ var QUALITY_SUBCOMMANDS = ["snapshot", "check"];
176
185
  var EDIT_SUBCOMMANDS = ["start", "detach"];
177
186
  var TOOLS_SUBCOMMANDS = ["status", "install"];
178
187
  var TOOLS = ["editor"];
188
+ var VFX_SUBCOMMANDS = ["make", "presets", "parts"];
179
189
  function parseArgs(argv) {
180
190
  const [command = "help"] = argv;
181
191
  if (!COMMANDS.includes(command)) {
@@ -265,6 +275,12 @@ function parseArgs(argv) {
265
275
  throw new UsageError("skeleton-preview needs a keypoints file or an animate-skeleton asset id");
266
276
  }
267
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);
268
284
  } else if (command === "refine") {
269
285
  subcommand = rest[0]?.startsWith("-") || rest[0] === void 0 ? "run" : rest[0];
270
286
  if (!REFINE_SUBCOMMANDS.includes(subcommand)) {
@@ -432,7 +448,14 @@ function parseArgs(argv) {
432
448
  weight: get("--weight"),
433
449
  glyphPx: numberOption("--glyph-px", { min: 8, max: 64, integer: true }),
434
450
  frames: numberOption("--frames", { min: 3, max: 15, integer: true }),
435
- 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")
436
459
  };
437
460
  }
438
461
 
@@ -499,8 +522,16 @@ Commands
499
522
  add/remove/list/status/claims. Offline.
500
523
  unzoom PixelLab: shrink an upscaled image (--from) back to its native
501
524
  pixel grid before using it as a style or reference image.
525
+ pixelate PixelLab: convert a photo, painting, or render (--from) into
526
+ pixel art with Image to Pixel Art Pro Flash; PixelLab picks the
527
+ output size. Optional --description steers the style. 6
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.
502
533
  font PixelLab: generate a pixel font (--description, --out); writes a
503
- .ttf and a glyph atlas PNG. 25 generations; asks first.
534
+ .ttf and a glyph atlas PNG. 15 generations; asks first.
504
535
 
505
536
  Options
506
537
  --columns <n> pack/export: sprites or tiles per row (default: near-square)
@@ -523,8 +554,16 @@ Options
523
554
  --model-root <dir> recipe verify: also hash required local model files
524
555
  --generation <n|hash> restore: the previous generation to bring back (1 = newest)
525
556
  --quantize <n> unzoom: 0 auto palette (default), -1 keep all colors, 2-256 exact
526
- --description <text> font: the style to draw, e.g. "warm orange arcade font"
557
+ --description <text> font: the style to draw, e.g. "warm orange arcade font";
558
+ pixelate: an optional style steer, e.g. "light dithering"
527
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
528
567
  --glyph-px <n> font: native glyph size, 8, 16 (default), 32, or 64
529
568
  --prune cache: remove invalid/unreferenced local cache data
530
569
  --manifest <path> Default: pixelkiln.manifest.json
@@ -583,6 +622,8 @@ Examples
583
622
  pixelkiln quality snapshot --inputs quality-inputs.json --out pixelkiln.quality.json
584
623
  pixelkiln quality check --from pixelkiln.quality.json
585
624
  pixelkiln unzoom --from refs/knight-512.png --out refs/knight.png
625
+ pixelkiln pixelate --from renders/lodge.png --out art/lodge.png
626
+ pixelkiln vfx --preset explosion --part scorch_mark --out fx/explosion --game-pack
586
627
  pixelkiln font --description "warm orange arcade font" --weight Bold --out fonts/arcade
587
628
  pixelkiln workspace add ../other-game/pixelkiln.manifest.json
588
629
  pixelkiln workspace status --json
@@ -665,16 +706,17 @@ import { createReadStream } from "fs";
665
706
  function sha256(data) {
666
707
  return createHash("sha256").update(data).digest("hex");
667
708
  }
668
- async function sha256File(path52) {
709
+ async function sha256File(path53) {
669
710
  return new Promise((resolve, reject) => {
670
711
  const hash = createHash("sha256");
671
- const stream = createReadStream(path52);
712
+ const stream = createReadStream(path53);
672
713
  stream.on("data", (chunk2) => hash.update(chunk2));
673
714
  stream.on("error", reject);
674
715
  stream.on("end", () => resolve(hash.digest("hex")));
675
716
  });
676
717
  }
677
718
  function specHash(spec, styleImageHashes, providerOptionIdentity = spec.providerOptions, providerInputIdentity = spec.providerInputs ?? {}) {
719
+ const effect = Boolean(spec.vfx);
678
720
  return sha256(
679
721
  JSON.stringify({
680
722
  // Preserve every existing PixelLab hash while making a provider switch
@@ -683,15 +725,15 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
683
725
  providerOptions: providerOptionIdentity && (typeof providerOptionIdentity !== "object" || Object.keys(providerOptionIdentity).length > 0) ? providerOptionIdentity : void 0,
684
726
  providerInputs: providerInputIdentity && (typeof providerInputIdentity !== "object" || Object.keys(providerInputIdentity).length > 0) ? providerInputIdentity : void 0,
685
727
  generator: spec.generator,
686
- prompt: spec.prompt,
687
- width: spec.width,
688
- height: spec.height,
689
- view: spec.view,
690
- outline: spec.outline ?? null,
691
- shading: spec.shading ?? null,
692
- detail: spec.detail ?? null,
693
- seed: spec.seed ?? null,
694
- 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,
695
737
  // `noBackground` only reaches the wire for pixflux and uiAsset; the
696
738
  // tile fields are undefined for every other generator. `tileSize` is
697
739
  // intentionally absent; width/height are derived from it, so it is
@@ -766,6 +808,15 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
766
808
  styleTraits: spec.objectPro.styleTraits,
767
809
  reference: spec.objectPro.reference?.sha256
768
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,
769
820
  // A mirror's bytes come from its source's recorded outputs; the plan
770
821
  // compares those directly, so only the choice of source is identity.
771
822
  mirror: spec.mirror ? { of: spec.mirror.sourceAssetId } : void 0,
@@ -815,7 +866,7 @@ import path2 from "path";
815
866
  // src/types.ts
816
867
  import { z } from "zod";
817
868
  var MediaTypeSchema = z.enum(["image/png", "image/gif"]);
818
- 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"]);
819
870
  var GridConfidenceSchema = z.enum(["low", "medium", "high"]);
820
871
  var RevisionModeSchema = z.enum([
821
872
  "image-to-image",
@@ -848,18 +899,26 @@ function candidateCount(size) {
848
899
  return 1;
849
900
  }
850
901
  function generationCost(width, height, generator = "map") {
902
+ if (generator === "vfx") return 0;
851
903
  if (generator === "map" || generator === "pixflux") return 1;
852
- if (generator === "imagePro") return 40;
904
+ if (generator === "imagePro") return proTierCost(width, height);
853
905
  const px = width * height;
854
- if (px <= 1024) return 20;
855
- if (px <= 2048) return 25;
856
- return 40;
906
+ if (px <= 1024) return PRO_TIER_PRICES[0];
907
+ if (px <= 2048) return PRO_TIER_PRICES[1];
908
+ return PRO_TIER_PRICES[2];
909
+ }
910
+ var PRO_TIER_PRICES = [10, 15, 25];
911
+ function proTierCost(width, height) {
912
+ const px = width * height;
913
+ if (px <= 65536) return PRO_TIER_PRICES[0];
914
+ if (px <= 102400) return PRO_TIER_PRICES[1];
915
+ return PRO_TIER_PRICES[2];
857
916
  }
858
917
  function tilesCost(tileSize, variations) {
859
918
  const px = tileSize * tileSize * Math.max(1, variations);
860
- if (px <= 1024) return 20;
861
- if (px <= 2048) return 25;
862
- return 40;
919
+ if (px <= 1024) return PRO_TIER_PRICES[0];
920
+ if (px <= 2048) return PRO_TIER_PRICES[1];
921
+ return PRO_TIER_PRICES[2];
863
922
  }
864
923
  function isometricTileCost() {
865
924
  return 1;
@@ -1025,6 +1084,29 @@ var MapSceneSchema = z.object({
1025
1084
  image: z.string().min(1),
1026
1085
  placement: MapScenePlacementSchema.default({ oval: 0.3 })
1027
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
+ });
1028
1110
  var CharacterOutfitSchema = z.object({
1029
1111
  /** The loop (a character animation asset) to re-clothe, in the same style. */
1030
1112
  of: z.string().min(1),
@@ -1375,7 +1457,7 @@ var MEMBER_SET_REVISION_MODES = ["reduce-colors", "correct-pixelart", "edit-anim
1375
1457
  var StyleObjectSchema = z.object({
1376
1458
  /** Generation backend for this style. Omit to inherit the manifest default. */
1377
1459
  provider: z.string().min(1).optional(),
1378
- // `map` is the default because it is 20-40x cheaper and correct for any
1460
+ // `map` is the default because it is 10-25x cheaper and correct for any
1379
1461
  // asset that is not going to be rotated or animated.
1380
1462
  generator: GeneratorSchema.default("map"),
1381
1463
  /** Square edge length. The active provider owns its exact limit. */
@@ -1658,7 +1740,7 @@ var StyleSchema = StyleObjectSchema.refine((s) => !(s.tileFeature && s.styleImag
1658
1740
  message: "tileFeature and styleImages cannot be combined; a connectable set derives its own tile geometry, so remove one or the other",
1659
1741
  path: ["tileFeature"]
1660
1742
  }).superRefine((style, ctx) => {
1661
- 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")) {
1662
1744
  ctx.addIssue({
1663
1745
  code: z.ZodIssueCode.custom,
1664
1746
  message: "quality profiles currently support single-image generators only",
@@ -1788,6 +1870,8 @@ var AssetSchema = z.object({
1788
1870
  outfit: CharacterOutfitSchema.optional(),
1789
1871
  /** `map` styles only: draw this object in the style of a scene image. See `MapSceneSchema`. */
1790
1872
  scene: MapSceneSchema.optional(),
1873
+ /** `vfx` styles only: the effect to render. See `AssetVfxSchema`. */
1874
+ vfx: AssetVfxSchema.optional(),
1791
1875
  /** `character` styles, `standard` humanoid bases: this character's proportions, over the style's. */
1792
1876
  proportions: CharacterProportionsSchema.optional(),
1793
1877
  /**
@@ -1864,7 +1948,7 @@ var AssetSchema = z.object({
1864
1948
  path: ["revision"]
1865
1949
  });
1866
1950
  }
1867
- 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);
1868
1952
  if (shapes.length > 1) {
1869
1953
  context.addIssue({
1870
1954
  code: z.ZodIssueCode.custom,
@@ -1879,7 +1963,7 @@ var AssetSchema = z.object({
1879
1963
  path: ["mirror"]
1880
1964
  });
1881
1965
  }
1882
- 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) {
1883
1967
  context.addIssue({
1884
1968
  code: z.ZodIssueCode.custom,
1885
1969
  message: "Required",
@@ -2904,6 +2988,11 @@ function frameSetFps(entry) {
2904
2988
  }
2905
2989
  return null;
2906
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
+ }
2907
2996
 
2908
2997
  // src/palette.ts
2909
2998
  function normalizePalette(values) {
@@ -4325,6 +4414,7 @@ async function inspectRevisionReadiness(spec, lock) {
4325
4414
  if (spec.character?.styleAnchor) return inspectStyleAnchor(spec, lock, /* @__PURE__ */ new Set());
4326
4415
  if (spec.objectPro?.parentSpec) return inspectObjectProParent(spec, lock, /* @__PURE__ */ new Set());
4327
4416
  if (spec.scene) return inspectScene(spec);
4417
+ if (spec.vfx?.sprite && "file" in spec.vfx.sprite) return inspectVfxSprite(spec);
4328
4418
  if (!spec.revision) return null;
4329
4419
  return inspectRevision(spec, lock, /* @__PURE__ */ new Set());
4330
4420
  }
@@ -4352,6 +4442,12 @@ async function inspectScene(spec) {
4352
4442
  }
4353
4443
  return { ready: true, reason: "scene is current" };
4354
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
+ }
4355
4451
  async function inspectMirror(spec, lock, seen) {
4356
4452
  const source = spec.mirror.sourceSpec;
4357
4453
  const label = `mirror source ${spec.styleId}/${spec.mirror.sourceAssetId}`;
@@ -4588,7 +4684,7 @@ async function buildPlan(specs, lock, opts = {}) {
4588
4684
  } else if (opts.force) {
4589
4685
  state = "missing";
4590
4686
  reason = "--force";
4591
- } else if (!entry && spec.source && spec.generator !== "frames") {
4687
+ } else if (!entry && spec.source && spec.generator !== "frames" && spec.generator !== "vfx") {
4592
4688
  if (existsSync5(path9.resolve(spec.root, spec.source))) {
4593
4689
  state = "ok";
4594
4690
  reason = `placed from ${spec.source}; not generated`;
@@ -4828,8 +4924,8 @@ async function confirm(question, auto) {
4828
4924
  }
4829
4925
 
4830
4926
  // src/cli/commands/account.ts
4831
- import path23 from "path";
4832
- import { existsSync as existsSync16 } from "fs";
4927
+ import path24 from "path";
4928
+ import { existsSync as existsSync17 } from "fs";
4833
4929
 
4834
4930
  // src/env.ts
4835
4931
  import { readFileSync, existsSync as existsSync6 } from "fs";
@@ -5841,10 +5937,10 @@ function canonical2(value) {
5841
5937
  }
5842
5938
 
5843
5939
  // src/providers/pixellab.ts
5844
- import { mkdirSync, existsSync as existsSync7, readFileSync as readFileSync2, writeFileSync } from "fs";
5940
+ import { mkdirSync, existsSync as existsSync8, readFileSync as readFileSync2, writeFileSync } from "fs";
5845
5941
  import { randomUUID as randomUUID3 } from "crypto";
5846
5942
  import os from "os";
5847
- import path12 from "path";
5943
+ import path13 from "path";
5848
5944
 
5849
5945
  // src/client.ts
5850
5946
  import { z as z3 } from "zod";
@@ -6261,9 +6357,9 @@ var PixelLabClient = class {
6261
6357
  apiKey;
6262
6358
  http;
6263
6359
  /** Retries transport failures, 429, and 5xx; see http.ts for the policy. */
6264
- async request(path52, init) {
6360
+ async request(path53, init) {
6265
6361
  const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
6266
- const res = await this.http(`${BASE}${path52}`, {
6362
+ const res = await this.http(`${BASE}${path53}`, {
6267
6363
  ...init,
6268
6364
  headers: {
6269
6365
  Authorization: auth,
@@ -6273,13 +6369,13 @@ var PixelLabClient = class {
6273
6369
  });
6274
6370
  const text = await res.text();
6275
6371
  if (!res.ok) {
6276
- 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);
6277
6373
  }
6278
6374
  if (!text) return {};
6279
6375
  try {
6280
6376
  return JSON.parse(text);
6281
6377
  } catch {
6282
- throw new Error(`${init?.method ?? "GET"} ${path52} returned invalid JSON`);
6378
+ throw new Error(`${init?.method ?? "GET"} ${path53} returned invalid JSON`);
6283
6379
  }
6284
6380
  }
6285
6381
  async balance() {
@@ -6684,10 +6780,10 @@ var PixelLabClient = class {
6684
6780
  const palette = args.paletteSwatchBase64 ? { color_image: { type: "base64", base64: args.paletteSwatchBase64, format: "png" }, force_colors: true } : {};
6685
6781
  const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
6686
6782
  const south = args.reference?.south;
6687
- let path52;
6783
+ let path53;
6688
6784
  let body;
6689
6785
  if (args.mode === "v3") {
6690
- path52 = "/create-character-v3";
6786
+ path53 = "/create-character-v3";
6691
6787
  body = {
6692
6788
  description: args.description,
6693
6789
  image_size: imageSize,
@@ -6701,7 +6797,7 @@ var PixelLabClient = class {
6701
6797
  ...args.enhancePrompt ? { enhance_prompt: true } : {}
6702
6798
  };
6703
6799
  } else if (args.mode === "pro-flash") {
6704
- path52 = "/create-character-pro-flash";
6800
+ path53 = "/create-character-pro-flash";
6705
6801
  const traits = args.styleTraits;
6706
6802
  body = {
6707
6803
  description: args.description,
@@ -6725,7 +6821,7 @@ var PixelLabClient = class {
6725
6821
  } : {}
6726
6822
  };
6727
6823
  } else if (args.mode === "pro") {
6728
- path52 = "/create-character-pro";
6824
+ path53 = "/create-character-pro";
6729
6825
  body = {
6730
6826
  description: args.description,
6731
6827
  image_size: imageSize,
@@ -6739,7 +6835,7 @@ var PixelLabClient = class {
6739
6835
  ...!south && args.styleCharacterId ? { style_character_id: args.styleCharacterId } : {}
6740
6836
  };
6741
6837
  } else {
6742
- 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";
6743
6839
  body = {
6744
6840
  description: args.description,
6745
6841
  image_size: imageSize,
@@ -6756,8 +6852,8 @@ var PixelLabClient = class {
6756
6852
  ...palette
6757
6853
  };
6758
6854
  }
6759
- const raw = await this.request(path52, { method: "POST", body: JSON.stringify(body) });
6760
- 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));
6761
6857
  }
6762
6858
  /** A text edit of an existing character, applied to every direction and stored as a sibling. */
6763
6859
  async createCharacterState(args) {
@@ -6820,7 +6916,8 @@ var PixelLabClient = class {
6820
6916
  * A full-body sprite in, a bust portrait out (or the reverse, unused
6821
6917
  * today). Job-based like a state or `1dir`, not the couple-generations
6822
6918
  * "conversion" the name suggests: the smallest `resultSize` (16px) billed
6823
- * 20 generations live (docs/ENDPOINTS.md, "Characters, measured").
6919
+ * 20 generations live (docs/ENDPOINTS.md, "Characters, measured"), the
6920
+ * Pro floor before PixelLab's October 2026 cut put it at 10.
6824
6921
  */
6825
6922
  async createPortraitCharacterPro(args) {
6826
6923
  const raw = await this.request("/portrait-character-pro", {
@@ -6859,7 +6956,8 @@ var PixelLabClient = class {
6859
6956
  * everything else here, but the completed job returns the result frames
6860
6957
  * inline as base64 (`last_response.quantized_images`), not as storage
6861
6958
  * URLs — the one PixelLab character tool that works this way. A live
6862
- * 2-frame, 92x92 job billed 20 generations (docs/ENDPOINTS.md).
6959
+ * 2-frame, 92x92 job billed 20 generations (docs/ENDPOINTS.md), before
6960
+ * PixelLab's October 2026 Pro cut halved that floor to 10.
6863
6961
  */
6864
6962
  async transferOutfitV2(args) {
6865
6963
  const image = (img) => ({
@@ -6909,7 +7007,9 @@ var PixelLabClient = class {
6909
7007
  * far higher than this tiering assumes; 352x352 (123904px²), 384x384
6910
7008
  * (147456px²), and 512x512 (262144px², the tiering ceiling) all billed
6911
7009
  * 40. Both breakpoints are now tightly bracketed: 20->25 in
6912
- * (65536px², 82944px²], 25->40 in (102400px², 123904px²].
7010
+ * (65536px², 82944px²], 25->40 in (102400px², 123904px²]. Those prices
7011
+ * predate PixelLab's October 2026 Pro cut to 10/15/25; `proTierCost`
7012
+ * keeps the breakpoints with the new prices.
6913
7013
  * `editImagesV2` below returns the same fields under `images`, plural
6914
7014
  * and array-wrapped, not `image` — do not assume the two endpoints share
6915
7015
  * one response shape. See docs/ENDPOINTS.md and docs/REVISIONS.md for
@@ -6962,8 +7062,9 @@ var PixelLabClient = class {
6962
7062
  );
6963
7063
  }
6964
7064
  /**
6965
- * PixelLab's Pro image tier, `/generate-image-v2`: a flat 40 generations
6966
- * (docs/ENDPOINTS.md, "Single-image generators, measured") for real style
7065
+ * PixelLab's Pro image tier, `/generate-image-v2`: 10 to 25 generations by
7066
+ * canvas since PixelLab's October 2026 Pro cut, a flat 40 before
7067
+ * (docs/ENDPOINTS.md, "Single-image generators, measured"), for real style
6967
7068
  * transfer and non-square canvases up to 792x688, where `pixflux` is
6968
7069
  * limited to 400x400 and no style reference. Like a revision, this hands
6969
7070
  * back a plain background job with no resource of its own; unlike a
@@ -7049,6 +7150,24 @@ var PixelLabClient = class {
7049
7150
  "inpaint-image-pro-flash"
7050
7151
  );
7051
7152
  }
7153
+ /**
7154
+ * `/image-to-pixelart-pro-flash`: a photo, painting, or render in, pixel
7155
+ * art out, with no size to pass: PixelLab detects the native pixel scale
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
7158
+ * in October 2026. Sources over 2048px on the longer side are scaled down
7159
+ * upstream. The finished job carries the image at `last_response.images[0]`.
7160
+ */
7161
+ async imageToPixelartProFlash(args) {
7162
+ const body = { image: args.image };
7163
+ if (args.description) body.description = args.description;
7164
+ if (args.seed != null) body.seed = args.seed;
7165
+ return validateResponse(
7166
+ ProFlashImageSubmitSchema,
7167
+ await this.request("/image-to-pixelart-pro-flash", { method: "POST", body: JSON.stringify(body) }),
7168
+ "image-to-pixelart-pro-flash"
7169
+ );
7170
+ }
7052
7171
  /**
7053
7172
  * `/generate-ui-v2` ("Generate UI (Pro)"): one UI element (a button, a
7054
7173
  * health bar, an inventory slot, a dialogue box) from a description,
@@ -7406,6 +7525,337 @@ function clientFromEnv() {
7406
7525
  return new PixelLabClient(key);
7407
7526
  }
7408
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
+
7409
7859
  // src/providers/pixellab.ts
7410
7860
  function pixelLabObjectUrl(generator, objectId) {
7411
7861
  if (!objectId) return null;
@@ -7420,6 +7870,7 @@ function pixelLabObjectUrl(generator, objectId) {
7420
7870
  const characterId = objectId.split("#")[0];
7421
7871
  return characterId ? `https://www.pixellab.ai/create-character/${encodeURIComponent(characterId)}` : null;
7422
7872
  }
7873
+ if (generator === "vfx") return `https://pixellab.ai/create-vfx?effect=${encodeURIComponent(objectId)}`;
7423
7874
  return null;
7424
7875
  }
7425
7876
  function billedFromUsage(usage) {
@@ -7443,7 +7894,7 @@ function characterCost(spec) {
7443
7894
  }
7444
7895
  if (character.kind === "state" || character.kind === "portrait") return generationCost(spec.width, spec.height, "1dir");
7445
7896
  if (character.kind === "outfit") {
7446
- return 20;
7897
+ return PRO_TIER_PRICES[0];
7447
7898
  }
7448
7899
  if (character.mode === "standard") return 1;
7449
7900
  if (character.mode === "v3") {
@@ -7595,13 +8046,20 @@ function decodeObjectAnimationJob(jobId) {
7595
8046
  return null;
7596
8047
  }
7597
8048
  var PixelLabProvider = class _PixelLabProvider {
7598
- 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) {
7599
8055
  this.client = client2;
8056
+ this.mcp = mcp;
7600
8057
  }
7601
8058
  client;
8059
+ mcp;
7602
8060
  id = "pixellab";
7603
8061
  static fromEnv() {
7604
- return new _PixelLabProvider(clientFromEnv());
8062
+ return new _PixelLabProvider(clientFromEnv(), mcpClientFromEnv());
7605
8063
  }
7606
8064
  /** Public storage URLs and the local content cache do not require API auth. */
7607
8065
  static forDownloads() {
@@ -7612,7 +8070,7 @@ var PixelLabProvider = class _PixelLabProvider {
7612
8070
  return new _PixelLabProvider(new PixelLabClient("download-only"));
7613
8071
  }
7614
8072
  supports(generator) {
7615
- 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";
7616
8074
  }
7617
8075
  /** `/map-objects` style matching: `background_image` plus `inpainting`. */
7618
8076
  supportsMapScene = true;
@@ -7654,11 +8112,12 @@ var PixelLabProvider = class _PixelLabProvider {
7654
8112
  * is a file read.
7655
8113
  */
7656
8114
  static cacheDir(namespace = "pixflux") {
7657
- const dir = path12.join(os.tmpdir(), `pixelkiln-${namespace}`);
8115
+ const dir = path13.join(os.tmpdir(), `pixelkiln-${namespace}`);
7658
8116
  mkdirSync(dir, { recursive: true });
7659
8117
  return dir;
7660
8118
  }
7661
8119
  estimate(spec) {
8120
+ if (spec.vfx) return { unit: "generations", amount: 0, candidates: 1 };
7662
8121
  if (spec.batch?.role === "member") {
7663
8122
  return { unit: "generations", amount: 0, candidates: 1 };
7664
8123
  }
@@ -7695,7 +8154,7 @@ var PixelLabProvider = class _PixelLabProvider {
7695
8154
  if (spec.revision) {
7696
8155
  const width = spec.revision.sourceWidth ?? spec.width;
7697
8156
  const height = spec.revision.sourceHeight ?? spec.height;
7698
- return { unit: "generations", amount: generationCost(width, height, "1dir"), candidates: 1 };
8157
+ return { unit: "generations", amount: proTierCost(width, height), candidates: 1 };
7699
8158
  }
7700
8159
  if (spec.generator === "tiles" || spec.generator === "terrain" || spec.generator === "isometricTile") {
7701
8160
  return { unit: "generations", amount: spec.cost, candidates: spec.candidates };
@@ -7716,6 +8175,9 @@ var PixelLabProvider = class _PixelLabProvider {
7716
8175
  };
7717
8176
  }
7718
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
+ }
7719
8181
  const members = spec.revision?.sourceMembers;
7720
8182
  if (spec.revision && members && !MEMBER_SET_REVISION_MODES.includes(spec.revision.mode)) {
7721
8183
  throw new Error(
@@ -8000,7 +8462,7 @@ var PixelLabProvider = class _PixelLabProvider {
8000
8462
  }
8001
8463
  }
8002
8464
  if (endFrame) {
8003
- 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);
8004
8466
  if (start && (start.width !== endFrame.width || start.height !== endFrame.height)) {
8005
8467
  throw new Error(
8006
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`
@@ -8158,7 +8620,7 @@ var PixelLabProvider = class _PixelLabProvider {
8158
8620
  }
8159
8621
  }
8160
8622
  if (endFrame) {
8161
- 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);
8162
8624
  if (start && (start.width !== endFrame.width || start.height !== endFrame.height)) {
8163
8625
  throw new Error(
8164
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`
@@ -8468,7 +8930,7 @@ var PixelLabProvider = class _PixelLabProvider {
8468
8930
  const sources = images.map((image, index) => {
8469
8931
  if (!image.base64) throw new Error(`outfit transfer frame ${index} has no image data`);
8470
8932
  const role = meta?.frameRoles?.[index] ?? void 0;
8471
- const file = path12.join(dir, `${jobId}-${index}.png`);
8933
+ const file = path13.join(dir, `${jobId}-${index}.png`);
8472
8934
  writeFileSync(file, Buffer.from(image.base64, "base64"));
8473
8935
  return { url: `file://${file}`, ...role ? { role } : {} };
8474
8936
  });
@@ -8620,6 +9082,7 @@ var PixelLabProvider = class _PixelLabProvider {
8620
9082
  async submit(spec, styleImages, context) {
8621
9083
  this.validate(spec, styleImages);
8622
9084
  if (spec.revision) return this.submitRevision(spec);
9085
+ if (spec.vfx) return this.submitVfx(spec, context);
8623
9086
  if (spec.generator === "character") return this.submitCharacter(spec, styleImages, context);
8624
9087
  if (spec.generator === "objectPro") return this.submitObjectPro(spec, styleImages, context);
8625
9088
  if (spec.generator === "uiAsset") {
@@ -8673,7 +9136,7 @@ var PixelLabProvider = class _PixelLabProvider {
8673
9136
  seed: spec.seed
8674
9137
  });
8675
9138
  const jobId = randomUUID3();
8676
- writeFileSync(path12.join(_PixelLabProvider.cacheDir(), `${jobId}.png`), png);
9139
+ writeFileSync(path13.join(_PixelLabProvider.cacheDir(), `${jobId}.png`), png);
8677
9140
  return { jobId };
8678
9141
  }
8679
9142
  if (spec.generator === "tiles") {
@@ -9002,11 +9465,11 @@ var PixelLabProvider = class _PixelLabProvider {
9002
9465
  cacheCleanupResult(pngs, usage, roles, fps) {
9003
9466
  const jobId = randomUUID3();
9004
9467
  if (!roles) {
9005
- writeFileSync(path12.join(_PixelLabProvider.cacheDir(), `${jobId}.png`), pngs[0]);
9468
+ writeFileSync(path13.join(_PixelLabProvider.cacheDir(), `${jobId}.png`), pngs[0]);
9006
9469
  return { jobId, metadata: { revisionUsage: usage } };
9007
9470
  }
9008
9471
  for (const [index, png] of pngs.entries()) {
9009
- writeFileSync(path12.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`), png);
9472
+ writeFileSync(path13.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`), png);
9010
9473
  }
9011
9474
  return { jobId, metadata: { revisionUsage: usage, revisionMembers: roles, ...fps != null ? { revisionFps: fps } : {} } };
9012
9475
  }
@@ -9034,10 +9497,11 @@ var PixelLabProvider = class _PixelLabProvider {
9034
9497
  return this.pollAnimateRevision(jobId, context);
9035
9498
  }
9036
9499
  if (context?.spec?.revision) return this.pollRevision(jobId);
9500
+ if (generator === "vfx") return this.pollVfx(jobId, context);
9037
9501
  if (generator === "imageProFlash") return this.pollRevision(jobId);
9038
9502
  if (generator === "pixflux") {
9039
- const file = path12.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
9040
- if (existsSync7(file)) {
9503
+ const file = path13.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
9504
+ if (existsSync8(file)) {
9041
9505
  const sourceUrl = `file://${file}`;
9042
9506
  return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }] };
9043
9507
  }
@@ -9080,6 +9544,120 @@ var PixelLabProvider = class _PixelLabProvider {
9080
9544
  * background job to ask afterwards the way `billedForJob` does for async
9081
9545
  * work.
9082
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
+ }
9083
9661
  async pollCachedRevision(jobId, context) {
9084
9662
  const usage = context?.metadata?.revisionUsage;
9085
9663
  const roles = context?.metadata?.revisionMembers;
@@ -9089,10 +9667,10 @@ var PixelLabProvider = class _PixelLabProvider {
9089
9667
  };
9090
9668
  if (Array.isArray(roles) && roles.length) {
9091
9669
  const sources = roles.map((role, index) => ({
9092
- url: `file://${path12.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`)}`,
9670
+ url: `file://${path13.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`)}`,
9093
9671
  role: String(role)
9094
9672
  }));
9095
- 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;
9096
9674
  const fps = context?.metadata?.revisionFps;
9097
9675
  const frames = sources.every((source) => source.role.startsWith("frame-"));
9098
9676
  return {
@@ -9104,8 +9682,8 @@ var PixelLabProvider = class _PixelLabProvider {
9104
9682
  billed: billedFromUsage(usage)
9105
9683
  };
9106
9684
  }
9107
- const file = path12.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
9108
- if (!existsSync7(file)) return gone;
9685
+ const file = path13.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
9686
+ if (!existsSync8(file)) return gone;
9109
9687
  const sourceUrl = `file://${file}`;
9110
9688
  return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }], billed: billedFromUsage(usage) };
9111
9689
  }
@@ -9161,7 +9739,7 @@ var PixelLabProvider = class _PixelLabProvider {
9161
9739
  }
9162
9740
  const base64 = extractBase64(item);
9163
9741
  if (base64) {
9164
- const file = path12.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`);
9742
+ const file = path13.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`);
9165
9743
  writeFileSync(file, Buffer.from(base64, "base64"));
9166
9744
  urls.push(`file://${file}`);
9167
9745
  continue;
@@ -9212,7 +9790,7 @@ var PixelLabProvider = class _PixelLabProvider {
9212
9790
  const candidate = done[key];
9213
9791
  const base64 = extractBase64(candidate);
9214
9792
  if (base64) {
9215
- const file = path12.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
9793
+ const file = path13.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
9216
9794
  writeFileSync(file, Buffer.from(base64, "base64"));
9217
9795
  const sourceUrl = `file://${file}`;
9218
9796
  return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }], billed };
@@ -9221,7 +9799,7 @@ var PixelLabProvider = class _PixelLabProvider {
9221
9799
  if (Array.isArray(done.images) && done.images.length) {
9222
9800
  const base64 = extractBase64(done.images[0]);
9223
9801
  if (base64) {
9224
- const file = path12.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
9802
+ const file = path13.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
9225
9803
  writeFileSync(file, Buffer.from(base64, "base64"));
9226
9804
  const sourceUrl = `file://${file}`;
9227
9805
  return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }], billed };
@@ -9255,7 +9833,7 @@ var PixelLabProvider = class _PixelLabProvider {
9255
9833
  for (const [index, candidate] of done.images.entries()) {
9256
9834
  const base64 = extractBase64(candidate);
9257
9835
  if (!base64) continue;
9258
- const file = path12.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`);
9836
+ const file = path13.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`);
9259
9837
  writeFileSync(file, Buffer.from(base64, "base64"));
9260
9838
  urls.push(`file://${file}`);
9261
9839
  }
@@ -9285,7 +9863,7 @@ var PixelLabProvider = class _PixelLabProvider {
9285
9863
  for (const [index, candidate] of images.entries()) {
9286
9864
  const base64 = extractBase64(candidate);
9287
9865
  if (!base64) continue;
9288
- const file = path12.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`);
9866
+ const file = path13.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`);
9289
9867
  writeFileSync(file, Buffer.from(base64, "base64"));
9290
9868
  urls.push(`file://${file}`);
9291
9869
  }
@@ -9383,7 +9961,7 @@ var PixelLabProvider = class _PixelLabProvider {
9383
9961
  for (const [index, tile] of tileset.tiles.entries()) {
9384
9962
  const slug2 = tile.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "tile";
9385
9963
  const role = `tile-${String(index).padStart(2, "0")}-${slug2}`;
9386
- const file = path12.join(_PixelLabProvider.cacheDir(), `${tilesetId}-${index}.png`);
9964
+ const file = path13.join(_PixelLabProvider.cacheDir(), `${tilesetId}-${index}.png`);
9387
9965
  writeFileSync(file, Buffer.from(tile.image.base64, "base64"));
9388
9966
  sources.push({ url: `file://${file}`, role });
9389
9967
  terrainTiles.push({ role, corners: tile.corners, pattern4x4: tile.pattern_4x4 });
@@ -9413,7 +9991,7 @@ var PixelLabProvider = class _PixelLabProvider {
9413
9991
  const { image, usage } = await this.client.getIsometricTile(tileId);
9414
9992
  const backgroundJobId = context?.metadata?.backgroundJobId;
9415
9993
  const billed = billedFromUsage(usage) ?? await this.billedForJob(backgroundJobId);
9416
- const file = path12.join(_PixelLabProvider.cacheDir(), `${tileId}.png`);
9994
+ const file = path13.join(_PixelLabProvider.cacheDir(), `${tileId}.png`);
9417
9995
  writeFileSync(file, Buffer.from(image.base64, "base64"));
9418
9996
  return {
9419
9997
  status: "ready",
@@ -9457,8 +10035,8 @@ var PixelLabProvider = class _PixelLabProvider {
9457
10035
  return { objectId: `${jobId}#${index}`, sourceUrl: url };
9458
10036
  }
9459
10037
  if (generator === "imagePro" || generator === "uiElement") {
9460
- const file = path12.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`);
9461
- 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}`);
9462
10040
  return { objectId: `${jobId}#${index}`, sourceUrl: `file://${file}` };
9463
10041
  }
9464
10042
  const promoted = await this.client.selectFrames(jobId, [index], commonTag);
@@ -9735,9 +10313,9 @@ var RetroDiffusionClient = class {
9735
10313
  if (!response.ok) throw new ProviderError("retrodiffusion", `Retro Diffusion download failed (${response.status})`, { status: response.status });
9736
10314
  return Buffer.from(await response.arrayBuffer());
9737
10315
  }
9738
- async call(path52, init = {}) {
10316
+ async call(path53, init = {}) {
9739
10317
  if (!this.token) throw new Error("RD_API_KEY is not set");
9740
- const response = await this.request(`${this.baseUrl}${path52}`, {
10318
+ const response = await this.request(`${this.baseUrl}${path53}`, {
9741
10319
  ...init,
9742
10320
  headers: {
9743
10321
  "Content-Type": "application/json",
@@ -10049,7 +10627,7 @@ function retroError(value) {
10049
10627
  import { createHash as createHash3 } from "crypto";
10050
10628
  import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, renameSync, writeFileSync as writeFileSync2 } from "fs";
10051
10629
  import os2 from "os";
10052
- import path13 from "path";
10630
+ import path14 from "path";
10053
10631
  var DEFAULT_BASE_URL3 = "https://api.cloud.scenario.com/v1";
10054
10632
  var SOURCE_PROTOCOL2 = "scenario:";
10055
10633
  var SUBMIT_RETRIES = 8;
@@ -10432,7 +11010,7 @@ var ScenarioProvider = class _ScenarioProvider {
10432
11010
  }
10433
11011
  };
10434
11012
  function uploadCachePath() {
10435
- 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");
10436
11014
  }
10437
11015
  function uploadCacheKey(baseUrl, projectId, digest2) {
10438
11016
  return createHash3("sha256").update(`${baseUrl}
@@ -10450,7 +11028,7 @@ function readUploadCache() {
10450
11028
  function writeUploadCache(key, assetId) {
10451
11029
  try {
10452
11030
  const file = uploadCachePath();
10453
- mkdirSync2(path13.dirname(file), { recursive: true });
11031
+ mkdirSync2(path14.dirname(file), { recursive: true });
10454
11032
  const tmp = `${file}.${process.pid}.tmp`;
10455
11033
  writeFileSync2(tmp, JSON.stringify({ ...readUploadCache(), [key]: assetId }));
10456
11034
  renameSync(tmp, file);
@@ -10710,9 +11288,9 @@ registerProvider({
10710
11288
  });
10711
11289
 
10712
11290
  // src/manifest.ts
10713
- import { readFile as readFile6 } from "fs/promises";
10714
- import { existsSync as existsSync8 } from "fs";
10715
- import path14 from "path";
11291
+ import { readFile as readFile7 } from "fs/promises";
11292
+ import { existsSync as existsSync9 } from "fs";
11293
+ import path15 from "path";
10716
11294
 
10717
11295
  // src/loop-directions.ts
10718
11296
  var SINGLE_OUTPUT_FIELDS = ["file", "cell", "remoteId", "source", "sourceByStyle", "outputRole"];
@@ -10801,9 +11379,9 @@ function isObject2(value) {
10801
11379
 
10802
11380
  // src/manifest.ts
10803
11381
  async function loadManifest(manifestPath) {
10804
- const abs = path14.resolve(manifestPath);
10805
- if (!existsSync8(abs)) throw new ProjectError(`No manifest at ${abs}`);
10806
- 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")));
10807
11385
  if (expansion.issues.length) {
10808
11386
  throw new ProjectError(`Manifest at ${abs} is invalid:
10809
11387
  ${expansion.issues.map((i) => ` ${i}`).join("\n")}`);
@@ -10858,7 +11436,7 @@ ${issues}`);
10858
11436
  if (fromStyle) {
10859
11437
  if (!styleIds.has(fromStyle)) {
10860
11438
  unknownReferences.push(`assets.${assetId}.revision.fromStyle: unknown style "${fromStyle}"`);
10861
- } else if (parent && parent.styles.length && !parent.styles.includes(fromStyle)) {
11439
+ } else if (parent && !assetInStyle(parent, fromStyle, parsed.data.styles[fromStyle])) {
10862
11440
  unknownReferences.push(
10863
11441
  `assets.${assetId}.revision.fromStyle: "${asset.revision.from}" is not in style "${fromStyle}"`
10864
11442
  );
@@ -10905,7 +11483,11 @@ ${issues}`);
10905
11483
  ${unknownReferences.map((i) => ` ${i}`).join("\n")}`);
10906
11484
  }
10907
11485
  const loopFamilies = Object.keys(expansion.families).length ? { loopFamilies: expansion.families } : {};
10908
- 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);
10909
11491
  }
10910
11492
  function parentAssetId(asset) {
10911
11493
  return asset?.revision?.from ?? asset?.state?.of ?? asset?.animation?.of ?? asset?.portrait?.of ?? asset?.outfit?.of;
@@ -11009,8 +11591,8 @@ async function resolveSpecs(loaded, filter) {
11009
11591
  const needed = /* @__PURE__ */ new Set();
11010
11592
  for (const assetId of resolutionAssetIds) {
11011
11593
  const asset = manifest.assets[assetId];
11012
- if (asset?.styles.length && !asset.styles.includes(styleId)) continue;
11013
- const fromStyle = asset?.revision?.fromStyle;
11594
+ if (!asset || !assetInStyle(asset, styleId, manifest.styles[styleId])) continue;
11595
+ const fromStyle = asset.revision?.fromStyle;
11014
11596
  if (fromStyle && fromStyle !== styleId) needed.add(fromStyle);
11015
11597
  }
11016
11598
  styleDependencies.set(styleId, needed);
@@ -11039,11 +11621,11 @@ async function resolveSpecs(loaded, filter) {
11039
11621
  const styleFinalizers = /* @__PURE__ */ new Map();
11040
11622
  const styleImageCache = /* @__PURE__ */ new Map();
11041
11623
  async function loadStyleImage(rel, what = "Style image") {
11042
- const abs = path14.resolve(root, rel);
11624
+ const abs = path15.resolve(root, rel);
11043
11625
  let hit = styleImageCache.get(abs);
11044
11626
  if (!hit) {
11045
- if (!existsSync8(abs)) throw new Error(`${what} not found: ${abs}`);
11046
- const buf = await readFile6(abs);
11627
+ if (!existsSync9(abs)) throw new Error(`${what} not found: ${abs}`);
11628
+ const buf = await readFile7(abs);
11047
11629
  const metadata = imageMetadata(buf);
11048
11630
  if (!metadata) throw new Error(`${what} is not a readable PNG or JPEG: ${abs}`);
11049
11631
  if (metadata.width < 1 || metadata.height < 1) {
@@ -11069,14 +11651,14 @@ async function resolveSpecs(loaded, filter) {
11069
11651
  styleImageDimensions.push(loadedImage);
11070
11652
  }
11071
11653
  const resolvedImages = style.styleImages.map((image) => {
11072
- const hit = styleImageCache.get(path14.resolve(root, image.path));
11654
+ const hit = styleImageCache.get(path15.resolve(root, image.path));
11073
11655
  return { base64: hit.base64, width: hit.width, height: hit.height, format: hit.format };
11074
11656
  });
11075
11657
  const styleSpecs = /* @__PURE__ */ new Map();
11076
11658
  const providerInputIdentities = /* @__PURE__ */ new Map();
11077
11659
  for (const [assetId, asset] of Object.entries(manifest.assets)) {
11078
11660
  if (!resolutionAssetIds.has(assetId)) continue;
11079
- if (asset.styles.length && !asset.styles.includes(styleId)) continue;
11661
+ if (!assetInStyle(asset, styleId, style)) continue;
11080
11662
  const generator = style.generator;
11081
11663
  if (!activeProvider.supports(generator)) {
11082
11664
  throw new Error(`Provider "${activeProvider.id}" does not support generator "${generator}"`);
@@ -11096,6 +11678,15 @@ async function resolveSpecs(loaded, filter) {
11096
11678
  const providerInputs = inputResolution.inputs;
11097
11679
  const providerInputIdentity = inputResolution.identity ?? providerInputs;
11098
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;
11099
11690
  let width;
11100
11691
  let height;
11101
11692
  let size;
@@ -11123,6 +11714,11 @@ async function resolveSpecs(loaded, filter) {
11123
11714
  size = asset.size ?? style.size ?? 64;
11124
11715
  width = size;
11125
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);
11126
11722
  } else if (generator === "uiAsset" || generator === "uiElement") {
11127
11723
  width = asset.width ?? style.size ?? 256;
11128
11724
  height = asset.height ?? style.size ?? 256;
@@ -11165,14 +11761,14 @@ async function resolveSpecs(loaded, filter) {
11165
11761
  terrainUpperDescription = wrap(parsed.upper);
11166
11762
  terrainTransitionDescription = parsed.transition ? wrap(parsed.transition) : void 0;
11167
11763
  prompt = subject.trim();
11168
- } else if ((generator === "character" || generator === "objectPro") && characterKind !== "base") {
11764
+ } else if ((generator === "character" || generator === "objectPro") && characterKind !== "base" || vfx) {
11169
11765
  prompt = subject.trim();
11170
11766
  } else {
11171
11767
  prompt = [style.promptPrefix, subject, style.promptSuffix].map((p) => p.trim()).filter(Boolean).join(", ");
11172
11768
  }
11173
- const relFile = asset.file ?? path14.join(asset.category ?? "", `${assetId}.png`);
11174
- const outFile = path14.resolve(root, style.outDir, relFile);
11175
- 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;
11176
11772
  const tileSize = generator === "tiles" ? size : style.tileSize ?? 32;
11177
11773
  const tileVariations = tileFeatureOutputCount(generator === "tiles" ? style.tileFeature : void 0) ?? tileVariationCount(countNumberedDescriptions(prompt));
11178
11774
  const terrainTiles = generator === "terrain" ? terrainTileCount(style.terrainTransitionSize) : 0;
@@ -11243,6 +11839,7 @@ async function resolveSpecs(loaded, filter) {
11243
11839
  // A portrait needs a character style (refused above), so an
11244
11840
  // objectPro asset is only ever a base, state, or animation.
11245
11841
  ...generator === "objectPro" && !asset.revision && characterKind !== "portrait" ? { objectPro: await resolveObjectProShape(assetId, asset, style, characterKind, { root, load: loadStyleImage }) } : {},
11842
+ ...vfx ? { vfx } : {},
11246
11843
  cost: generator === "tiles" ? tilesCost(tileSize, tileVariations) : generator === "terrain" ? tilesCost(size, terrainTiles) : generator === "isometricTile" ? isometricTileCost() : generationCost(width, height, generator),
11247
11844
  costUnit: "generations",
11248
11845
  candidates: generator === "tiles" ? tileVariations : generator === "terrain" ? terrainTiles : generator === "1dir" || generator === "imagePro" ? candidateCount(size) : 1
@@ -11267,7 +11864,7 @@ async function resolveSpecs(loaded, filter) {
11267
11864
  minGridConfidence: style.quality.minGridConfidence,
11268
11865
  ...style.quality.minTransparency == null ? {} : { minTransparency: style.quality.minTransparency },
11269
11866
  ...style.quality.fixerRevision ? { fixerRevision: style.quality.fixerRevision } : {},
11270
- ...style.quality.fixerPython ? { fixerPython: path14.resolve(root, style.quality.fixerPython) } : {},
11867
+ ...style.quality.fixerPython ? { fixerPython: path15.resolve(root, style.quality.fixerPython) } : {},
11271
11868
  ...style.quality.fps == null ? {} : { fps: style.quality.fps }
11272
11869
  }
11273
11870
  } : {},
@@ -11350,7 +11947,7 @@ async function resolveSpecs(loaded, filter) {
11350
11947
  const file = memberPath(anchorSpec.outFile, "south", 0, anchorSpec.character.directions, MediaType.PNG);
11351
11948
  resolved.character = {
11352
11949
  ...resolved.character,
11353
- 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 }
11354
11951
  };
11355
11952
  }
11356
11953
  const characterParent = asset.state?.of ?? asset.animation?.of ?? asset.portrait?.of;
@@ -11363,7 +11960,7 @@ async function resolveSpecs(loaded, filter) {
11363
11960
  parentAssetId: characterParent,
11364
11961
  parentSpec,
11365
11962
  parentFile,
11366
- parentSha256: existsSync8(parentFile) ? await sha256File(parentFile) : null,
11963
+ parentSha256: existsSync9(parentFile) ? await sha256File(parentFile) : null,
11367
11964
  // A state and its animations keep the engine and rotations of the base.
11368
11965
  mode: resolved.character.kind === "base" ? resolved.character.mode : parentSpec.character?.mode ?? resolved.character.mode,
11369
11966
  directions: resolved.character.kind === "base" ? resolved.character.directions : parentSpec.character?.directions ?? resolved.character.directions
@@ -11381,7 +11978,7 @@ async function resolveSpecs(loaded, filter) {
11381
11978
  parentAssetId: characterParent,
11382
11979
  parentSpec,
11383
11980
  parentFile,
11384
- parentSha256: existsSync8(parentFile) ? await sha256File(parentFile) : null,
11981
+ parentSha256: existsSync9(parentFile) ? await sha256File(parentFile) : null,
11385
11982
  // A state and its animations keep the base's own rotation count.
11386
11983
  directions: resolved.objectPro.kind === "base" ? resolved.objectPro.directions : parentSpec.objectPro.directions
11387
11984
  };
@@ -11403,7 +12000,7 @@ async function resolveSpecs(loaded, filter) {
11403
12000
  animation: sourceSpec.character.animation,
11404
12001
  outfit: {
11405
12002
  reference: {
11406
- path: path14.resolve(root, asset.outfit.reference),
12003
+ path: path15.resolve(root, asset.outfit.reference),
11407
12004
  sha256: referenceImage.hash,
11408
12005
  width: referenceImage.width,
11409
12006
  height: referenceImage.height,
@@ -11426,23 +12023,23 @@ async function resolveSpecs(loaded, filter) {
11426
12023
  }
11427
12024
  const sourceStyleId = asset.revision.fromStyle && asset.revision.fromStyle !== styleId ? asset.revision.fromStyle : void 0;
11428
12025
  const sourceSpec = sourceStyleId ? await styleFinalizers.get(sourceStyleId)(asset.revision.from) : await finalize(asset.revision.from);
11429
- 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);
11430
12027
  const sourceMembers = await revisionSourceMembers(sourceStem, `revision source ${asset.revision.from}`);
11431
12028
  const sourceFile = sourceMembers?.[0]?.file ?? sourceStem;
11432
12029
  const sourceImage = sourceMembers ? { ...sourceMembers[0], hash: sha256(JSON.stringify(sourceMembers.map((member) => member.sha256))) } : await optionalRevisionImage(sourceFile, "revision source");
11433
12030
  const sourceFps = sourceSpec.character?.animation?.fps ?? sourceSpec.objectPro?.animation?.fps ?? sourceSpec.revision?.fps ?? sourceSpec.revision?.sourceFps;
11434
- 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;
11435
12032
  const maskImage = maskFile ? await optionalRevisionImage(maskFile, "revision mask", "png") : null;
11436
12033
  if (sourceImage && maskImage && (sourceImage.width !== maskImage.width || sourceImage.height !== maskImage.height)) {
11437
12034
  throw new Error(
11438
12035
  `Revision mask for ${styleId}/${assetId} is ${maskImage.width}x${maskImage.height}; source ${asset.revision.from} is ${sourceImage.width}x${sourceImage.height}`
11439
12036
  );
11440
12037
  }
11441
- 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;
11442
12039
  const paletteImage = paletteImageFile ? await optionalRevisionImage(paletteImageFile, "revision palette image", "png") : null;
11443
- 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;
11444
12041
  const lastFrame = lastFrameFile ? await optionalRevisionImage(lastFrameFile, "revision last frame") : null;
11445
- 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;
11446
12043
  const keypoints = keypointsFile ? await optionalSkeletonSet(keypointsFile) : null;
11447
12044
  resolved.revision = {
11448
12045
  mode: asset.revision.mode,
@@ -11612,7 +12209,7 @@ async function resolveSpecs(loaded, filter) {
11612
12209
  return specs;
11613
12210
  }
11614
12211
  function pngPath(file) {
11615
- const extension = path14.extname(file);
12212
+ const extension = path15.extname(file);
11616
12213
  return extension ? `${file.slice(0, -extension.length)}.png` : `${file}.png`;
11617
12214
  }
11618
12215
  async function resolveMapScene(root, styleId, assetId, generator, scene, provider) {
@@ -11622,12 +12219,12 @@ async function resolveMapScene(root, styleId, assetId, generator, scene, provide
11622
12219
  if (!provider.supportsMapScene) {
11623
12220
  throw new Error(`assets.${assetId}.scene: provider "${provider.id}" cannot draw into a scene; PixelLab's /map-objects can`);
11624
12221
  }
11625
- const file = path14.resolve(root, scene.image);
12222
+ const file = path15.resolve(root, scene.image);
11626
12223
  const image = await optionalRevisionImage(file, "scene image", "png");
11627
12224
  const base = { file, sha256: image?.hash ?? null, width: image?.width ?? null, height: image?.height ?? null };
11628
12225
  const placement = scene.placement;
11629
12226
  if ("mask" in placement) {
11630
- const maskFile = path14.resolve(root, placement.mask);
12227
+ const maskFile = path15.resolve(root, placement.mask);
11631
12228
  const mask = await optionalRevisionImage(maskFile, "scene mask", "png");
11632
12229
  if (image && mask && (image.width !== mask.width || image.height !== mask.height)) {
11633
12230
  throw new Error(
@@ -11641,9 +12238,38 @@ async function resolveMapScene(root, styleId, assetId, generator, scene, provide
11641
12238
  }
11642
12239
  return "oval" in placement ? { ...base, placement: { type: "oval", fraction: placement.oval } } : { ...base, placement: { type: "rectangle", fraction: placement.rectangle } };
11643
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
+ }
11644
12270
  async function optionalRevisionImage(file, label, requiredFormat) {
11645
- if (!existsSync8(file)) return null;
11646
- const bytes = await readFile6(file);
12271
+ if (!existsSync9(file)) return null;
12272
+ const bytes = await readFile7(file);
11647
12273
  const metadata = imageMetadata(bytes);
11648
12274
  if (!metadata) throw new Error(`${label} is not a readable PNG or JPEG: ${file}`);
11649
12275
  if (requiredFormat && metadata.format !== requiredFormat) {
@@ -11652,8 +12278,8 @@ async function optionalRevisionImage(file, label, requiredFormat) {
11652
12278
  return { hash: sha256(bytes), ...metadata };
11653
12279
  }
11654
12280
  async function optionalSkeletonSet(file) {
11655
- if (!existsSync8(file)) return null;
11656
- const bytes = await readFile6(file);
12281
+ if (!existsSync9(file)) return null;
12282
+ const bytes = await readFile7(file);
11657
12283
  let json;
11658
12284
  try {
11659
12285
  json = JSON.parse(bytes.toString("utf8"));
@@ -11663,17 +12289,17 @@ async function optionalSkeletonSet(file) {
11663
12289
  return { hash: sha256(bytes), skeleton: parseSkeletonSet(json, file) };
11664
12290
  }
11665
12291
  async function revisionSourceMembers(stem, label) {
11666
- if (existsSync8(stem)) return null;
12292
+ if (existsSync9(stem)) return null;
11667
12293
  const at = (role) => memberPath(stem, role, 0, 2, MediaType.PNG);
11668
12294
  let roles = [];
11669
12295
  for (const set of [CHARACTER_DIRECTIONS_8, CHARACTER_DIRECTIONS_4]) {
11670
- if (set.every((direction) => existsSync8(at(direction)))) {
12296
+ if (set.every((direction) => existsSync9(at(direction)))) {
11671
12297
  roles = [...set];
11672
12298
  break;
11673
12299
  }
11674
12300
  }
11675
12301
  if (!roles.length) {
11676
- 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++) {
11677
12303
  roles.push(`frame-${String(index).padStart(2, "0")}`);
11678
12304
  }
11679
12305
  }
@@ -11699,8 +12325,8 @@ async function resolveStyleImages(loaded, styleId) {
11699
12325
  if (!style) throw new Error(`Unknown style "${styleId}"`);
11700
12326
  const out = [];
11701
12327
  for (const img of style.styleImages) {
11702
- const file = path14.resolve(loaded.root, img.path);
11703
- const buf = await readFile6(file);
12328
+ const file = path15.resolve(loaded.root, img.path);
12329
+ const buf = await readFile7(file);
11704
12330
  const metadata = imageMetadata(buf);
11705
12331
  if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${file}`);
11706
12332
  if (metadata.width < 1 || metadata.height < 1) {
@@ -11727,7 +12353,7 @@ async function resolveCharacterShape(asset, style, kind, files) {
11727
12353
  };
11728
12354
  if (asset.concept) {
11729
12355
  const image = await files.load(asset.concept, "Concept image");
11730
- 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 };
11731
12357
  }
11732
12358
  if (asset.reference) {
11733
12359
  const byDirection = typeof asset.reference === "string" ? { south: asset.reference } : asset.reference;
@@ -11735,7 +12361,7 @@ async function resolveCharacterShape(asset, style, kind, files) {
11735
12361
  for (const [direction, rel] of Object.entries(byDirection)) {
11736
12362
  const image = await files.load(rel, `Reference sprite (${direction})`);
11737
12363
  shape.reference[direction] = {
11738
- path: path14.resolve(files.root, rel),
12364
+ path: path15.resolve(files.root, rel),
11739
12365
  sha256: image.hash,
11740
12366
  width: image.width,
11741
12367
  height: image.height,
@@ -11756,7 +12382,7 @@ async function resolveCharacterShape(asset, style, kind, files) {
11756
12382
  const animationMode = asset.animation.mode ?? (asset.animation.template ? "template" : "v3");
11757
12383
  const pose = async (rel, what) => {
11758
12384
  const image = await files.load(rel, what);
11759
- 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 };
11760
12386
  };
11761
12387
  shape.animation = {
11762
12388
  mode: animationMode,
@@ -11787,7 +12413,7 @@ async function resolveObjectProShape(assetId, asset, style, kind, files) {
11787
12413
  const south = typeof asset.reference === "string" ? asset.reference : asset.reference.south;
11788
12414
  if (south) {
11789
12415
  const image = await files.load(south, "Reference sprite");
11790
- 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 };
11791
12417
  }
11792
12418
  }
11793
12419
  if (asset.state) {
@@ -11815,7 +12441,7 @@ async function resolveObjectProShape(assetId, asset, style, kind, files) {
11815
12441
  }
11816
12442
  const pose = async (rel, what) => {
11817
12443
  const image = await files.load(rel, what);
11818
- 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 };
11819
12445
  };
11820
12446
  shape.animation = {
11821
12447
  mode: asset.animation.mode ?? "v3",
@@ -11832,9 +12458,9 @@ async function resolveObjectProShape(assetId, asset, style, kind, files) {
11832
12458
  }
11833
12459
 
11834
12460
  // src/pipeline/fetch.ts
11835
- import { existsSync as existsSync9, readFileSync as readFileSync4 } from "fs";
11836
- import { mkdir as mkdir4, readFile as readFile7, rename as rename4, rm as rm4, writeFile as writeFile4 } from "fs/promises";
11837
- 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";
11838
12464
 
11839
12465
  // src/source-url.ts
11840
12466
  var SENSITIVE_QUERY_KEYS = /* @__PURE__ */ new Set([
@@ -11880,7 +12506,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
11880
12506
  const result = { downloaded: 0, skipped: 0, failed: 0, ...opts.refresh ? { unchanged: 0 } : {} };
11881
12507
  const specByKey = new Map(specs.map((s) => [lockKey(s.styleId, s.assetId), s]));
11882
12508
  normalizeLockOutputPaths(lock, specs);
11883
- 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"));
11884
12510
  const pending = Object.entries(lock.entries).filter(([key, e]) => {
11885
12511
  if (e.provider !== provider.id || !specByKey.has(key)) return false;
11886
12512
  if (specByKey.get(key)?.mirror) return false;
@@ -11891,14 +12517,14 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
11891
12517
  if (spec && e.specHash === spec.specHash && e.outputs.length && !postprocessCurrent(e, spec)) {
11892
12518
  return Boolean(opts.force) || e.outputs.every((output) => {
11893
12519
  const file = resolveOutputPath(output.path, spec.root);
11894
- return existsSync9(file) && sha256(readFileSync4(file)) === output.sha256;
12520
+ return existsSync10(file) && sha256(readFileSync4(file)) === output.sha256;
11895
12521
  });
11896
12522
  }
11897
12523
  if (!opts.repair) return false;
11898
12524
  if (e.outputs.length === 0 || !spec) return true;
11899
12525
  return e.outputs.some((output) => {
11900
12526
  const file = resolveOutputPath(output.path, spec.root);
11901
- if (!existsSync9(file)) return true;
12527
+ if (!existsSync10(file)) return true;
11902
12528
  const current = sha256(readFileSync4(file));
11903
12529
  if (current === output.sha256) return false;
11904
12530
  return Boolean(opts.force) || (e.supersededOutputs ?? []).some((old) => old.sha256 === current);
@@ -11982,21 +12608,21 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
11982
12608
  if (!source.url) throw new Error(`no durable source URL to refresh ${source.role ?? "asset"} from`);
11983
12609
  fresh = await provider.download(source.url);
11984
12610
  if (recorded && sha256(fresh) === recordedRaw && !reapply) {
11985
- log2(` current ${path15.relative(process.cwd(), target)}`);
12611
+ log2(` current ${path16.relative(process.cwd(), target)}`);
11986
12612
  outputs.push({ ...recorded, path: portableOutputPath(target, spec.root) });
11987
12613
  continue;
11988
12614
  }
11989
12615
  }
11990
12616
  let raw = null;
11991
12617
  let final = null;
11992
- if (existsSync9(target)) {
12618
+ if (existsSync10(target)) {
11993
12619
  const currentHash = await sha256File(target);
11994
12620
  if (recorded && currentHash === recorded.sha256) {
11995
12621
  if (!fresh && !reapply) {
11996
12622
  if (cacheDir) {
11997
12623
  await cacheMedia(
11998
12624
  cacheDir,
11999
- await readFile7(target),
12625
+ await readFile8(target),
12000
12626
  recorded.mediaType ?? MediaType.PNG,
12001
12627
  recorded.sha256
12002
12628
  );
@@ -12005,10 +12631,10 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12005
12631
  continue;
12006
12632
  }
12007
12633
  if (reapply && !fresh) {
12008
- if (!recorded.raw) raw = await readFile7(target);
12009
- log2(` reapply ${path15.relative(process.cwd(), target)}`);
12634
+ if (!recorded.raw) raw = await readFile8(target);
12635
+ log2(` reapply ${path16.relative(process.cwd(), target)}`);
12010
12636
  } else {
12011
- log2(` changed ${path15.relative(process.cwd(), target)} (upstream)`);
12637
+ log2(` changed ${path16.relative(process.cwd(), target)} (upstream)`);
12012
12638
  }
12013
12639
  } else {
12014
12640
  const superseded = (entry.supersededOutputs ?? []).find(
@@ -12029,14 +12655,14 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12029
12655
  }
12030
12656
  if (outgoing && cacheDir) {
12031
12657
  try {
12032
- const oldBytes = await readFile7(target);
12658
+ const oldBytes = await readFile8(target);
12033
12659
  const oldMediaType = detectMediaType(oldBytes);
12034
12660
  if (oldMediaType) await cacheMedia(cacheDir, oldBytes, oldMediaType, currentHash);
12035
12661
  } catch {
12036
12662
  }
12037
12663
  }
12038
12664
  log2(
12039
- ` 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)")
12040
12666
  );
12041
12667
  }
12042
12668
  }
@@ -12045,7 +12671,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12045
12671
  } else if (!raw && recorded && cacheDir) {
12046
12672
  if (!reapply) final = await readCachedMedia(cacheDir, recorded.sha256, expectedMediaType);
12047
12673
  if (!final && recordedRaw) raw = await readCachedMedia(cacheDir, recordedRaw, expectedMediaType);
12048
- if (final || raw) log2(` cached ${path15.relative(process.cwd(), target)}`);
12674
+ if (final || raw) log2(` cached ${path16.relative(process.cwd(), target)}`);
12049
12675
  }
12050
12676
  if (!final && !raw) {
12051
12677
  if (!source.url) {
@@ -12072,13 +12698,13 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12072
12698
  buf = processed.bytes;
12073
12699
  if (processed.changed) {
12074
12700
  rawHash = sha256(raw);
12075
- 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`);
12076
12702
  }
12077
12703
  }
12078
12704
  if (cacheDir) await cacheMedia(cacheDir, buf, mediaType);
12079
- await mkdir4(path15.dirname(target), { recursive: true });
12705
+ await mkdir5(path16.dirname(target), { recursive: true });
12080
12706
  const tmp = `${target}.pixelkiln.tmp`;
12081
- await writeFile4(tmp, buf);
12707
+ await writeFile5(tmp, buf);
12082
12708
  outputs.push({
12083
12709
  path: portableOutputPath(target, spec.root),
12084
12710
  sha256: sha256(buf),
@@ -12090,7 +12716,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12090
12716
  await saveLock(lockPath, lock);
12091
12717
  await rename4(tmp, target);
12092
12718
  wrote = true;
12093
- log2(` wrote ${path15.relative(process.cwd(), target)}`);
12719
+ log2(` wrote ${path16.relative(process.cwd(), target)}`);
12094
12720
  }
12095
12721
  if (opts.refresh && !wrote) {
12096
12722
  if (rotated) {
@@ -12134,10 +12760,10 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12134
12760
  return result;
12135
12761
  }
12136
12762
  async function readCachedMedia(cacheDir, hash, mediaType) {
12137
- const file = path15.join(cacheDir, cacheFileName(hash, mediaType));
12138
- if (!existsSync9(file)) return null;
12763
+ const file = path16.join(cacheDir, cacheFileName(hash, mediaType));
12764
+ if (!existsSync10(file)) return null;
12139
12765
  try {
12140
- const buf = await readFile7(file);
12766
+ const buf = await readFile8(file);
12141
12767
  if (sha256(buf) !== hash) return null;
12142
12768
  validateMedia(buf, mediaType);
12143
12769
  return buf;
@@ -12148,15 +12774,15 @@ async function readCachedMedia(cacheDir, hash, mediaType) {
12148
12774
  async function cacheMedia(cacheDir, buf, mediaType, knownHash) {
12149
12775
  const hash = knownHash ?? sha256(buf);
12150
12776
  validateMedia(buf, mediaType);
12151
- const file = path15.join(cacheDir, cacheFileName(hash, mediaType));
12777
+ const file = path16.join(cacheDir, cacheFileName(hash, mediaType));
12152
12778
  if (await readCachedMedia(cacheDir, hash, mediaType)) return;
12153
- await mkdir4(cacheDir, { recursive: true });
12779
+ await mkdir5(cacheDir, { recursive: true });
12154
12780
  const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
12155
- await writeFile4(tmp, buf);
12781
+ await writeFile5(tmp, buf);
12156
12782
  try {
12157
12783
  await rename4(tmp, file);
12158
12784
  } catch (err) {
12159
- if (!existsSync9(file)) throw err;
12785
+ if (!existsSync10(file)) throw err;
12160
12786
  } finally {
12161
12787
  await rm4(tmp, { force: true });
12162
12788
  }
@@ -12176,7 +12802,7 @@ async function pushTags(provider, specs, lock, opts = {}) {
12176
12802
  for (const [key, entry] of Object.entries(lock.entries)) {
12177
12803
  const spec = specByKey.get(key);
12178
12804
  if (!spec || entry.provider !== provider.id || !entry.objectId) continue;
12179
- if (entry.generator === "pixflux") continue;
12805
+ if (entry.generator === "pixflux" || entry.generator === "vfx") continue;
12180
12806
  if (!provider.setTags) return tagged;
12181
12807
  try {
12182
12808
  await provider.setTags(entry.objectId, spec.tags, entry.generator);
@@ -12189,13 +12815,13 @@ async function pushTags(provider, specs, lock, opts = {}) {
12189
12815
  }
12190
12816
 
12191
12817
  // src/pipeline/adopt.ts
12192
- import { existsSync as existsSync11 } from "fs";
12193
- import { readFile as readFile9, writeFile as writeFile6 } from "fs/promises";
12194
- 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";
12195
12821
 
12196
12822
  // src/cache.ts
12197
- import { mkdir as mkdir5, readFile as readFile8, rename as rename5, rm as rm5, writeFile as writeFile5 } from "fs/promises";
12198
- 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";
12199
12825
  import pathModule from "path";
12200
12826
  import { z as z4 } from "zod";
12201
12827
  var HashCacheSchema = z4.object({
@@ -12210,10 +12836,10 @@ function isSha256Hash(value) {
12210
12836
  function parseCache(value) {
12211
12837
  return HashCacheSchema.parse(value);
12212
12838
  }
12213
- async function loadCache(path52) {
12214
- if (!existsSync10(path52)) return { version: 1, hashes: {} };
12839
+ async function loadCache(path53) {
12840
+ if (!existsSync11(path53)) return { version: 1, hashes: {} };
12215
12841
  try {
12216
- const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile8(path52, "utf8")));
12842
+ const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile9(path53, "utf8")));
12217
12843
  if (!parsed.success) return { version: 1, hashes: {} };
12218
12844
  return {
12219
12845
  version: 1,
@@ -12225,18 +12851,18 @@ async function loadCache(path52) {
12225
12851
  return { version: 1, hashes: {} };
12226
12852
  }
12227
12853
  }
12228
- async function saveCache(path52, cache) {
12854
+ async function saveCache(path53, cache) {
12229
12855
  const sorted = {};
12230
12856
  for (const key of Object.keys(cache.hashes).sort()) {
12231
12857
  const hash = cache.hashes[key];
12232
12858
  if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
12233
12859
  sorted[key] = hash;
12234
12860
  }
12235
- await mkdir5(pathModule.dirname(pathModule.resolve(path52)), { recursive: true });
12236
- 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`;
12237
12863
  try {
12238
- await writeFile5(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
12239
- await rename5(tmp, path52);
12864
+ await writeFile6(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
12865
+ await rename5(tmp, path53);
12240
12866
  } finally {
12241
12867
  await rm5(tmp, { force: true });
12242
12868
  }
@@ -12270,11 +12896,11 @@ async function adopt(provider, specs, lock, lockPath, opts = {}) {
12270
12896
  const localByHash = /* @__PURE__ */ new Map();
12271
12897
  const unmatchedLocal = [];
12272
12898
  for (const spec of specs) {
12273
- if (!existsSync11(spec.outFile)) {
12899
+ if (!existsSync12(spec.outFile)) {
12274
12900
  unmatchedLocal.push(`${spec.styleId}/${spec.assetId} (no file at ${spec.outFile})`);
12275
12901
  continue;
12276
12902
  }
12277
- const bytes = await readFile9(spec.outFile);
12903
+ const bytes = await readFile10(spec.outFile);
12278
12904
  try {
12279
12905
  decodePng(bytes);
12280
12906
  } catch (err) {
@@ -12394,7 +13020,7 @@ async function writePromptsBack(manifestPath, lock, opts = {}) {
12394
13020
  const log2 = opts.onProgress ?? (() => {
12395
13021
  });
12396
13022
  const selectedAssets = opts.assetIds ? new Set(opts.assetIds) : null;
12397
- const raw = JSON.parse(await readFile9(manifestPath, "utf8"));
13023
+ const raw = JSON.parse(await readFile10(manifestPath, "utf8"));
12398
13024
  const best = /* @__PURE__ */ new Map();
12399
13025
  for (const entry of Object.values(lock.entries)) {
12400
13026
  if (opts.provider && entry.provider !== opts.provider) continue;
@@ -12416,7 +13042,7 @@ async function writePromptsBack(manifestPath, lock, opts = {}) {
12416
13042
  stillEmpty.push(id);
12417
13043
  }
12418
13044
  }
12419
- await writeFile6(manifestPath, JSON.stringify(raw, null, 2) + "\n");
13045
+ await writeFile7(manifestPath, JSON.stringify(raw, null, 2) + "\n");
12420
13046
  log2(` filled ${filled} prompt(s) from adopted objects`);
12421
13047
  return { filled, stillEmpty };
12422
13048
  }
@@ -12427,9 +13053,9 @@ function formatUnmatchedRemote(objects, limit = 15) {
12427
13053
  }
12428
13054
 
12429
13055
  // src/pipeline/adopt-characters.ts
12430
- import { existsSync as existsSync12 } from "fs";
12431
- import { mkdir as mkdir6, readFile as readFile10, writeFile as writeFile7 } from "fs/promises";
12432
- 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";
12433
13059
  async function adoptCharacters(provider, specs, lock, lockPath, opts) {
12434
13060
  const log2 = opts.onProgress ?? (() => {
12435
13061
  });
@@ -12480,12 +13106,12 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
12480
13106
  const bytes = await download(source.url);
12481
13107
  const hash = sha256(bytes);
12482
13108
  const file = expectedOutputPath(spec, source.role, index, sources.length);
12483
- if (existsSync12(file)) {
12484
- 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));
12485
13111
  } else {
12486
- await mkdir6(path17.dirname(file), { recursive: true });
12487
- await writeFile7(file, bytes);
12488
- 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));
12489
13115
  }
12490
13116
  outputs.push({ path: portableOutputPath(file, spec.root), sha256: hash, ...source.role ? { role: source.role } : {} });
12491
13117
  }
@@ -12523,11 +13149,11 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
12523
13149
  if (!match) throw new Error(`remoteId ${spec.remoteId} is not a character on this account`);
12524
13150
  } else {
12525
13151
  const south = expectedOutputPath(spec, "south", 0, spec.character.directions);
12526
- if (!existsSync12(south)) {
12527
- 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`);
12528
13154
  return false;
12529
13155
  }
12530
- const localHash = sha256(await readFile10(south));
13156
+ const localHash = sha256(await readFile11(south));
12531
13157
  for (const candidate of remote) {
12532
13158
  if (claimed.has(candidate.id)) continue;
12533
13159
  if (await southHash(candidate) === localHash) {
@@ -12536,7 +13162,7 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
12536
13162
  }
12537
13163
  }
12538
13164
  if (!match) {
12539
- 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)}`);
12540
13166
  return false;
12541
13167
  }
12542
13168
  }
@@ -12603,15 +13229,15 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
12603
13229
  if (!match) throw new Error(`remoteId ${spec.remoteId} names no ${animation.direction} animation of character ${parentId}`);
12604
13230
  } else {
12605
13231
  const first = expectedOutputPath(spec, "frame-00", 0, 2);
12606
- if (!existsSync12(first)) {
13232
+ if (!existsSync13(first)) {
12607
13233
  const byName = candidates.find((a) => a.name === characterAnimationName(spec));
12608
13234
  if (!byName) {
12609
- 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`);
12610
13236
  return false;
12611
13237
  }
12612
13238
  match = byName;
12613
13239
  } else {
12614
- const localHash = sha256(await readFile10(first));
13240
+ const localHash = sha256(await readFile11(first));
12615
13241
  for (const candidate of candidates) {
12616
13242
  if (sha256(await download(candidate.frames[0])) === localHash) {
12617
13243
  match = candidate;
@@ -12619,7 +13245,7 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
12619
13245
  }
12620
13246
  }
12621
13247
  if (!match) {
12622
- 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)}`);
12623
13249
  return false;
12624
13250
  }
12625
13251
  }
@@ -12679,19 +13305,19 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
12679
13305
  }
12680
13306
 
12681
13307
  // src/pipeline/salvage.ts
12682
- import { readFile as readFile11 } from "fs/promises";
12683
- import { existsSync as existsSync13 } from "fs";
12684
- import path18 from "path";
13308
+ import { readFile as readFile12 } from "fs/promises";
13309
+ import { existsSync as existsSync14 } from "fs";
13310
+ import path19 from "path";
12685
13311
  function providerClaimId(provider, objectId) {
12686
13312
  return `${encodeURIComponent(provider)}:${encodeURIComponent(objectId)}`;
12687
13313
  }
12688
13314
  async function loadClaims(lockPaths, opts = {}) {
12689
13315
  const claimed = /* @__PURE__ */ new Set();
12690
13316
  for (const p of lockPaths) {
12691
- if (!existsSync13(p)) throw new Error(`Claim lockfile not found: ${p}`);
13317
+ if (!existsSync14(p)) throw new Error(`Claim lockfile not found: ${p}`);
12692
13318
  let parsed;
12693
13319
  try {
12694
- parsed = parseLock(JSON.parse(await readFile11(p, "utf8")));
13320
+ parsed = parseLock(JSON.parse(await readFile12(p, "utf8")));
12695
13321
  } catch {
12696
13322
  throw new Error(`Claim lockfile is malformed: ${p}`);
12697
13323
  }
@@ -12858,20 +13484,20 @@ function matchStyleByPattern(prompt, manifest) {
12858
13484
  return null;
12859
13485
  }
12860
13486
  async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, claimPaths) {
12861
- const own = path18.resolve(ownManifestPath);
13487
+ const own = path19.resolve(ownManifestPath);
12862
13488
  const siblingManifestPaths = [
12863
13489
  .../* @__PURE__ */ new Set([
12864
13490
  ...workspaceManifestPaths,
12865
- ...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"))
12866
13492
  ])
12867
13493
  ];
12868
13494
  const siblings = [];
12869
13495
  for (const siblingManifestPath of siblingManifestPaths) {
12870
- if (path18.resolve(siblingManifestPath) === own) continue;
12871
- if (!existsSync13(siblingManifestPath)) continue;
13496
+ if (path19.resolve(siblingManifestPath) === own) continue;
13497
+ if (!existsSync14(siblingManifestPath)) continue;
12872
13498
  try {
12873
13499
  const { manifest } = await loadManifest(siblingManifestPath);
12874
- siblings.push({ label: path18.basename(path18.dirname(siblingManifestPath)), manifest });
13500
+ siblings.push({ label: path19.basename(path19.dirname(siblingManifestPath)), manifest });
12875
13501
  } catch {
12876
13502
  }
12877
13503
  }
@@ -12991,9 +13617,9 @@ async function applyTags(provider, decisions, existing, opts = {}) {
12991
13617
  }
12992
13618
 
12993
13619
  // src/workspace.ts
12994
- import { mkdir as mkdir7, readFile as readFile12, rename as rename6, rm as rm6, writeFile as writeFile8 } from "fs/promises";
12995
- import { existsSync as existsSync14 } from "fs";
12996
- 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";
12997
13623
  import { z as z5 } from "zod";
12998
13624
  var WorkspaceProjectSchema = z5.object({
12999
13625
  id: z5.string().min(1),
@@ -13018,10 +13644,10 @@ ${parsed.error.issues.slice(0, 5).map((i) => ` ${i.path.join(".")}: ${i.message
13018
13644
  );
13019
13645
  }
13020
13646
  async function loadWorkspace(workspacePath) {
13021
- if (!existsSync14(workspacePath)) return { version: 1, projects: [] };
13647
+ if (!existsSync15(workspacePath)) return { version: 1, projects: [] };
13022
13648
  let raw;
13023
13649
  try {
13024
- raw = JSON.parse(await readFile12(workspacePath, "utf8"));
13650
+ raw = JSON.parse(await readFile13(workspacePath, "utf8"));
13025
13651
  } catch (err) {
13026
13652
  throw new ProjectError(
13027
13653
  `Workspace catalog at ${workspacePath} is malformed:
@@ -13035,22 +13661,22 @@ async function saveWorkspace(workspacePath, ws) {
13035
13661
  version: 1,
13036
13662
  projects: [...ws.projects].sort((a, b) => a.id.localeCompare(b.id))
13037
13663
  };
13038
- await mkdir7(path19.dirname(path19.resolve(workspacePath)), { recursive: true });
13664
+ await mkdir8(path20.dirname(path20.resolve(workspacePath)), { recursive: true });
13039
13665
  const tmp = `${workspacePath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
13040
13666
  try {
13041
- await writeFile8(tmp, JSON.stringify(sorted, null, 2) + "\n");
13667
+ await writeFile9(tmp, JSON.stringify(sorted, null, 2) + "\n");
13042
13668
  await rename6(tmp, workspacePath);
13043
13669
  } finally {
13044
13670
  await rm6(tmp, { force: true });
13045
13671
  }
13046
13672
  }
13047
13673
  function toPortablePath(dir, absolute) {
13048
- return path19.relative(dir, absolute).split(path19.sep).join("/");
13674
+ return path20.relative(dir, absolute).split(path20.sep).join("/");
13049
13675
  }
13050
13676
  function resolveProject(dir, project) {
13051
13677
  return {
13052
- manifestPath: path19.resolve(dir, project.manifest.split("/").join(path19.sep)),
13053
- 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))
13054
13680
  };
13055
13681
  }
13056
13682
  function validateWorkspace(ws, dir) {
@@ -13063,21 +13689,21 @@ function validateWorkspace(ws, dir) {
13063
13689
  const { manifestPath, lockPath } = resolveProject(dir, project);
13064
13690
  lockOwners.set(lockPath, [...lockOwners.get(lockPath) ?? [], project.id]);
13065
13691
  manifestOwners.set(manifestPath, [...manifestOwners.get(manifestPath) ?? [], project.id]);
13066
- if (path19.isAbsolute(project.manifest) || path19.isAbsolute(project.lock)) {
13692
+ if (path20.isAbsolute(project.manifest) || path20.isAbsolute(project.lock)) {
13067
13693
  diagnostics.push({
13068
13694
  id: "absolute-path",
13069
13695
  level: "warning",
13070
13696
  message: `project "${project.id}" stores an absolute path; the catalog will not resolve correctly if this tree is cloned or moved elsewhere`
13071
13697
  });
13072
13698
  }
13073
- if (!existsSync14(manifestPath)) {
13699
+ if (!existsSync15(manifestPath)) {
13074
13700
  diagnostics.push({
13075
13701
  id: "missing-manifest",
13076
13702
  level: "error",
13077
13703
  message: `project "${project.id}" manifest not found: ${manifestPath}`
13078
13704
  });
13079
13705
  }
13080
- if (!existsSync14(lockPath)) {
13706
+ if (!existsSync15(lockPath)) {
13081
13707
  diagnostics.push({
13082
13708
  id: "missing-lock",
13083
13709
  level: "error",
@@ -13124,8 +13750,8 @@ function validateWorkspace(ws, dir) {
13124
13750
  }
13125
13751
 
13126
13752
  // src/pick/salvage-server.ts
13127
- import { mkdir as mkdir8, writeFile as writeFile9, readFile as readFile14 } from "fs/promises";
13128
- import path20 from "path";
13753
+ import { mkdir as mkdir9, writeFile as writeFile10, readFile as readFile15 } from "fs/promises";
13754
+ import path21 from "path";
13129
13755
 
13130
13756
  // src/pick/salvage-sheet.ts
13131
13757
  var escapeHtml = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
@@ -13286,7 +13912,7 @@ refresh();
13286
13912
 
13287
13913
  // src/pick/review-server.ts
13288
13914
  import { createServer } from "http";
13289
- import { readFile as readFile13 } from "fs/promises";
13915
+ import { readFile as readFile14 } from "fs/promises";
13290
13916
 
13291
13917
  // src/open.ts
13292
13918
  import { spawn } from "child_process";
@@ -13313,7 +13939,7 @@ function serveReviewPage(opts) {
13313
13939
  if (req.method === "GET" && req.url && opts.assets?.has(req.url)) {
13314
13940
  try {
13315
13941
  const asset = opts.assets.get(req.url);
13316
- const bytes = await readFile13(asset.path);
13942
+ const bytes = await readFile14(asset.path);
13317
13943
  res.writeHead(200, {
13318
13944
  "Content-Type": asset.contentType,
13319
13945
  "Cache-Control": "no-store",
@@ -13390,7 +14016,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
13390
14016
  });
13391
14017
  const html = renderSalvageSheet(orphans, {
13392
14018
  styleId: ctx.styleId,
13393
- importDir: path20.relative(process.cwd(), ctx.importDir) || "."
14019
+ importDir: path21.relative(process.cwd(), ctx.importDir) || "."
13394
14020
  });
13395
14021
  const byId = new Map(orphans.map((o) => [o.id, o]));
13396
14022
  const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
@@ -13446,10 +14072,10 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
13446
14072
  if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
13447
14073
  decodePng(buf);
13448
14074
  const assetId = idFromPrompt(orphan.prompt, taken);
13449
- const rel = path20.join("_salvaged", `${assetId}.png`);
13450
- const outFile = path20.resolve(ctx.importDir, rel);
13451
- await mkdir8(path20.dirname(outFile), { recursive: true });
13452
- 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);
13453
14079
  ctx.manifest.assets[assetId] = {
13454
14080
  prompt: orphan.prompt,
13455
14081
  promptByStyle: {},
@@ -13481,7 +14107,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
13481
14107
  sourceUrl: durableSource,
13482
14108
  sourceUrls: durableSource ? [{ url: durableSource }] : [],
13483
14109
  outputs: [{
13484
- path: portableOutputPath(outFile, path20.dirname(ctx.manifestPath)),
14110
+ path: portableOutputPath(outFile, path21.dirname(ctx.manifestPath)),
13485
14111
  sha256: sha256(buf)
13486
14112
  }],
13487
14113
  submittedAt: orphan.createdAt,
@@ -13505,9 +14131,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
13505
14131
  }
13506
14132
  }
13507
14133
  await applyTags(provider, decisions, existingTags, { onProgress: log2, characters: characterMembers });
13508
- const raw = JSON.parse(await readFile14(ctx.manifestPath, "utf8"));
14134
+ const raw = JSON.parse(await readFile15(ctx.manifestPath, "utf8"));
13509
14135
  for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
13510
- await writeFile9(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
14136
+ await writeFile10(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
13511
14137
  await saveLock(ctx.lockPath, ctx.lock);
13512
14138
  if (characterAssetIds.length) result.characterAssetIds = characterAssetIds;
13513
14139
  return result;
@@ -13516,19 +14142,19 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
13516
14142
  }
13517
14143
 
13518
14144
  // src/cli/project.ts
13519
- import path22 from "path";
13520
- import { existsSync as existsSync15 } from "fs";
14145
+ import path23 from "path";
14146
+ import { existsSync as existsSync16 } from "fs";
13521
14147
 
13522
14148
  // src/project.ts
13523
- import path21 from "path";
14149
+ import path22 from "path";
13524
14150
  async function openProject(manifestPath = "pixelkiln.manifest.json", opts = {}) {
13525
- const absolute = path21.resolve(manifestPath);
13526
- const root = path21.dirname(absolute);
14151
+ const absolute = path22.resolve(manifestPath);
14152
+ const root = path22.dirname(absolute);
13527
14153
  if (opts.env !== false) {
13528
14154
  loadEnvFiles(root);
13529
- if (path21.resolve(process.cwd()) !== root) loadEnvFiles(process.cwd());
14155
+ if (path22.resolve(process.cwd()) !== root) loadEnvFiles(process.cwd());
13530
14156
  }
13531
- 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"));
13532
14158
  const loaded = await loadManifest(absolute);
13533
14159
  const specs = await resolveSpecs(loaded, { styles: opts.styles ?? [], assets: opts.assets ?? [] });
13534
14160
  const lock = await loadLock(lockPath);
@@ -13659,18 +14285,18 @@ async function workspaceStatus(ws, dir) {
13659
14285
 
13660
14286
  // src/cli/project.ts
13661
14287
  async function provenanceFile(id, file) {
13662
- const absolute = path22.resolve(file);
14288
+ const absolute = path23.resolve(file);
13663
14289
  return {
13664
14290
  id,
13665
14291
  path: absolute,
13666
- sha256: existsSync15(absolute) ? await sha256File(absolute) : null,
14292
+ sha256: existsSync16(absolute) ? await sha256File(absolute) : null,
13667
14293
  included: true
13668
14294
  };
13669
14295
  }
13670
14296
  function manifestSources(manifest, styleId) {
13671
14297
  const sources = {};
13672
14298
  for (const [assetId, asset] of Object.entries(manifest.assets)) {
13673
- if (asset.styles.length && !asset.styles.includes(styleId)) continue;
14299
+ if (!assetInStyle(asset, styleId, manifest.styles[styleId])) continue;
13674
14300
  const source = asset.sourceByStyle[styleId] ?? asset.source;
13675
14301
  if (source) sources[assetId] = source;
13676
14302
  }
@@ -13735,10 +14361,10 @@ function budgetsForPlan(plan, args) {
13735
14361
  return out;
13736
14362
  }
13737
14363
  async function requireCompleteWorkspaceClaims(workspacePath) {
13738
- if (!existsSync15(workspacePath)) {
14364
+ if (!existsSync16(workspacePath)) {
13739
14365
  throw new ProjectError(`Workspace catalog not found: ${workspacePath}`);
13740
14366
  }
13741
- const dir = path22.dirname(path22.resolve(workspacePath));
14367
+ const dir = path23.dirname(path23.resolve(workspacePath));
13742
14368
  const ws = await loadWorkspace(workspacePath);
13743
14369
  const diagnostics = validateWorkspace(ws, dir);
13744
14370
  const errors = diagnostics.filter((d) => d.level === "error");
@@ -13761,9 +14387,9 @@ async function openAccountProject(args) {
13761
14387
  return project;
13762
14388
  }
13763
14389
  async function open(args) {
13764
- if (!existsSync15(path22.resolve(args.manifest))) {
14390
+ if (!existsSync16(path23.resolve(args.manifest))) {
13765
14391
  throw new ProjectError(
13766
- `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>\`.`
13767
14393
  );
13768
14394
  }
13769
14395
  const project = await openProject(args.manifest, { lockPath: args.lock, styles: args.styles, assets: args.assets });
@@ -13805,7 +14431,7 @@ function providerCache(mode2) {
13805
14431
 
13806
14432
  // src/cli/commands/account.ts
13807
14433
  async function runBalance(args) {
13808
- loadEnvFiles(path23.dirname(path23.resolve(args.manifest)));
14434
+ loadEnvFiles(path24.dirname(path24.resolve(args.manifest)));
13809
14435
  loadEnvFiles(process.cwd());
13810
14436
  const loaded = await loadManifest(args.manifest);
13811
14437
  const selectedProvider = accountProviderId(loaded.manifest, args.provider, "balance");
@@ -13878,12 +14504,12 @@ async function runAdopt(args) {
13878
14504
  tagged ${n} object(s) upstream`);
13879
14505
  }
13880
14506
  if (args.writePrompts) {
13881
- const { filled, stillEmpty } = await writePromptsBack(path23.resolve(args.manifest), lock, {
14507
+ const { filled, stillEmpty } = await writePromptsBack(path24.resolve(args.manifest), lock, {
13882
14508
  onProgress: log,
13883
14509
  provider: provider.id,
13884
14510
  assetIds: specs.map((spec) => spec.assetId)
13885
14511
  });
13886
- 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)}`);
13887
14513
  const reloaded = await loadManifest(args.manifest);
13888
14514
  const rebased = (await resolveSpecs(reloaded, {
13889
14515
  styles: args.styles,
@@ -13913,12 +14539,12 @@ async function runSalvage2(args) {
13913
14539
  const provider = providerFor(accountProvider);
13914
14540
  const jsonMode = args.dryRun && args.json;
13915
14541
  const diag = jsonMode ? (msg = "") => console.error(msg) : log;
13916
- const ownLock = path23.resolve(args.lock);
14542
+ const ownLock = path24.resolve(args.lock);
13917
14543
  let workspaceProjects = [];
13918
14544
  let workspaceDir = "";
13919
14545
  if (args.workspace) {
13920
- const workspacePath = path23.resolve(args.workspace);
13921
- workspaceDir = path23.dirname(workspacePath);
14546
+ const workspacePath = path24.resolve(args.workspace);
14547
+ workspaceDir = path24.dirname(workspacePath);
13922
14548
  const complete = await requireCompleteWorkspaceClaims(workspacePath);
13923
14549
  workspaceProjects = complete.ws.projects;
13924
14550
  for (const d of complete.diagnostics) diag(` WARN ${d.id}: ${d.message}`);
@@ -13927,12 +14553,12 @@ async function runSalvage2(args) {
13927
14553
  const lockPaths = [
13928
14554
  .../* @__PURE__ */ new Set([
13929
14555
  ...workspaceLockPaths,
13930
- ...existsSync16(ownLock) ? [ownLock] : [],
13931
- ...args.claims.map((c) => path23.resolve(c))
14556
+ ...existsSync17(ownLock) ? [ownLock] : [],
14557
+ ...args.claims.map((c) => path24.resolve(c))
13932
14558
  ])
13933
14559
  ];
13934
14560
  diag(` claim set (${lockPaths.length} lockfile(s)):`);
13935
- for (const p of lockPaths) diag(` ${path23.relative(process.cwd(), p)}`);
14561
+ for (const p of lockPaths) diag(` ${path24.relative(process.cwd(), p)}`);
13936
14562
  if (!args.claims.length && !args.workspace) {
13937
14563
  diag(
13938
14564
  `
@@ -13940,7 +14566,7 @@ async function runSalvage2(args) {
13940
14566
  pass every other project's lockfile via --claims a.json,b.json, or
13941
14567
  register every project in a workspace catalog and pass --workspace.`
13942
14568
  );
13943
- } 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))) {
13944
14570
  diag(
13945
14571
  `
13946
14572
  This project's manifest is not registered in the workspace catalog. Its own
@@ -14059,7 +14685,7 @@ async function runSalvage2(args) {
14059
14685
  manifestPath: loaded.path,
14060
14686
  manifest: accountManifest,
14061
14687
  styleId,
14062
- importDir: path23.resolve(loaded.root, style.outDir),
14688
+ importDir: path24.resolve(loaded.root, style.outDir),
14063
14689
  lock,
14064
14690
  lockPath: args.lock
14065
14691
  },
@@ -14186,20 +14812,20 @@ async function runTag(args) {
14186
14812
  }
14187
14813
 
14188
14814
  // src/cli/commands/edit.ts
14189
- import path26 from "path";
14815
+ import path27 from "path";
14190
14816
 
14191
14817
  // src/pipeline/hand-edit.ts
14192
- import { existsSync as existsSync17 } from "fs";
14193
- import { copyFile, mkdir as mkdir9, readFile as readFile16, rename as rename8, rm as rm8, writeFile as writeFile11 } from "fs/promises";
14194
- 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";
14195
14821
  import { z as z7 } from "zod";
14196
14822
 
14197
14823
  // src/manifest-edit.ts
14198
- import { readFile as readFile15, rename as rename7, rm as rm7, writeFile as writeFile10 } from "fs/promises";
14199
- 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";
14200
14826
  import { z as z6 } from "zod";
14201
14827
  var HexSha = z6.string().regex(/^[0-9a-f]{64}$/);
14202
- 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");
14203
14829
  var AssetPatchSchema = z6.object({
14204
14830
  prompt: z6.string().optional(),
14205
14831
  /** Per-style override for the style being viewed; null removes it. */
@@ -14618,8 +15244,8 @@ function applyEdit(raw, edit) {
14618
15244
  }
14619
15245
  }
14620
15246
  async function withCandidate(manifestPath, edit, use, unchanged) {
14621
- const absolute = path24.resolve(manifestPath);
14622
- const original = await readFile15(absolute, "utf8");
15247
+ const absolute = path25.resolve(manifestPath);
15248
+ const original = await readFile16(absolute, "utf8");
14623
15249
  if (sha256(original) !== edit.expectedSha256) throw new ManifestDriftError(absolute);
14624
15250
  let raw;
14625
15251
  try {
@@ -14632,12 +15258,12 @@ async function withCandidate(manifestPath, edit, use, unchanged) {
14632
15258
  applyEdit(raw, edit);
14633
15259
  const next = serializeLike(original, raw);
14634
15260
  if (next === original) return unchanged(absolute, original);
14635
- const tmp = path24.join(
14636
- path24.dirname(absolute),
14637
- `.${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`
14638
15264
  );
14639
15265
  try {
14640
- await writeFile10(tmp, next);
15266
+ await writeFile11(tmp, next);
14641
15267
  let specs;
14642
15268
  try {
14643
15269
  specs = await resolveSpecs(await loadManifest(tmp));
@@ -14707,11 +15333,11 @@ async function priceManifestEdit(manifestPath, edit, opts = {}) {
14707
15333
  var HAND_EDIT_DIR = "edits";
14708
15334
  function handEditPath(loaded, spec) {
14709
15335
  const style = loaded.manifest.styles[spec.styleId];
14710
- const outDir = path25.resolve(loaded.root, style?.outDir ?? ".");
14711
- const relative = path25.relative(outDir, spec.outFile);
14712
- const inside = relative && !relative.startsWith("..") && !path25.isAbsolute(relative);
14713
- const file = inside ? relative : path25.basename(spec.outFile);
14714
- 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"));
14715
15341
  }
14716
15342
  function handEditBases(spec, lock) {
14717
15343
  const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
@@ -14724,10 +15350,10 @@ function handEditBases(spec, lock) {
14724
15350
  }));
14725
15351
  if (members.some((member) => member.mediaType && member.mediaType !== "image/png")) return null;
14726
15352
  if (entry.outputs.length === 1 && !primaryOutput(entry)) return null;
14727
- if (!members.every((member) => existsSync17(member.path))) return null;
15353
+ if (!members.every((member) => existsSync18(member.path))) return null;
14728
15354
  return members.map(({ role, path: file }) => ({ role, path: file }));
14729
15355
  }
14730
- return existsSync17(spec.outFile) ? [{ role: null, path: spec.outFile }] : null;
15356
+ return existsSync18(spec.outFile) ? [{ role: null, path: spec.outFile }] : null;
14731
15357
  }
14732
15358
  function handEditMembers(loaded, spec, lock, editPath) {
14733
15359
  const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
@@ -14753,18 +15379,18 @@ async function startHandEdit(loaded, lock, spec, opts = {}) {
14753
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`
14754
15380
  );
14755
15381
  }
14756
- const existing = spec.source ? path25.resolve(loaded.root, spec.source) : null;
15382
+ const existing = spec.source ? path26.resolve(loaded.root, spec.source) : null;
14757
15383
  const editPath = existing ?? handEditPath(loaded, spec);
14758
15384
  const members = handEditMembers(loaded, spec, lock, editPath);
14759
15385
  let created = false;
14760
15386
  for (const member of members) {
14761
- if (existsSync17(member.path)) continue;
15387
+ if (existsSync18(member.path)) continue;
14762
15388
  if (!member.base) {
14763
15389
  throw new ManifestEditError(
14764
15390
  `${spec.styleId}/${spec.assetId} has no generated PNG on disk to start from; generate or restore it first`
14765
15391
  );
14766
15392
  }
14767
- await mkdir9(path25.dirname(member.path), { recursive: true });
15393
+ await mkdir10(path26.dirname(member.path), { recursive: true });
14768
15394
  await copyFile(member.base, member.path);
14769
15395
  created = true;
14770
15396
  }
@@ -14843,7 +15469,7 @@ function handEditProjectPath(editPath) {
14843
15469
  async function readHandEditCompanion(editPath) {
14844
15470
  let text;
14845
15471
  try {
14846
- text = await readFile16(handEditCompanionPath(editPath), "utf8");
15472
+ text = await readFile17(handEditCompanionPath(editPath), "utf8");
14847
15473
  } catch {
14848
15474
  return null;
14849
15475
  }
@@ -14886,7 +15512,7 @@ async function saveHandEdit(loaded, lock, spec, input) {
14886
15512
  }
14887
15513
  });
14888
15514
  const bases = handEditBases(spec, lock);
14889
- 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 };
14890
15516
  for (const [index, image] of decoded.entries()) {
14891
15517
  if (image.width !== expectedSize.width || image.height !== expectedSize.height) {
14892
15518
  const which = memberSet ? `member ${incoming[index].role ?? index}` : "the edit";
@@ -14902,7 +15528,7 @@ async function saveHandEdit(loaded, lock, spec, input) {
14902
15528
  editor: input.editor,
14903
15529
  protocol: input.protocol,
14904
15530
  savedAt: (input.now ?? /* @__PURE__ */ new Date()).toISOString(),
14905
- project: projectPath ? path25.basename(projectPath) : null,
15531
+ project: projectPath ? path26.basename(projectPath) : null,
14906
15532
  outputs: incoming.map((frame, index) => ({
14907
15533
  role: frame.role,
14908
15534
  basedOn: basedOnHash(entry, index),
@@ -14932,7 +15558,7 @@ function basedOnHash(entry, index) {
14932
15558
  async function replaceFile(file, bytes) {
14933
15559
  const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
14934
15560
  try {
14935
- await writeFile11(tmp, bytes);
15561
+ await writeFile12(tmp, bytes);
14936
15562
  await rename8(tmp, file);
14937
15563
  } finally {
14938
15564
  await rm8(tmp, { force: true });
@@ -14962,7 +15588,7 @@ async function runEdit(args) {
14962
15588
  const set = started.members.length > 1;
14963
15589
  const kind = spec.generator === "frames" ? "frames" : "members";
14964
15590
  log(` ${started.created ? "created" : "found"} ${started.source}${set ? ` (${started.members.length} ${kind})` : ""}${started.declared ? " and declared it as the asset's source" : ""}`);
14965
- 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)}`);
14966
15592
  if (args.noOpen) {
14967
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`);
14968
15594
  } else {
@@ -14972,24 +15598,24 @@ async function runEdit(args) {
14972
15598
  }
14973
15599
 
14974
15600
  // src/cli/commands/gallery.ts
14975
- import path36 from "path";
14976
- import { existsSync as existsSync27 } from "fs";
15601
+ import path37 from "path";
15602
+ import { existsSync as existsSync28 } from "fs";
14977
15603
 
14978
15604
  // src/gallery/snapshot.ts
14979
15605
  import { createHash as createHash4 } from "crypto";
14980
- import { existsSync as existsSync22 } from "fs";
14981
- import { readFile as readFile20, stat as stat2 } from "fs/promises";
14982
- 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";
14983
15609
 
14984
15610
  // src/gallery/edit.ts
14985
- import { readFile as readFile19, rename as rename9, writeFile as writeFile15 } from "fs/promises";
14986
- import path30 from "path";
15611
+ import { readFile as readFile20, rename as rename9, writeFile as writeFile16 } from "fs/promises";
15612
+ import path31 from "path";
14987
15613
  import { z as z11 } from "zod";
14988
15614
 
14989
15615
  // src/gallery/upload.ts
14990
- import { existsSync as existsSync18 } from "fs";
14991
- import { mkdir as mkdir10, writeFile as writeFile12 } from "fs/promises";
14992
- 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";
14993
15619
  import { z as z8 } from "zod";
14994
15620
  var MAX_UPLOAD_BYTES = 8 * 1024 * 1024;
14995
15621
  var UploadImageSchema = z8.object({
@@ -15002,12 +15628,12 @@ var UploadImageSchema = z8.object({
15002
15628
  overwrite: z8.boolean().optional()
15003
15629
  }).strict();
15004
15630
  async function saveProjectImage(root, request) {
15005
- const file = path27.resolve(root, request.path);
15006
- const relative = path27.relative(root, file);
15007
- 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)) {
15008
15634
  throw new ManifestEditError("an upload must stay inside the project");
15009
15635
  }
15010
- const extension = path27.extname(file).toLowerCase();
15636
+ const extension = path28.extname(file).toLowerCase();
15011
15637
  const expected = extension === ".png" ? "png" : extension === ".jpg" || extension === ".jpeg" ? "jpeg" : null;
15012
15638
  if (!expected) throw new ManifestEditError("an upload must be a .png, .jpg, or .jpeg path");
15013
15639
  const bytes = Buffer.from(request.base64, "base64");
@@ -15019,18 +15645,18 @@ async function saveProjectImage(root, request) {
15019
15645
  if (metadata.format !== expected) {
15020
15646
  throw new ManifestEditError(`the upload is a ${metadata.format.toUpperCase()}; name it ${metadata.format === "png" ? ".png" : ".jpg"}`);
15021
15647
  }
15022
- if (existsSync18(file) && !request.overwrite) {
15023
- 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`);
15024
15650
  }
15025
- await mkdir10(path27.dirname(file), { recursive: true });
15026
- await writeFile12(file, bytes);
15027
- 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("/");
15028
15654
  }
15029
15655
 
15030
15656
  // src/gallery/image-revision.ts
15031
- import { existsSync as existsSync19 } from "fs";
15032
- import { readFile as readFile17, rm as rm9, writeFile as writeFile13 } from "fs/promises";
15033
- 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";
15034
15660
  import { z as z9 } from "zod";
15035
15661
  var InpaintRevisionRequestSchema = z9.object({
15036
15662
  action: z9.literal("create-inpaint-revision"),
@@ -15049,15 +15675,15 @@ var InpaintRevisionRequestSchema = z9.object({
15049
15675
  overwrite: z9.boolean().optional()
15050
15676
  }).strict();
15051
15677
  async function createInpaintRevision(manifestPath, request) {
15052
- const root = path28.dirname(path28.resolve(manifestPath));
15053
- if (path28.extname(request.maskPath).toLowerCase() !== ".png") {
15678
+ const root = path29.dirname(path29.resolve(manifestPath));
15679
+ if (path29.extname(request.maskPath).toLowerCase() !== ".png") {
15054
15680
  throw new ManifestEditError("the mask must be a .png path inside the project");
15055
15681
  }
15056
15682
  const bytes = Buffer.from(request.maskBase64, "base64");
15057
15683
  const metadata = imageMetadata(bytes);
15058
15684
  if (!metadata || metadata.format !== "png") throw new ManifestEditError("the mask is not a readable PNG");
15059
- const file = path28.resolve(root, request.maskPath);
15060
- 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;
15061
15687
  const mask = await saveProjectImage(root, {
15062
15688
  action: "upload-image",
15063
15689
  path: request.maskPath,
@@ -15076,7 +15702,7 @@ async function createInpaintRevision(manifestPath, request) {
15076
15702
  }
15077
15703
  });
15078
15704
  } catch (error) {
15079
- if (previous) await writeFile13(file, previous);
15705
+ if (previous) await writeFile14(file, previous);
15080
15706
  else await rm9(file, { force: true });
15081
15707
  throw error;
15082
15708
  }
@@ -15084,9 +15710,9 @@ async function createInpaintRevision(manifestPath, request) {
15084
15710
  }
15085
15711
 
15086
15712
  // src/gallery/skeleton.ts
15087
- import { existsSync as existsSync20 } from "fs";
15088
- import { mkdir as mkdir11, readFile as readFile18, rm as rm10, writeFile as writeFile14 } from "fs/promises";
15089
- 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";
15090
15716
  import { z as z10 } from "zod";
15091
15717
  var SkeletonEstimateRequestSchema = z10.object({
15092
15718
  project: z10.string().min(1).optional(),
@@ -15125,8 +15751,8 @@ var SkeletonKeypointsUpdateSchema = z10.object({
15125
15751
  }).strict();
15126
15752
  var DEFAULT_ESTIMATE_LIMIT = 10;
15127
15753
  function skeletonSourceFile(spec) {
15128
- if (spec.source) return path29.resolve(spec.root, spec.source);
15129
- 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;
15130
15756
  return spec.outFile;
15131
15757
  }
15132
15758
  function createGallerySkeletonHandlers(opts) {
@@ -15147,10 +15773,10 @@ function createGallerySkeletonHandlers(opts) {
15147
15773
  const spec = ctx.specs.find((candidate) => lockKey(candidate.styleId, candidate.assetId) === key);
15148
15774
  if (!spec) throw new ManifestEditError(`"${key}" is not declared by the manifest`);
15149
15775
  const file = skeletonSourceFile(spec);
15150
- if (!existsSync20(file)) throw new ManifestEditError(`${key} has no image on disk to estimate from`);
15151
- 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);
15152
15778
  const metadata = imageMetadata(bytes);
15153
- 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`);
15154
15780
  const problem = estimateSkeletonSizeProblem(metadata.width, metadata.height);
15155
15781
  if (problem) throw new ManifestEditError(`${key} is ${problem}`);
15156
15782
  if (used >= limit) {
@@ -15167,7 +15793,7 @@ function createGallerySkeletonHandlers(opts) {
15167
15793
  log2(` estimated a skeleton for ${key} (PixelLab estimate-skeleton, ${used}/${limit} this session${billed ? `, billed ${billed.amount} ${billed.unit}` : ""})`);
15168
15794
  return {
15169
15795
  set: scaffoldSkeletonSet(res.keypoints, request.frames ?? DEFAULT_SCAFFOLD_FRAMES),
15170
- source: path29.relative(ctx.loaded.root, file).split(path29.sep).join("/"),
15796
+ source: path30.relative(ctx.loaded.root, file).split(path30.sep).join("/"),
15171
15797
  session: status()
15172
15798
  };
15173
15799
  }
@@ -15181,21 +15807,21 @@ function billedAmount(usage) {
15181
15807
  return typeof amount === "number" && Number.isFinite(amount) ? { unit: type, amount } : null;
15182
15808
  }
15183
15809
  async function createSkeletonAnimation(manifestPath, request) {
15184
- const root = path29.dirname(path29.resolve(manifestPath));
15185
- const file = path29.resolve(root, request.keypointsFile);
15186
- const relative = path29.relative(root, file);
15187
- 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)) {
15188
15814
  throw new ManifestEditError("the keypoints file must be a .json path inside the project");
15189
15815
  }
15190
- const existed = existsSync20(file);
15816
+ const existed = existsSync21(file);
15191
15817
  let previous = null;
15192
15818
  if (request.set) {
15193
15819
  if (existed && !request.overwrite) {
15194
15820
  throw new ManifestEditError(`${request.keypointsFile} already exists; choose another path, or replace it`);
15195
15821
  }
15196
- if (existed) previous = await readFile18(file);
15197
- await mkdir11(path29.dirname(file), { recursive: true });
15198
- 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");
15199
15825
  } else if (!existed) {
15200
15826
  throw new ManifestEditError(`${request.keypointsFile} does not exist; paste poses or estimate them from the sprite`);
15201
15827
  }
@@ -15210,7 +15836,7 @@ async function createSkeletonAnimation(manifestPath, request) {
15210
15836
  revision: {
15211
15837
  mode: "animate-skeleton",
15212
15838
  from: request.from,
15213
- keypointsFile: relative.split(path29.sep).join("/"),
15839
+ keypointsFile: relative.split(path30.sep).join("/"),
15214
15840
  direction: request.direction,
15215
15841
  ...request.description ? { description: request.description } : {},
15216
15842
  ...request.skeletonTemplate ? { skeletonTemplate: request.skeletonTemplate } : {}
@@ -15219,7 +15845,7 @@ async function createSkeletonAnimation(manifestPath, request) {
15219
15845
  });
15220
15846
  } catch (error) {
15221
15847
  if (request.set) {
15222
- if (previous) await writeFile14(file, previous);
15848
+ if (previous) await writeFile15(file, previous);
15223
15849
  else await rm10(file, { force: true });
15224
15850
  }
15225
15851
  throw error;
@@ -15231,19 +15857,19 @@ async function updateSkeletonKeypoints(ctx, request) {
15231
15857
  if (!spec) throw new ManifestEditError(`"${key}" is not declared by the manifest`);
15232
15858
  const file = spec.revision?.mode === "animate-skeleton" ? spec.revision.keypointsFile : void 0;
15233
15859
  if (!file) throw new ManifestEditError(`${key} is not an animate-skeleton revision`);
15234
- const relative = path29.relative(ctx.loaded.root, file);
15235
- if (relative.startsWith("..") || path29.isAbsolute(relative)) {
15860
+ const relative = path30.relative(ctx.loaded.root, file);
15861
+ if (relative.startsWith("..") || path30.isAbsolute(relative)) {
15236
15862
  throw new ManifestEditError(`${key}'s keypoints file is outside the project; edit it by hand`);
15237
15863
  }
15238
- const current = existsSync20(file) ? sha256(await readFile18(file)) : null;
15864
+ const current = existsSync21(file) ? sha256(await readFile19(file)) : null;
15239
15865
  if (current !== request.expectedSha256) {
15240
15866
  throw Object.assign(
15241
- 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`),
15242
15868
  { status: 409 }
15243
15869
  );
15244
15870
  }
15245
- await writeFile14(file, JSON.stringify(request.set, null, 2) + "\n");
15246
- return relative.split(path29.sep).join("/");
15871
+ await writeFile15(file, JSON.stringify(request.set, null, 2) + "\n");
15872
+ return relative.split(path30.sep).join("/");
15247
15873
  }
15248
15874
 
15249
15875
  // src/gallery/edit.ts
@@ -15297,7 +15923,7 @@ function createGalleryEditHandler(opts) {
15297
15923
  const history = /* @__PURE__ */ new Map();
15298
15924
  const manifestOf = async (project) => {
15299
15925
  try {
15300
- 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));
15301
15927
  } catch {
15302
15928
  return null;
15303
15929
  }
@@ -15307,7 +15933,7 @@ function createGalleryEditHandler(opts) {
15307
15933
  if (undo.success) {
15308
15934
  const manifestPath2 = await manifestOf(undo.data.project);
15309
15935
  if (!manifestPath2) throw new ManifestEditError("no manifest to undo an edit in");
15310
- const current = await readFile19(manifestPath2, "utf8");
15936
+ const current = await readFile20(manifestPath2, "utf8");
15311
15937
  if (sha256(current) !== undo.data.expectedSha256) throw new ManifestDriftError(manifestPath2);
15312
15938
  const stack = history.get(manifestPath2) ?? [];
15313
15939
  const last = stack[stack.length - 1];
@@ -15317,17 +15943,17 @@ function createGalleryEditHandler(opts) {
15317
15943
  throw Object.assign(new Error("the manifest changed outside the gallery since its last edit; undo would discard that change"), { status: 409 });
15318
15944
  }
15319
15945
  const tmp = manifestPath2 + ".pixelkiln-undo";
15320
- await writeFile15(tmp, last.before);
15946
+ await writeFile16(tmp, last.before);
15321
15947
  await rename9(tmp, manifestPath2);
15322
15948
  stack.pop();
15323
- log2(` manifest edit undone in ${path30.relative(process.cwd(), manifestPath2) || manifestPath2}`);
15949
+ log2(` manifest edit undone in ${path31.relative(process.cwd(), manifestPath2) || manifestPath2}`);
15324
15950
  return opts.reload();
15325
15951
  }
15326
15952
  const manifestPath = await manifestOf(body?.project);
15327
- const before = manifestPath ? await readFile19(manifestPath, "utf8").catch(() => null) : null;
15953
+ const before = manifestPath ? await readFile20(manifestPath, "utf8").catch(() => null) : null;
15328
15954
  const build = await apply(body);
15329
15955
  if (manifestPath && before !== null) {
15330
- const after = await readFile19(manifestPath, "utf8").catch(() => null);
15956
+ const after = await readFile20(manifestPath, "utf8").catch(() => null);
15331
15957
  if (after !== null && after !== before) {
15332
15958
  const stack = history.get(manifestPath) ?? [];
15333
15959
  stack.push({ before, afterSha: sha256(after) });
@@ -15382,7 +16008,7 @@ function createEditApplier(opts, log2) {
15382
16008
  } catch (error) {
15383
16009
  throw new ManifestEditError(error instanceof Error ? error.message : String(error));
15384
16010
  }
15385
- const written = await saveProjectImage(path30.dirname(path30.resolve(manifestPath2)), upload.data);
16011
+ const written = await saveProjectImage(path31.dirname(path31.resolve(manifestPath2)), upload.data);
15386
16012
  log2(` image added: ${written}`);
15387
16013
  return opts.reload();
15388
16014
  }
@@ -15449,7 +16075,7 @@ function createEditApplier(opts, log2) {
15449
16075
  }
15450
16076
  const result = await applyManifestEdit(manifestPath, parsed.data);
15451
16077
  if (result.changed) {
15452
- const relative = path30.relative(process.cwd(), result.manifestPath);
16078
+ const relative = path31.relative(process.cwd(), result.manifestPath);
15453
16079
  const shown = !relative || relative.startsWith("..") ? result.manifestPath : relative;
15454
16080
  log2(` manifest edited: ${describeEdit(parsed.data)} in ${shown}`);
15455
16081
  }
@@ -15500,8 +16126,8 @@ function createGalleryPriceHandler(opts) {
15500
16126
  }
15501
16127
 
15502
16128
  // src/pipeline/history.ts
15503
- import { existsSync as existsSync21 } from "fs";
15504
- import path31 from "path";
16129
+ import { existsSync as existsSync22 } from "fs";
16130
+ import path32 from "path";
15505
16131
  var HISTORY_ENV = "PIXELKILN_HISTORY";
15506
16132
  function historyLimit(manifest, env = process.env) {
15507
16133
  if (manifest?.history !== void 0) return manifest.history;
@@ -15588,10 +16214,10 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
15588
16214
  }
15589
16215
  for (let i = 0; i < entry.outputs.length; i++) {
15590
16216
  const file = currentEntryOutputPath(entry, spec, i);
15591
- if (!existsSync21(file)) continue;
16217
+ if (!existsSync22(file)) continue;
15592
16218
  if (await sha256File(file) !== entry.outputs[i].sha256 && !opts.force) {
15593
16219
  throw new OverwriteRefusedError(
15594
- `${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`,
15595
16221
  [file]
15596
16222
  );
15597
16223
  }
@@ -15644,15 +16270,20 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
15644
16270
  // src/gallery/snapshot.ts
15645
16271
  var PROJECT_FILE_TYPE = "application/octet-stream";
15646
16272
  function galleryMediaId(absolutePath) {
15647
- 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);
15648
16274
  }
15649
16275
  function galleryMediaRoute(id) {
15650
16276
  return `/media/${id}`;
15651
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
+ }
15652
16282
  function isDeclared(loaded, styleId, assetId) {
15653
16283
  const asset = loaded.manifest.assets[assetId];
15654
- if (!asset || !loaded.manifest.styles[styleId]) return false;
15655
- 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);
15656
16287
  }
15657
16288
  function matchesFilter(filter, styleId, assetId) {
15658
16289
  if (filter?.styles?.length && !filter.styles.includes(styleId)) return false;
@@ -15663,9 +16294,9 @@ function describeHistory(media, entry, cacheDir) {
15663
16294
  return (entry.history ?? []).map((generation, i) => {
15664
16295
  const outputs = generation.outputs.map((output) => {
15665
16296
  const mediaType = output.mediaType ?? MediaType.PNG;
15666
- const file = path32.join(cacheDir, cacheFileName(output.sha256, mediaType));
16297
+ const file = path33.join(cacheDir, cacheFileName(output.sha256, mediaType));
15667
16298
  let url = null;
15668
- if (existsSync22(file)) {
16299
+ if (existsSync23(file)) {
15669
16300
  const id = galleryMediaId(file);
15670
16301
  media.set(id, { path: file, contentType: mediaType });
15671
16302
  url = `${galleryMediaRoute(id)}?v=${output.sha256.slice(0, 24)}`;
@@ -15697,7 +16328,7 @@ function describeHistory(media, entry, cacheDir) {
15697
16328
  var MAX_COMPARED_BYTES = 8 * 1024 * 1024;
15698
16329
  async function samePixels(a, b) {
15699
16330
  try {
15700
- const [left, right] = await Promise.all([readFile20(a), readFile20(b)]);
16331
+ const [left, right] = await Promise.all([readFile21(a), readFile21(b)]);
15701
16332
  if (left.length > MAX_COMPARED_BYTES || right.length > MAX_COMPARED_BYTES) return false;
15702
16333
  const x = decodePng(left);
15703
16334
  const y = decodePng(right);
@@ -15795,10 +16426,10 @@ async function describeOutput(media, root, absolutePath, recorded = {}) {
15795
16426
  async function describeQuality(media, root, inspection) {
15796
16427
  const outputPaths = inspection.outputs ?? [inspection.output];
15797
16428
  const outputs = await Promise.all(
15798
- outputPaths.map((file) => describeOutput(media, root, path32.resolve(root, file)))
16429
+ outputPaths.map((file) => describeOutput(media, root, path33.resolve(root, file)))
15799
16430
  );
15800
- const recordPath = path32.resolve(root, inspection.record);
15801
- const recordExists = existsSync22(recordPath);
16431
+ const recordPath = path33.resolve(root, inspection.record);
16432
+ const recordExists = existsSync23(recordPath);
15802
16433
  let options = null;
15803
16434
  let check = null;
15804
16435
  if (recordExists) {
@@ -15817,7 +16448,7 @@ async function describeQuality(media, root, inspection) {
15817
16448
  return {
15818
16449
  state: inspection.state,
15819
16450
  reason: inspection.reason,
15820
- output: portableOutputPath(path32.resolve(root, inspection.output), root),
16451
+ output: portableOutputPath(path33.resolve(root, inspection.output), root),
15821
16452
  record: portableOutputPath(recordPath, root),
15822
16453
  recordExists,
15823
16454
  outputs,
@@ -15855,9 +16486,9 @@ async function buildGallerySnapshot(opts) {
15855
16486
  const media = /* @__PURE__ */ new Map();
15856
16487
  const plan = await buildPlan(specs, lock);
15857
16488
  const items = [];
15858
- const manifestText = await readFile20(loaded.path, "utf8");
16489
+ const manifestText = await readFile21(loaded.path, "utf8");
15859
16490
  const manifestSha256 = sha256(manifestText);
15860
- const cacheDir = path32.resolve(path32.dirname(opts.lockPath), ".pixelkiln", "cache");
16491
+ const cacheDir = path33.resolve(path33.dirname(opts.lockPath), ".pixelkiln", "cache");
15861
16492
  let rawStyles = {};
15862
16493
  try {
15863
16494
  const raw = JSON.parse(manifestText);
@@ -15881,12 +16512,12 @@ async function buildGallerySnapshot(opts) {
15881
16512
  )
15882
16513
  );
15883
16514
  if (spec.source) {
15884
- const editPath = path32.resolve(root, spec.source);
16515
+ const editPath = path33.resolve(root, spec.source);
15885
16516
  const memberCount = sourceIsStem(spec.source, entry, root) ? entry.outputs.length : 1;
15886
16517
  const editShas = await Promise.all(
15887
16518
  Array.from({ length: memberCount }, async (_, index) => {
15888
16519
  const file = sourceOutputPath(spec.source, entry, index, root);
15889
- return { file, sha256: existsSync22(file) ? await sha256File(file) : null };
16520
+ return { file, sha256: existsSync23(file) ? await sha256File(file) : null };
15890
16521
  })
15891
16522
  );
15892
16523
  edits = await Promise.all(editShas.map(({ file, sha256: editSha }) => describeOutput(media, root, file, { sha256: editSha })));
@@ -15894,7 +16525,7 @@ async function buildGallerySnapshot(opts) {
15894
16525
  const companion = await readHandEditCompanion(editPath);
15895
16526
  if (companion) {
15896
16527
  const projectPath = handEditProjectPath(editPath);
15897
- const project = companion.project && existsSync22(projectPath) ? await fileInfo(projectPath) : null;
16528
+ const project = companion.project && existsSync23(projectPath) ? await fileInfo(projectPath) : null;
15898
16529
  let projectUrl = null;
15899
16530
  if (project) {
15900
16531
  const id = galleryMediaId(projectPath);
@@ -15930,8 +16561,8 @@ async function buildGallerySnapshot(opts) {
15930
16561
  editStatus = edits.some((member) => !member.exists) ? "missing" : editChanged.every((changed) => !changed) ? "same" : regenerated ? "regenerated-since" : "edited";
15931
16562
  }
15932
16563
  } else if (spec.source) {
15933
- outputs = [await describeOutput(media, root, path32.resolve(root, spec.source))];
15934
- } else if (!entry && existsSync22(spec.outFile)) {
16564
+ outputs = [await describeOutput(media, root, path33.resolve(root, spec.source))];
16565
+ } else if (!entry && existsSync23(spec.outFile)) {
15935
16566
  outputs = [await describeOutput(media, root, spec.outFile)];
15936
16567
  } else {
15937
16568
  outputs = [];
@@ -15952,8 +16583,8 @@ async function buildGallerySnapshot(opts) {
15952
16583
  generator: entry?.generator ?? spec.generator,
15953
16584
  prompt: entry?.prompt ?? spec.prompt,
15954
16585
  currentPrompt: entry && entry.prompt !== spec.prompt ? spec.prompt : null,
15955
- width: entry?.width ?? spec.width,
15956
- height: entry?.height ?? spec.height,
16586
+ width: renderedSize(entry)?.width ?? entry?.width ?? spec.width,
16587
+ height: renderedSize(entry)?.height ?? entry?.height ?? spec.height,
15957
16588
  tileFeature: entry?.tileFeature ?? spec.tileFeature ?? null,
15958
16589
  fps: quality?.frameSet?.fps ?? metadataFps(entry) ?? spec.quality?.fps ?? null,
15959
16590
  cost: entry?.cost ?? 0,
@@ -16134,7 +16765,7 @@ async function buildGallerySnapshot(opts) {
16134
16765
  id: loaded.manifest.name,
16135
16766
  name: loaded.manifest.name,
16136
16767
  manifest: loaded.path,
16137
- lock: path32.resolve(opts.lockPath),
16768
+ lock: path33.resolve(opts.lockPath),
16138
16769
  root,
16139
16770
  provider: loaded.manifest.provider,
16140
16771
  account: null,
@@ -16160,7 +16791,7 @@ async function buildGallerySnapshot(opts) {
16160
16791
  return { snapshot, media };
16161
16792
  }
16162
16793
  async function buildWorkspaceGallerySnapshot(opts) {
16163
- const dir = path32.dirname(path32.resolve(opts.workspacePath));
16794
+ const dir = path33.dirname(path33.resolve(opts.workspacePath));
16164
16795
  const media = /* @__PURE__ */ new Map();
16165
16796
  const projects = [];
16166
16797
  const items = [];
@@ -16203,11 +16834,11 @@ async function buildWorkspaceGallerySnapshot(opts) {
16203
16834
  id: project.id,
16204
16835
  name: loaded.manifest.name,
16205
16836
  manifest: manifestPath,
16206
- lock: path32.resolve(lockPath),
16837
+ lock: path33.resolve(lockPath),
16207
16838
  root: loaded.root,
16208
16839
  provider: loaded.manifest.provider,
16209
16840
  account: project.account ?? null,
16210
- manifestSha256: manifestSha256 ?? sha256(await readFile20(manifestPath, "utf8")),
16841
+ manifestSha256: manifestSha256 ?? sha256(await readFile21(manifestPath, "utf8")),
16211
16842
  historyLimit: historyLimit(loaded.manifest),
16212
16843
  entries: Object.keys(lock.entries).length,
16213
16844
  items: projectItems.length,
@@ -16219,8 +16850,8 @@ async function buildWorkspaceGallerySnapshot(opts) {
16219
16850
  id: project.id,
16220
16851
  name: project.id,
16221
16852
  manifest: manifestPath,
16222
- lock: path32.resolve(lockPath),
16223
- root: path32.dirname(manifestPath),
16853
+ lock: path33.resolve(lockPath),
16854
+ root: path33.dirname(manifestPath),
16224
16855
  provider: project.provider,
16225
16856
  account: project.account ?? null,
16226
16857
  manifestSha256: null,
@@ -16247,7 +16878,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
16247
16878
  version: 1,
16248
16879
  generatedAt: (opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
16249
16880
  project: null,
16250
- workspace: { path: path32.resolve(opts.workspacePath), dir, projects },
16881
+ workspace: { path: path33.resolve(opts.workspacePath), dir, projects },
16251
16882
  filter,
16252
16883
  totals: {
16253
16884
  entries: projects.reduce((sum, project) => sum + project.entries, 0),
@@ -16266,10 +16897,10 @@ async function buildWorkspaceGallerySnapshot(opts) {
16266
16897
  // src/gallery/server.ts
16267
16898
  import { randomBytes } from "crypto";
16268
16899
  import { createServer as createServer2 } from "http";
16269
- import { readFile as readFile21 } from "fs/promises";
16900
+ import { readFile as readFile22 } from "fs/promises";
16270
16901
 
16271
16902
  // src/gallery/page.ts
16272
- import { existsSync as existsSync23, readFileSync as readFileSync5 } from "fs";
16903
+ import { existsSync as existsSync24, readFileSync as readFileSync5 } from "fs";
16273
16904
 
16274
16905
  // src/gallery/client-bundle.ts
16275
16906
  import { createRequire } from "module";
@@ -16310,7 +16941,7 @@ function clientAssets() {
16310
16941
  const prebuilt = new URL("gallery.js", CLIENT_DIR);
16311
16942
  client ??= {
16312
16943
  css: readFileSync5(new URL("gallery.css", CLIENT_DIR), "utf8"),
16313
- js: existsSync23(prebuilt) ? readFileSync5(prebuilt, "utf8") : bundleGalleryClient()
16944
+ js: existsSync24(prebuilt) ? readFileSync5(prebuilt, "utf8") : bundleGalleryClient()
16314
16945
  };
16315
16946
  return client;
16316
16947
  }
@@ -16581,7 +17212,7 @@ async function serveGallery(opts) {
16581
17212
  const asset = reviewAssets.get(jobId)?.get(url2.pathname);
16582
17213
  if (!asset) return fail(404, "no such review asset");
16583
17214
  try {
16584
- const bytes = await readFile21(asset.path);
17215
+ const bytes = await readFile22(asset.path);
16585
17216
  res.writeHead(200, {
16586
17217
  "Content-Type": asset.contentType,
16587
17218
  "Cache-Control": "no-store",
@@ -16664,7 +17295,7 @@ async function serveGallery(opts) {
16664
17295
  if (!asset) return fail(404, "no such gallery media");
16665
17296
  let bytes;
16666
17297
  try {
16667
- bytes = await readFile21(asset.path);
17298
+ bytes = await readFile22(asset.path);
16668
17299
  } catch {
16669
17300
  return fail(404, "gallery media is no longer on disk; refresh the page");
16670
17301
  }
@@ -17037,6 +17668,8 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
17037
17668
  const anchor = spec.character?.styleAnchor;
17038
17669
  const anchorEntry = anchor ? lock.entries[lockKey(anchor.spec.styleId, anchor.spec.assetId)] : void 0;
17039
17670
  const replacedMetadata = previousEntry?.providerMetadata?.[provider.id];
17671
+ const lastGeneration = previousEntry?.status === "downloaded" ? previousEntry : previousEntry?.history?.[0];
17672
+ const lastGenerationMetadata = lastGeneration?.providerMetadata?.[provider.id];
17040
17673
  const parentOutputs = spec.character?.kind === "outfit" && parentEntry ? parentEntry.outputs.map((output, index) => ({
17041
17674
  path: currentEntryOutputPath(parentEntry, spec.character.parentSpec, index),
17042
17675
  role: output.role
@@ -17049,6 +17682,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
17049
17682
  ...anchorEntry?.objectId ? { styleObjectId: anchorEntry.objectId } : {},
17050
17683
  ...parentOutputs ? { parentOutputs } : {},
17051
17684
  ...replacedMetadata ? { replacedMetadata } : {},
17685
+ ...lastGenerationMetadata ? { lastGenerationMetadata } : {},
17052
17686
  ...previousMetadata ? { previousMetadata } : {},
17053
17687
  checkpoint: async (checkpoint) => {
17054
17688
  if (!checkpoint.jobId) throw new Error("Provider checkpoint returned an empty job id");
@@ -17124,13 +17758,13 @@ function ownedReferenceImageId(spec, lock, providerId) {
17124
17758
  }
17125
17759
 
17126
17760
  // src/pick/server.ts
17127
- import { existsSync as existsSync25 } from "fs";
17128
- import { readFile as readFile23 } from "fs/promises";
17761
+ import { existsSync as existsSync26 } from "fs";
17762
+ import { readFile as readFile24 } from "fs/promises";
17129
17763
 
17130
17764
  // src/pipeline/init.ts
17131
- import { readdir, readFile as readFile22, writeFile as writeFile16 } from "fs/promises";
17132
- import { existsSync as existsSync24 } from "fs";
17133
- 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";
17134
17768
  function pngSize(buf) {
17135
17769
  if (buf.length < 24) return null;
17136
17770
  const isPng = buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834;
@@ -17144,7 +17778,7 @@ function slugify(value) {
17144
17778
  async function walk(dir, exclude) {
17145
17779
  const out = [];
17146
17780
  for (const entry of await readdir(dir, { withFileTypes: true })) {
17147
- const full = path33.join(dir, entry.name);
17781
+ const full = path34.join(dir, entry.name);
17148
17782
  if (exclude.some((x) => full.includes(x))) continue;
17149
17783
  if (entry.isDirectory()) out.push(...await walk(full, exclude));
17150
17784
  else if (entry.name.toLowerCase().endsWith(".png")) out.push(full);
@@ -17158,14 +17792,14 @@ async function scanAssets(root, opts = {}) {
17158
17792
  const skipped = [];
17159
17793
  const seen = /* @__PURE__ */ new Map();
17160
17794
  for (const file of files.sort()) {
17161
- const size = pngSize(await readFile22(file));
17795
+ const size = pngSize(await readFile23(file));
17162
17796
  if (!size) {
17163
17797
  skipped.push(`${file} (not a readable PNG)`);
17164
17798
  continue;
17165
17799
  }
17166
- const rel = path33.relative(root, file);
17167
- const category = path33.dirname(rel) === "." ? "" : path33.dirname(rel);
17168
- 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));
17169
17803
  if (seen.has(id)) {
17170
17804
  const n = seen.get(id) + 1;
17171
17805
  seen.set(id, n);
@@ -17223,10 +17857,10 @@ function mode(values) {
17223
17857
  return [...counts.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? 64;
17224
17858
  }
17225
17859
  async function writeManifestFile(target, manifest) {
17226
- if (existsSync24(target)) {
17860
+ if (existsSync25(target)) {
17227
17861
  throw new Error(`${target} already exists; refusing to overwrite an existing manifest.`);
17228
17862
  }
17229
- await writeFile16(target, JSON.stringify(manifest, null, 2) + "\n");
17863
+ await writeFile17(target, JSON.stringify(manifest, null, 2) + "\n");
17230
17864
  }
17231
17865
 
17232
17866
  // src/pick/sheet.ts
@@ -17639,13 +18273,13 @@ async function currentArt(entry, spec) {
17639
18273
  const previous = primaryOutput({ ...entry, outputs: entry.supersededOutputs });
17640
18274
  if (!previous) return null;
17641
18275
  const file = resolveOutputPath(previous.path, spec.root);
17642
- if (!existsSync25(file)) return null;
18276
+ if (!existsSync26(file)) return null;
17643
18277
  const contentType = previous.mediaType ?? mediaTypeFromExtension(file);
17644
18278
  if (!contentType) return null;
17645
18279
  let width = entry.width;
17646
18280
  let height = entry.height;
17647
18281
  if (contentType === "image/png") {
17648
- const size = pngSize(await readFile23(file));
18282
+ const size = pngSize(await readFile24(file));
17649
18283
  if (size) ({ width, height } = size);
17650
18284
  }
17651
18285
  return { path: file, contentType, width, height };
@@ -17653,7 +18287,7 @@ async function currentArt(entry, spec) {
17653
18287
  async function cachedFrame(url) {
17654
18288
  if (!url.startsWith("file://")) return null;
17655
18289
  const file = url.slice("file://".length);
17656
- 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";
17657
18291
  return { path: file, contentType };
17658
18292
  }
17659
18293
  async function prepareReview(provider, lock, opts = {}) {
@@ -18199,15 +18833,15 @@ function createGenerateHandlers(opts) {
18199
18833
  }
18200
18834
 
18201
18835
  // src/gallery/editor.ts
18202
- import { readFile as readFile25 } from "fs/promises";
18203
- import path35 from "path";
18836
+ import { readFile as readFile26 } from "fs/promises";
18837
+ import path36 from "path";
18204
18838
 
18205
18839
  // src/editor/install.ts
18206
18840
  import { createHash as createHash5 } from "crypto";
18207
- import { existsSync as existsSync26 } from "fs";
18208
- 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";
18209
18843
  import os3 from "os";
18210
- import path34 from "path";
18844
+ import path35 from "path";
18211
18845
 
18212
18846
  // tools/pixelorama-bridge/pin.json
18213
18847
  var pin_default = {
@@ -18271,15 +18905,15 @@ var EditorInstallError = class extends Error {
18271
18905
  };
18272
18906
  function editorToolsRoot() {
18273
18907
  const override = process.env.PIXELKILN_TOOLS_DIR?.trim();
18274
- if (override) return path34.resolve(override);
18275
- 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");
18276
18910
  if (process.platform === "win32") {
18277
- 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");
18278
18912
  }
18279
- 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");
18280
18914
  }
18281
18915
  function editorDir(pin = EDITOR_PIN, root = editorToolsRoot()) {
18282
- return path34.join(root, "pixelorama", pin.release ?? "unpublished");
18916
+ return path35.join(root, "pixelorama", pin.release ?? "unpublished");
18283
18917
  }
18284
18918
  function editorBaseUrl(pin = EDITOR_PIN) {
18285
18919
  const override = process.env.PIXELKILN_EDITOR_URL?.trim();
@@ -18289,7 +18923,7 @@ function editorBaseUrl(pin = EDITOR_PIN) {
18289
18923
  }
18290
18924
  async function sha256Of(file) {
18291
18925
  try {
18292
- return createHash5("sha256").update(await readFile24(file)).digest("hex");
18926
+ return createHash5("sha256").update(await readFile25(file)).digest("hex");
18293
18927
  } catch {
18294
18928
  return null;
18295
18929
  }
@@ -18302,8 +18936,8 @@ async function editorStatus(opts = {}) {
18302
18936
  let totalBytes = 0;
18303
18937
  for (const [name, expected] of Object.entries(pin.files)) {
18304
18938
  totalBytes += expected.bytes;
18305
- const file = path34.join(dir, name);
18306
- 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;
18307
18941
  if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
18308
18942
  installedBytes += expected.bytes;
18309
18943
  } else {
@@ -18332,7 +18966,7 @@ async function installEditor(opts = {}) {
18332
18966
  );
18333
18967
  }
18334
18968
  if (!baseUrl) throw new EditorInstallError("no download location for the editor build");
18335
- await mkdir12(dir, { recursive: true });
18969
+ await mkdir13(dir, { recursive: true });
18336
18970
  const downloaded = [];
18337
18971
  const skipped = Object.keys(pin.files).filter((name) => !before.missing.includes(name));
18338
18972
  let fetchedBytes = before.installedBytes;
@@ -18360,10 +18994,10 @@ async function installEditor(opts = {}) {
18360
18994
  if (digest2 !== expected.sha256) {
18361
18995
  throw new EditorInstallError(`${name}: sha256 ${digest2.slice(0, 12)}\u2026 does not match the pinned ${expected.sha256.slice(0, 12)}\u2026`, name);
18362
18996
  }
18363
- const final = path34.join(dir, name);
18997
+ const final = path35.join(dir, name);
18364
18998
  const tmp = `${final}.${process.pid}.part`;
18365
18999
  try {
18366
- await writeFile17(tmp, bytes);
19000
+ await writeFile18(tmp, bytes);
18367
19001
  await rename10(tmp, final);
18368
19002
  } finally {
18369
19003
  await rm11(tmp, { force: true });
@@ -18406,7 +19040,7 @@ var EDITOR_CONTENT_TYPES = {
18406
19040
  ".json": "application/json; charset=utf-8"
18407
19041
  };
18408
19042
  function editorContentType(name) {
18409
- return EDITOR_CONTENT_TYPES[path34.extname(name).toLowerCase()] ?? null;
19043
+ return EDITOR_CONTENT_TYPES[path35.extname(name).toLowerCase()] ?? null;
18410
19044
  }
18411
19045
 
18412
19046
  // src/gallery/editor.ts
@@ -18482,7 +19116,7 @@ function createGalleryEditorHandlers(opts = {}) {
18482
19116
  if (!status.installed) return null;
18483
19117
  let bytes;
18484
19118
  try {
18485
- bytes = await readFile25(path35.join(dir, name));
19119
+ bytes = await readFile26(path36.join(dir, name));
18486
19120
  } catch {
18487
19121
  verified = null;
18488
19122
  return null;
@@ -18522,7 +19156,7 @@ async function serveUntilStopped(initial, reload, args, access2) {
18522
19156
  };
18523
19157
  const loadProject = async (project) => {
18524
19158
  const ctx = await access2.loadProject(project);
18525
- loadProjectEnv(path36.dirname(ctx.loaded.path));
19159
+ loadProjectEnv(path37.dirname(ctx.loaded.path));
18526
19160
  return ctx;
18527
19161
  };
18528
19162
  const loadProjectEnv = (dir) => {
@@ -18531,7 +19165,7 @@ async function serveUntilStopped(initial, reload, args, access2) {
18531
19165
  for (const [name, value] of Object.entries(declared)) {
18532
19166
  if (credentialNames.has(name) && name in process.env && process.env[name] !== value) {
18533
19167
  throw new Error(
18534
- `${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.`
18535
19169
  );
18536
19170
  }
18537
19171
  }
@@ -18539,7 +19173,7 @@ async function serveUntilStopped(initial, reload, args, access2) {
18539
19173
  };
18540
19174
  const quoters = /* @__PURE__ */ new Map();
18541
19175
  const quoteFor = async (project) => {
18542
- loadProjectEnv(path36.dirname(path36.resolve(await access2.manifestFor(project))));
19176
+ loadProjectEnv(path37.dirname(path37.resolve(await access2.manifestFor(project))));
18543
19177
  const key = process.env.PIXELLAB_API_KEY;
18544
19178
  if (!key) return null;
18545
19179
  let quoter = quoters.get(key);
@@ -18590,8 +19224,8 @@ async function serveUntilStopped(initial, reload, args, access2) {
18590
19224
  }
18591
19225
  async function runGallery(args) {
18592
19226
  if (args.workspace) {
18593
- const workspacePath = path36.resolve(args.workspace);
18594
- if (!existsSync27(workspacePath)) {
19227
+ const workspacePath = path37.resolve(args.workspace);
19228
+ if (!existsSync28(workspacePath)) {
18595
19229
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
18596
19230
  }
18597
19231
  const filter2 = { styles: args.styles, assets: args.assets };
@@ -18611,7 +19245,7 @@ async function runGallery(args) {
18611
19245
  const registered = async (projectId) => {
18612
19246
  const project = (await loadWorkspace(workspacePath)).projects.find((candidate) => candidate.id === projectId);
18613
19247
  if (!projectId || !project) throw new Error(`unknown workspace project "${projectId ?? ""}"`);
18614
- return resolveProject(path36.dirname(workspacePath), project);
19248
+ return resolveProject(path37.dirname(workspacePath), project);
18615
19249
  };
18616
19250
  await serveUntilStopped(initial, build2, args, {
18617
19251
  manifestFor: async (projectId) => (await registered(projectId)).manifestPath,
@@ -18636,7 +19270,7 @@ async function runGallery(args) {
18636
19270
  return buildGallerySnapshot({ ...fresh, filter });
18637
19271
  };
18638
19272
  await serveUntilStopped(await build(), reload, args, {
18639
- manifestFor: () => path36.resolve(args.manifest),
19273
+ manifestFor: () => path37.resolve(args.manifest),
18640
19274
  loadProject: () => openProject(args.manifest, { lockPath: args.lock, env: false })
18641
19275
  });
18642
19276
  }
@@ -18879,7 +19513,7 @@ async function runLifecycle(args, wave, spent) {
18879
19513
  log(` Packaging stays blocked until each refined PNG has a current human approval.`);
18880
19514
  }
18881
19515
  }
18882
- 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);
18883
19517
  }
18884
19518
  return false;
18885
19519
  }
@@ -18945,18 +19579,18 @@ async function runRestoreGeneration(args) {
18945
19579
  }
18946
19580
 
18947
19581
  // src/cli/commands/housekeeping.ts
18948
- import { existsSync as existsSync29 } from "fs";
19582
+ import { existsSync as existsSync30 } from "fs";
18949
19583
 
18950
19584
  // src/pipeline/cache-health.ts
18951
- import { existsSync as existsSync28 } from "fs";
18952
- import { readFile as readFile26, readdir as readdir2, rm as rm12 } from "fs/promises";
18953
- 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";
18954
19588
  async function inspectCaches(lock, lockPath, options = {}) {
18955
- if (options.prune && !existsSync28(lockPath)) {
18956
- 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)}`);
18957
19591
  }
18958
- const contentDir = path37.resolve(path37.dirname(lockPath), ".pixelkiln", "cache");
18959
- const remotePath = path37.resolve(cachePathFor(lockPath));
19592
+ const contentDir = path38.resolve(path38.dirname(lockPath), ".pixelkiln", "cache");
19593
+ const remotePath = path38.resolve(cachePathFor(lockPath));
18960
19594
  const referenced = referencedHashes(lock);
18961
19595
  let content = await inspectContentCache(contentDir, referenced);
18962
19596
  let remoteHashes = await inspectRemoteHashCache(remotePath);
@@ -18968,7 +19602,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
18968
19602
  ]);
18969
19603
  for (const name of names) {
18970
19604
  try {
18971
- await rm12(path37.join(contentDir, name), { force: true });
19605
+ await rm12(path38.join(contentDir, name), { force: true });
18972
19606
  removed.contentFiles++;
18973
19607
  } catch {
18974
19608
  }
@@ -18977,7 +19611,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
18977
19611
  await saveCache(remotePath, { version: 1, hashes: {} });
18978
19612
  removed.resetRemoteHashCache = true;
18979
19613
  } else if (remoteHashes.invalidIds.length) {
18980
- const cache = parseCache(JSON.parse(await readFile26(remotePath, "utf8")));
19614
+ const cache = parseCache(JSON.parse(await readFile27(remotePath, "utf8")));
18981
19615
  for (const id of remoteHashes.invalidIds) delete cache.hashes[id];
18982
19616
  removed.remoteHashEntries = remoteHashes.invalidIds.length;
18983
19617
  await saveCache(remotePath, cache);
@@ -18995,7 +19629,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
18995
19629
  async function inspectContentCache(contentDir, referenced) {
18996
19630
  const report = {
18997
19631
  path: contentDir,
18998
- exists: existsSync28(contentDir),
19632
+ exists: existsSync29(contentDir),
18999
19633
  files: 0,
19000
19634
  bytes: 0,
19001
19635
  valid: 0,
@@ -19016,12 +19650,12 @@ async function inspectContentCache(contentDir, referenced) {
19016
19650
  continue;
19017
19651
  }
19018
19652
  report.files++;
19019
- const file = path37.join(contentDir, entry.name);
19653
+ const file = path38.join(contentDir, entry.name);
19020
19654
  const mediaType = mediaTypeFromExtension(entry.name);
19021
19655
  const expected = mediaType ? entry.name.slice(0, -4) : "";
19022
19656
  let bytes;
19023
19657
  try {
19024
- bytes = await readFile26(file);
19658
+ bytes = await readFile27(file);
19025
19659
  report.bytes += bytes.length;
19026
19660
  } catch (err) {
19027
19661
  report.invalid.push({
@@ -19059,7 +19693,7 @@ async function inspectContentCache(contentDir, referenced) {
19059
19693
  async function inspectRemoteHashCache(remotePath) {
19060
19694
  const report = {
19061
19695
  path: remotePath,
19062
- exists: existsSync28(remotePath),
19696
+ exists: existsSync29(remotePath),
19063
19697
  entries: 0,
19064
19698
  valid: 0,
19065
19699
  invalidIds: [],
@@ -19068,7 +19702,7 @@ async function inspectRemoteHashCache(remotePath) {
19068
19702
  if (!report.exists) return report;
19069
19703
  let cache;
19070
19704
  try {
19071
- cache = parseCache(JSON.parse(await readFile26(remotePath, "utf8")));
19705
+ cache = parseCache(JSON.parse(await readFile27(remotePath, "utf8")));
19072
19706
  } catch (err) {
19073
19707
  report.error = err instanceof Error ? err.message : String(err);
19074
19708
  return report;
@@ -19132,7 +19766,7 @@ async function runAccept(args) {
19132
19766
  let intact = true;
19133
19767
  for (const output of entry.outputs) {
19134
19768
  const file = resolveOutputPath(output.path, item.spec.root);
19135
- if (!existsSync29(file) || await sha256File(file) !== output.sha256) {
19769
+ if (!existsSync30(file) || await sha256File(file) !== output.sha256) {
19136
19770
  intact = false;
19137
19771
  break;
19138
19772
  }
@@ -19187,42 +19821,42 @@ This drops their provenance from the lockfile. The art on disk is`);
19187
19821
  }
19188
19822
 
19189
19823
  // src/cli/commands/init.ts
19190
- import path38 from "path";
19191
- import { existsSync as existsSync30 } from "fs";
19824
+ import path39 from "path";
19825
+ import { existsSync as existsSync31 } from "fs";
19192
19826
  async function runInit(args) {
19193
19827
  if (!args.from) throw new Error("init needs --from <dir> pointing at your existing PNGs.");
19194
- const root = path38.resolve(args.from);
19195
- 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}`);
19196
19830
  const generator = args.generator ?? "map";
19197
19831
  if (generator !== "1dir" && generator !== "map") {
19198
19832
  throw new Error(`--generator must be "1dir" or "map", got "${args.generator}".`);
19199
19833
  }
19200
- const target = path38.resolve(args.out ?? "pixelkiln.manifest.json");
19834
+ const target = path39.resolve(args.out ?? "pixelkiln.manifest.json");
19201
19835
  const { assets, skipped } = await scanAssets(root, { exclude: args.exclude });
19202
19836
  if (!assets.length) throw new Error(`No PNGs found under ${root}`);
19203
19837
  const manifest = buildManifest(
19204
- args.name ?? path38.basename(path38.dirname(target)),
19838
+ args.name ?? path39.basename(path39.dirname(target)),
19205
19839
  args.styles[0] ?? "base",
19206
19840
  generator,
19207
- path38.relative(path38.dirname(target), root) || ".",
19841
+ path39.relative(path39.dirname(target), root) || ".",
19208
19842
  assets
19209
19843
  );
19210
19844
  await writeManifestFile(target, manifest);
19211
- 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)}`);
19212
19846
  if (skipped.length) log(` skipped ${skipped.length} unreadable file(s)`);
19213
- log(` wrote ${path38.relative(process.cwd(), target)}`);
19847
+ log(` wrote ${path39.relative(process.cwd(), target)}`);
19214
19848
  log(`
19215
19849
  Prompts are intentionally empty. To recover the real ones from your`);
19216
19850
  log(` PixelLab account instead of inventing them:`);
19217
19851
  log(`
19218
- pixelkiln adopt --manifest ${path38.relative(process.cwd(), target)} --write-prompts
19852
+ pixelkiln adopt --manifest ${path39.relative(process.cwd(), target)} --write-prompts
19219
19853
  `);
19220
19854
  }
19221
19855
 
19222
19856
  // src/pipeline/doctor.ts
19223
- import { constants, existsSync as existsSync31 } from "fs";
19857
+ import { constants, existsSync as existsSync32 } from "fs";
19224
19858
  import { access, stat as stat4 } from "fs/promises";
19225
- import path39 from "path";
19859
+ import path40 from "path";
19226
19860
  async function doctor(loaded, specs, lock, lockPath, opts = {}) {
19227
19861
  const checks = [];
19228
19862
  const add = (id, level, message6) => checks.push({ id, level, message: message6 });
@@ -19233,8 +19867,8 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
19233
19867
  );
19234
19868
  add("lock", "ok", `${Object.keys(lock.entries).length} valid v2 lock entr(ies)`);
19235
19869
  const dirs = [...new Set(specs.flatMap((spec) => [
19236
- path39.dirname(spec.outFile),
19237
- ...spec.quality ? [path39.dirname(spec.quality.outFile)] : []
19870
+ path40.dirname(spec.outFile),
19871
+ ...spec.quality ? [path40.dirname(spec.quality.outFile)] : []
19238
19872
  ]))];
19239
19873
  const unwritable = [];
19240
19874
  for (const dir of dirs) {
@@ -19257,7 +19891,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
19257
19891
  for (let index = 0; index < entry.outputs.length; index++) {
19258
19892
  const output = entry.outputs[index];
19259
19893
  const spec = specByKey.get(key);
19260
- 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));
19261
19895
  const owner = outputOwners.get(absolute);
19262
19896
  if (owner && owner !== key) duplicateOutputs.push(`${output.path} (${owner}, ${key})`);
19263
19897
  outputOwners.set(absolute, key);
@@ -19272,7 +19906,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
19272
19906
  } else {
19273
19907
  add("lock-outputs", "ok", `${outputOwners.size} recorded output path(s) are uniquely owned`);
19274
19908
  }
19275
- const cacheDir = path39.join(path39.dirname(lockPath), ".pixelkiln", "cache");
19909
+ const cacheDir = path40.join(path40.dirname(lockPath), ".pixelkiln", "cache");
19276
19910
  const stranded = [];
19277
19911
  const invalidState = [];
19278
19912
  const staleMapUrls = [];
@@ -19288,11 +19922,11 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
19288
19922
  const hasSource = Boolean(entry.sourceUrl || entry.sourceUrls?.length);
19289
19923
  let hasCache = false;
19290
19924
  for (const output of entry.outputs) {
19291
- const cached = path39.join(
19925
+ const cached = path40.join(
19292
19926
  cacheDir,
19293
19927
  cacheFileName(output.sha256, output.mediaType ?? MediaType.PNG)
19294
19928
  );
19295
- if (existsSync31(cached) && await sha256File(cached) === output.sha256) {
19929
+ if (existsSync32(cached) && await sha256File(cached) === output.sha256) {
19296
19930
  hasCache = true;
19297
19931
  break;
19298
19932
  }
@@ -19388,16 +20022,16 @@ async function nearestExistingDirectory(start) {
19388
20022
  if ((await stat4(current)).isDirectory()) return current;
19389
20023
  } catch {
19390
20024
  }
19391
- const parent = path39.dirname(current);
20025
+ const parent = path40.dirname(current);
19392
20026
  if (parent === current) return current;
19393
20027
  current = parent;
19394
20028
  }
19395
20029
  }
19396
20030
 
19397
20031
  // src/pipeline/audit.ts
19398
- import { readFile as readFile27 } from "fs/promises";
19399
- import { existsSync as existsSync32 } from "fs";
19400
- import path40 from "path";
20032
+ import { readFile as readFile28 } from "fs/promises";
20033
+ import { existsSync as existsSync33 } from "fs";
20034
+ import path41 from "path";
19401
20035
  function paletteDistance(asset, reference) {
19402
20036
  if (!asset.length || !reference.length) return 0;
19403
20037
  let total = 0;
@@ -19433,12 +20067,12 @@ async function auditStyle(loaded, specs, styleId, lock) {
19433
20067
  const unreadable = [];
19434
20068
  for (const spec of mine) {
19435
20069
  for (const output of resolveSpecOutputs(spec, lock, loaded.root)) {
19436
- if (!existsSync32(output.absolutePath)) {
20070
+ if (!existsSync33(output.absolutePath)) {
19437
20071
  missing.push(output.id);
19438
20072
  continue;
19439
20073
  }
19440
20074
  try {
19441
- const png = decodePng(await readFile27(output.absolutePath));
20075
+ const png = decodePng(await readFile28(output.absolutePath));
19442
20076
  const palette = extractPalette(png, 12);
19443
20077
  assets.push({
19444
20078
  assetId: spec.assetId,
@@ -19462,10 +20096,10 @@ async function auditStyle(loaded, specs, styleId, lock) {
19462
20096
  let referenceFromStyleImages = false;
19463
20097
  const refPalettes = [];
19464
20098
  for (const img of style.styleImages) {
19465
- const abs = path40.resolve(loaded.root, img.path);
19466
- if (!existsSync32(abs)) continue;
20099
+ const abs = path41.resolve(loaded.root, img.path);
20100
+ if (!existsSync33(abs)) continue;
19467
20101
  try {
19468
- refPalettes.push(extractPalette(decodePng(await readFile27(abs)), 12));
20102
+ refPalettes.push(extractPalette(decodePng(await readFile28(abs)), 12));
19469
20103
  } catch {
19470
20104
  }
19471
20105
  }
@@ -19693,24 +20327,24 @@ async function runAudit(args) {
19693
20327
  }
19694
20328
 
19695
20329
  // src/cli/commands/pack.ts
19696
- import path42 from "path";
19697
- import { readFile as readFile28 } from "fs/promises";
20330
+ import path43 from "path";
20331
+ import { readFile as readFile29 } from "fs/promises";
19698
20332
 
19699
20333
  // src/pipeline/pack.ts
19700
20334
  import { readFileSync as readFileSync6 } from "fs";
19701
- import path41 from "path";
20335
+ import path42 from "path";
19702
20336
  var DEFAULT_FRAME_SET_FPS = 12;
19703
20337
  function resolvePackInputs(raw, inputsFilePath) {
19704
20338
  if (!Array.isArray(raw) || !raw.length) {
19705
20339
  throw new Error("--inputs must be a non-empty JSON array of { id, path }");
19706
20340
  }
19707
- const baseDir = path41.dirname(path41.resolve(inputsFilePath));
20341
+ const baseDir = path42.dirname(path42.resolve(inputsFilePath));
19708
20342
  return raw.map((entry, i) => {
19709
20343
  const e = entry;
19710
20344
  if (typeof e.id !== "string" || typeof e.path !== "string") {
19711
20345
  throw new Error(`--inputs[${i}] needs string "id" and "path"`);
19712
20346
  }
19713
- return { id: e.id, path: path41.resolve(baseDir, e.path) };
20347
+ return { id: e.id, path: path42.resolve(baseDir, e.path) };
19714
20348
  });
19715
20349
  }
19716
20350
  function packSprites(inputs, options = {}) {
@@ -19794,7 +20428,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
19794
20428
  }
19795
20429
  const inputs2 = Object.entries(options.sourceOverrides).map(([id, source]) => ({
19796
20430
  id,
19797
- path: path41.resolve(source)
20431
+ path: path42.resolve(source)
19798
20432
  }));
19799
20433
  if (!inputs2.length) throw new Error(`No approved quality outputs for style "${styleId}".`);
19800
20434
  const packed2 = packSprites(inputs2, options);
@@ -19818,7 +20452,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
19818
20452
  let outputs = resolveEntryOutputs(entry, id, manifestDir);
19819
20453
  const source = sources[id];
19820
20454
  if (source && outputs.length <= 1) {
19821
- inputs.push({ id, path: path41.resolve(manifestDir, source) });
20455
+ inputs.push({ id, path: path42.resolve(manifestDir, source) });
19822
20456
  continue;
19823
20457
  }
19824
20458
  if (source && sourceIsStem(source, entry, manifestDir)) {
@@ -19847,12 +20481,13 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
19847
20481
  id,
19848
20482
  kind: isFrames ? "frames" : "members",
19849
20483
  ...fps ? { fps } : {},
20484
+ ...isFrames && frameSetLoop(entry) === false ? { loop: false } : {},
19850
20485
  frames: selected.map((output) => output.id)
19851
20486
  });
19852
20487
  }
19853
20488
  }
19854
20489
  for (const [id, source] of Object.entries(sources)) {
19855
- 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) });
19856
20491
  }
19857
20492
  if (!inputs.length) {
19858
20493
  throw new Error(`No readable sprites for style "${styleId}"; ${noOutput.length} skipped.`);
@@ -19960,7 +20595,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
19960
20595
  const source = sources[id];
19961
20596
  const entry = lock.entries[`${styleId}/${id}`];
19962
20597
  if (source && !(entry && sourceIsStem(source, entry, manifestDir))) {
19963
- placements.push({ id, path: path41.resolve(manifestDir, source), cell });
20598
+ placements.push({ id, path: path42.resolve(manifestDir, source), cell });
19964
20599
  continue;
19965
20600
  }
19966
20601
  if (source && entry) {
@@ -19993,7 +20628,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
19993
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}`
19994
20629
  );
19995
20630
  }
19996
- 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;
19997
20632
  const mounted = mountSprites(placements, {
19998
20633
  cellWidth: mount.cellWidth,
19999
20634
  cellHeight: mount.cellHeight,
@@ -20003,7 +20638,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
20003
20638
  ...mounted,
20004
20639
  sources: mount.base ? [{
20005
20640
  id: "$base",
20006
- path: path41.resolve(manifestDir, mount.base),
20641
+ path: path42.resolve(manifestDir, mount.base),
20007
20642
  sha256: sha256(basePng),
20008
20643
  included: true
20009
20644
  }, ...mounted.sources] : mounted.sources,
@@ -20385,7 +21020,7 @@ function renderGodotSpriteFrames(atlas, opts) {
20385
21020
  );
20386
21021
  };
20387
21022
  for (const set of atlas.sets ?? []) {
20388
- 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);
20389
21024
  else for (const id of set.frames) animation(id, [id], GODOT_DEFAULT_SPEED, false);
20390
21025
  }
20391
21026
  for (const frame of atlas.frames) {
@@ -20407,12 +21042,12 @@ async function runPack(args) {
20407
21042
  if (args.primaryOnly || args.outputRoles.length) {
20408
21043
  throw new Error("--primary-only and --output-role require manifest-driven pack");
20409
21044
  }
20410
- const raw = JSON.parse(await readFile28(path42.resolve(args.inputs), "utf8"));
21045
+ const raw = JSON.parse(await readFile29(path43.resolve(args.inputs), "utf8"));
20411
21046
  const inputs = resolvePackInputs(raw, args.inputs);
20412
21047
  const { png, atlas, skipped, sources } = packSprites(inputs, { columns: args.columns });
20413
21048
  const format = sheetFormat(args);
20414
- const base = path42.resolve(args.out.replace(/\.(?:png|json|tres)$/i, ""));
20415
- 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`) });
20416
21051
  const outputs = [
20417
21052
  { path: `${base}.png`, data: png },
20418
21053
  { path: `${base}${extension}`, data: document }
@@ -20425,7 +21060,7 @@ async function runPack(args) {
20425
21060
  log(
20426
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})`
20427
21062
  );
20428
- log(` ${path42.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
21063
+ log(` ${path43.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
20429
21064
  for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
20430
21065
  return;
20431
21066
  }
@@ -20433,7 +21068,7 @@ async function runPack(args) {
20433
21068
  if (args.primaryOnly && args.outputRoles.length) {
20434
21069
  throw new Error("pack accepts either --primary-only or --output-role, not both");
20435
21070
  }
20436
- const manifestDir = path42.dirname(path42.resolve(args.manifest));
21071
+ const manifestDir = path43.dirname(path43.resolve(args.manifest));
20437
21072
  const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
20438
21073
  for (const styleId of styleIds) {
20439
21074
  const style = loaded.manifest.styles[styleId];
@@ -20448,9 +21083,9 @@ async function runPack(args) {
20448
21083
  sourceOverrides: qualitySources,
20449
21084
  sources: manifestSources(loaded.manifest, styleId)
20450
21085
  });
20451
- 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`);
20452
21087
  const format = sheetFormat(args);
20453
- const { extension, document } = renderSheetDocument(format, atlas, { imageName: path42.basename(`${base}.png`) });
21088
+ const { extension, document } = renderSheetDocument(format, atlas, { imageName: path43.basename(`${base}.png`) });
20454
21089
  const outputs = [
20455
21090
  { path: `${base}.png`, data: png },
20456
21091
  { path: `${base}${extension}`, data: document }
@@ -20482,13 +21117,13 @@ async function runPack(args) {
20482
21117
  log(
20483
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})`
20484
21119
  );
20485
- log(` ${path42.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
21120
+ log(` ${path43.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
20486
21121
  for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
20487
21122
  }
20488
21123
  }
20489
21124
  async function runMount(args) {
20490
21125
  const { loaded, lock } = await openProject2(args);
20491
- const manifestDir = path42.dirname(path42.resolve(args.manifest));
21126
+ const manifestDir = path43.dirname(path43.resolve(args.manifest));
20492
21127
  const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
20493
21128
  for (const styleId of styleIds) {
20494
21129
  const style = loaded.manifest.styles[styleId];
@@ -20527,9 +21162,9 @@ async function runMount(args) {
20527
21162
  sources,
20528
21163
  outputRoles
20529
21164
  );
20530
- const out = path42.resolve(manifestDir, style.mount.out);
21165
+ const out = path43.resolve(manifestDir, style.mount.out);
20531
21166
  const format = sheetFormat(args);
20532
- const { extension, document } = renderSheetDocument(format, atlas, { imageName: path42.basename(out) });
21167
+ const { extension, document } = renderSheetDocument(format, atlas, { imageName: path43.basename(out) });
20533
21168
  const metadata = out.replace(/\.png$/, "") + extension;
20534
21169
  const companion = out.replace(/\.png$/, "") + ".pixelkiln.json";
20535
21170
  const outputs = [
@@ -20549,7 +21184,7 @@ async function runMount(args) {
20549
21184
  await provenanceFile("$lock", args.lock),
20550
21185
  ...qualityRecords,
20551
21186
  ...artifactSources.filter(
20552
- (source) => source.id !== "$base" || path42.resolve(source.path) !== out
21187
+ (source) => source.id !== "$base" || path43.resolve(source.path) !== out
20553
21188
  )
20554
21189
  ],
20555
21190
  options: {
@@ -20563,14 +21198,14 @@ async function runMount(args) {
20563
21198
  log(
20564
21199
  ` ${styleId}: ${atlas.frames.length} cell(s) into ${atlas.sheet.width}x${atlas.sheet.height}` + (overBase ? ` over ${style.mount.base}` : " (new sheet)")
20565
21200
  );
20566
- log(` ${path42.relative(process.cwd(), out)} + atlas/provenance JSON`);
21201
+ log(` ${path43.relative(process.cwd(), out)} + atlas/provenance JSON`);
20567
21202
  for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
20568
21203
  }
20569
21204
  }
20570
21205
  async function runExport(args) {
20571
21206
  const { loaded, specs, lock } = await openProject2(args);
20572
21207
  const format = args.format ?? "generic";
20573
- const manifestDir = path42.dirname(path42.resolve(args.manifest));
21208
+ const manifestDir = path43.dirname(path43.resolve(args.manifest));
20574
21209
  const selected = specs.filter((spec) => {
20575
21210
  if (spec.generator !== "tiles" && spec.generator !== "terrain") return false;
20576
21211
  if (args.styles.length && !args.styles.includes(spec.styleId)) return false;
@@ -20586,12 +21221,12 @@ async function runExport(args) {
20586
21221
  for (const spec of selected) {
20587
21222
  const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
20588
21223
  const style = loaded.manifest.styles[spec.styleId];
20589
- const defaultBase = path42.resolve(manifestDir, style.outDir, `${spec.assetId}-tileset`);
20590
- 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;
20591
21226
  const result = exportTileset(entry, spec, {
20592
21227
  format,
20593
21228
  manifestDir,
20594
- imageName: path42.basename(`${base}.png`),
21229
+ imageName: path43.basename(`${base}.png`),
20595
21230
  columns: args.columns
20596
21231
  });
20597
21232
  const outputs = [
@@ -20609,7 +21244,7 @@ async function runExport(args) {
20609
21244
  asset: spec.assetId,
20610
21245
  columns: args.columns ?? null,
20611
21246
  format,
20612
- image: path42.basename(`${base}.png`),
21247
+ image: path43.basename(`${base}.png`),
20613
21248
  providerRules: result.generic.providerRules,
20614
21249
  style: spec.styleId,
20615
21250
  tileType: spec.tileType ?? null
@@ -20619,7 +21254,7 @@ async function runExport(args) {
20619
21254
  ` ${spec.styleId}/${spec.assetId}: ${result.generic.tiles.length} tile(s), ${result.generic.sheet.width}x${result.generic.sheet.height} (${format})`
20620
21255
  );
20621
21256
  log(
20622
- ` ${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`
20623
21258
  );
20624
21259
  }
20625
21260
  }
@@ -20632,23 +21267,23 @@ function sheetFormat(args) {
20632
21267
  }
20633
21268
 
20634
21269
  // src/cli/commands/pixellab-utils.ts
20635
- import path44 from "path";
21270
+ import path45 from "path";
20636
21271
 
20637
21272
  // src/pixellab-utils.ts
20638
- import { existsSync as existsSync33 } from "fs";
20639
- import { mkdir as mkdir13, readFile as readFile29, writeFile as writeFile18 } from "fs/promises";
20640
- 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";
20641
21276
  var UNZOOM_MIN_SIDE = 256;
20642
21277
  var UNZOOM_MAX_AREA = 2048 * 2048;
20643
21278
  function defaultUnzoomOut(input) {
20644
- const ext = path43.extname(input);
21279
+ const ext = path44.extname(input);
20645
21280
  return `${ext ? input.slice(0, -ext.length) : input}.unzoomed.png`;
20646
21281
  }
20647
21282
  async function unzoomFile(client2, input, opts = {}) {
20648
- const out = path43.resolve(opts.out ?? defaultUnzoomOut(input));
20649
- if (path43.resolve(input) === out) throw new Error("unzoom will not overwrite its own input; pass a different --out");
20650
- if (existsSync33(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
20651
- 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);
20652
21287
  const metadata = imageMetadata(bytes);
20653
21288
  if (!metadata) throw new Error(`${input} is not a readable PNG or JPEG`);
20654
21289
  const { width, height, format } = metadata;
@@ -20661,22 +21296,22 @@ async function unzoomFile(client2, input, opts = {}) {
20661
21296
  throw new Error(`${input} is ${width}x${height}; PixelLab's unzoom takes at most 2048x2048 worth of pixels`);
20662
21297
  }
20663
21298
  const res = await client2.unzoom({ image: { base64: bytes.toString("base64"), format }, quantize: opts.quantize });
20664
- await mkdir13(path43.dirname(out), { recursive: true });
20665
- await writeFile18(out, res.png);
21299
+ await mkdir14(path44.dirname(out), { recursive: true });
21300
+ await writeFile19(out, res.png);
20666
21301
  return { out, original: res.originalSize, unzoomed: res.unzoomedSize, zoomFactor: res.zoomFactor, usage: res.usage };
20667
21302
  }
20668
21303
  var FONT_WEIGHTS = ["Bold", "Regular"];
20669
21304
  var FONT_GLYPH_SIZES = [8, 16, 32, 64];
20670
- var FONT_COST_GENERATIONS = 25;
21305
+ var FONT_COST_GENERATIONS = 15;
20671
21306
  function fontOutputPaths(out) {
20672
- const base = path43.resolve(out.replace(/\.(ttf|png)$/i, ""));
21307
+ const base = path44.resolve(out.replace(/\.(ttf|png)$/i, ""));
20673
21308
  return { ttf: `${base}.ttf`, atlas: `${base}.png` };
20674
21309
  }
20675
21310
  async function generateFont(client2, opts) {
20676
21311
  const { ttf, atlas } = fontOutputPaths(opts.out);
20677
21312
  if (!opts.force) {
20678
21313
  for (const file of [ttf, atlas]) {
20679
- 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`);
20680
21315
  }
20681
21316
  }
20682
21317
  const sleep3 = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
@@ -20703,9 +21338,9 @@ async function generateFont(client2, opts) {
20703
21338
  client2.download(job.download_ttf_url),
20704
21339
  client2.download(job.download_atlas_url)
20705
21340
  ]);
20706
- await mkdir13(path43.dirname(ttf), { recursive: true });
20707
- await writeFile18(ttf, ttfBytes);
20708
- await writeFile18(atlas, atlasBytes);
21341
+ await mkdir14(path44.dirname(ttf), { recursive: true });
21342
+ await writeFile19(ttf, ttfBytes);
21343
+ await writeFile19(atlas, atlasBytes);
20709
21344
  return { jobId, ttf, atlas, glyphPx: job.glyph_px ?? null, usage: job.usage };
20710
21345
  }
20711
21346
  if (Date.now() - started > timeout) {
@@ -20716,10 +21351,65 @@ async function generateFont(client2, opts) {
20716
21351
  await sleep3(interval);
20717
21352
  }
20718
21353
  }
21354
+ var PIXELATE_COST_GENERATIONS = 6;
21355
+ var PIXELATE_MAX_SIDE = 2048;
21356
+ function defaultPixelateOut(input) {
21357
+ const ext = path44.extname(input);
21358
+ return `${ext ? input.slice(0, -ext.length) : input}.pixel.png`;
21359
+ }
21360
+ async function pixelateFile(client2, input, opts = {}) {
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);
21365
+ const metadata = imageMetadata(bytes);
21366
+ if (!metadata) throw new Error(`${input} is not a readable PNG or JPEG`);
21367
+ const { width, height, format } = metadata;
21368
+ const sleep3 = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
21369
+ const interval = opts.intervalMs ?? 5e3;
21370
+ const timeout = opts.timeoutMs ?? 10 * 6e4;
21371
+ const submitted = await client2.imageToPixelartProFlash({
21372
+ image: { base64: bytes.toString("base64"), format },
21373
+ description: opts.description,
21374
+ seed: opts.seed
21375
+ });
21376
+ const jobId = submitted.background_job_id;
21377
+ const started = Date.now();
21378
+ for (; ; ) {
21379
+ const job = await client2.getBackgroundJob(jobId);
21380
+ opts.onProgress?.(job.status);
21381
+ if (job.status === "failed") throw new Error(`PixelLab pixelate job ${jobId} failed upstream`);
21382
+ if (job.status === "completed") {
21383
+ const images = job.last_response?.images;
21384
+ const first = Array.isArray(images) ? images[0] : void 0;
21385
+ if (!first || typeof first.base64 !== "string" || !first.base64) {
21386
+ throw new Error(`PixelLab pixelate job ${jobId} completed without an image at last_response.images[0]`);
21387
+ }
21388
+ const png = Buffer.from(first.base64, "base64");
21389
+ const size = imageMetadata(png);
21390
+ await mkdir14(path44.dirname(out), { recursive: true });
21391
+ await writeFile19(out, png);
21392
+ return {
21393
+ jobId,
21394
+ out,
21395
+ original: { width, height },
21396
+ pixelated: { width: size?.width ?? Number(first.width), height: size?.height ?? Number(first.height) },
21397
+ imageId: submitted.image_id,
21398
+ usage: job.usage ?? null
21399
+ };
21400
+ }
21401
+ if (Date.now() - started > timeout) {
21402
+ throw new Error(
21403
+ `PixelLab pixelate job ${jobId} is still ${job.status} after ${Math.round(timeout / 1e3)} s; it keeps running upstream, but this command has stopped waiting`
21404
+ );
21405
+ }
21406
+ await sleep3(interval);
21407
+ }
21408
+ }
20719
21409
 
20720
21410
  // src/cli/commands/pixellab-utils.ts
20721
21411
  function loadKeys(args) {
20722
- loadEnvFiles(path44.dirname(path44.resolve(args.manifest)));
21412
+ loadEnvFiles(path45.dirname(path45.resolve(args.manifest)));
20723
21413
  loadEnvFiles(process.cwd());
20724
21414
  }
20725
21415
  async function runUnzoom(args) {
@@ -20783,21 +21473,109 @@ async function runFont(args) {
20783
21473
  log(` wrote ${res.ttf}`);
20784
21474
  log(` wrote ${res.atlas}`);
20785
21475
  }
21476
+ async function runPixelate(args) {
21477
+ if (!args.from) throw new UsageError("pixelate needs --from <image>: the photo, painting, or render to convert");
21478
+ const out = args.out ?? defaultPixelateOut(args.from);
21479
+ log(` pixelate ${args.from} \u2192 ${out}${args.description ? ` ("${args.description}")` : ""}; PixelLab picks the output size`);
21480
+ log(` cost: ${PIXELATE_COST_GENERATIONS} generations (Image to Pixel Art Pro Flash, flat)`);
21481
+ if (args.budget !== void 0 && args.budget < PIXELATE_COST_GENERATIONS) {
21482
+ throw new Error(`--budget ${args.budget} is below this conversion's ${PIXELATE_COST_GENERATIONS}-generation cost`);
21483
+ }
21484
+ if (args.dryRun) {
21485
+ log(" dry run: nothing sent");
21486
+ return;
21487
+ }
21488
+ if (!await confirm(` Spend ${PIXELATE_COST_GENERATIONS} generations on this conversion?`, args.yes)) return;
21489
+ loadKeys(args);
21490
+ let last = "";
21491
+ const res = await pixelateFile(clientFromEnv(), args.from, {
21492
+ out,
21493
+ description: args.description,
21494
+ force: args.force,
21495
+ onProgress: (status) => {
21496
+ if (status !== last) log(` ${status}\u2026`);
21497
+ last = status;
21498
+ }
21499
+ });
21500
+ if (args.json) {
21501
+ log(JSON.stringify({ version: 1, ...res }, null, 2));
21502
+ return;
21503
+ }
21504
+ const scaled = Math.max(res.original.width, res.original.height) > PIXELATE_MAX_SIDE ? " (scaled down upstream first)" : "";
21505
+ log(
21506
+ ` ${res.original.width}x${res.original.height}${scaled} \u2192 ${res.pixelated.width}x${res.pixelated.height}; wrote ${res.out}`
21507
+ );
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
+ }
20786
21564
 
20787
21565
  // src/cli/commands/quality.ts
20788
- import path46 from "path";
20789
- import { readFile as readFile31 } from "fs/promises";
21566
+ import path47 from "path";
21567
+ import { readFile as readFile32 } from "fs/promises";
20790
21568
 
20791
21569
  // src/pipeline/quality-regression.ts
20792
- import path45 from "path";
20793
- import { existsSync as existsSync34 } from "fs";
20794
- 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";
20795
21573
  import { z as z13 } from "zod";
20796
21574
  var SHA256_RE = /^[0-9a-f]{64}$/;
20797
21575
  var HEX_RE = /^#[0-9a-f]{6}$/;
20798
21576
  var MAX_REDMEAN_DISTANCE = 765;
20799
21577
  var QualityPathSchema = z13.string().min(1).refine(
20800
- (value) => !path45.posix.isAbsolute(value) && !value.includes("\\"),
21578
+ (value) => !path46.posix.isAbsolute(value) && !value.includes("\\"),
20801
21579
  "expected a portable relative path"
20802
21580
  );
20803
21581
  var DEFAULT_QUALITY_TOLERANCES = {
@@ -20944,10 +21722,10 @@ function measureDecodedImageQuality(png, digest2) {
20944
21722
  };
20945
21723
  }
20946
21724
  async function measureImageQuality(file) {
20947
- const absolute = path45.resolve(file);
21725
+ const absolute = path46.resolve(file);
20948
21726
  let bytes;
20949
21727
  try {
20950
- bytes = await readFile30(absolute);
21728
+ bytes = await readFile31(absolute);
20951
21729
  } catch (error) {
20952
21730
  throw new Error(`Cannot read quality image ${absolute}: ${message4(error)}`, { cause: error });
20953
21731
  }
@@ -20966,11 +21744,11 @@ function resolveQualityInputs(raw, inputsFilePath) {
20966
21744
  }
20967
21745
  const ids = parsed.data.map((entry) => entry.id);
20968
21746
  if (new Set(ids).size !== ids.length) throw new Error("--inputs contains a duplicate quality case id.");
20969
- const root = path45.dirname(path45.resolve(inputsFilePath));
21747
+ const root = path46.dirname(path46.resolve(inputsFilePath));
20970
21748
  return parsed.data.map((entry) => ({
20971
21749
  id: entry.id,
20972
- file: path45.resolve(root, entry.path),
20973
- ...entry.record ? { record: path45.resolve(root, entry.record) } : {},
21750
+ file: path46.resolve(root, entry.path),
21751
+ ...entry.record ? { record: path46.resolve(root, entry.record) } : {},
20974
21752
  tolerances: QualityToleranceSchema.parse({
20975
21753
  ...DEFAULT_QUALITY_TOLERANCES,
20976
21754
  ...entry.tolerances
@@ -20978,7 +21756,7 @@ function resolveQualityInputs(raw, inputsFilePath) {
20978
21756
  }));
20979
21757
  }
20980
21758
  function portableRelative2(from, to) {
20981
- return path45.relative(from, path45.resolve(to)).split(path45.sep).join("/") || ".";
21759
+ return path46.relative(from, path46.resolve(to)).split(path46.sep).join("/") || ".";
20982
21760
  }
20983
21761
  async function verifyRecordForImage(recordPath, imagePath) {
20984
21762
  const verification = await verifyArtifactBundle(recordPath);
@@ -20993,20 +21771,20 @@ async function verifyRecordForImage(recordPath, imagePath) {
20993
21771
  if (record.kind !== "refine" || options === null || typeof options !== "object" || options.schema !== "pixelkiln-quality") {
20994
21772
  throw new Error(`Quality record ${recordPath} is not a PixelKiln refinement record.`);
20995
21773
  }
20996
- const outputs = record.outputs.map((output) => path45.resolve(path45.dirname(recordPath), output.path));
20997
- 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))) {
20998
21776
  throw new Error(`Quality record ${recordPath} does not own ${imagePath}.`);
20999
21777
  }
21000
21778
  }
21001
21779
  async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
21002
21780
  if (!inputs.length) throw new Error("Cannot snapshot an empty quality baseline.");
21003
- const absolute = path45.resolve(baselinePath);
21781
+ const absolute = path46.resolve(baselinePath);
21004
21782
  for (const input of inputs) {
21005
- 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) {
21006
21784
  throw new Error(`Quality baseline output would overwrite an input: ${absolute}.`);
21007
21785
  }
21008
21786
  }
21009
- const root = path45.dirname(absolute);
21787
+ const root = path46.dirname(absolute);
21010
21788
  const cases = [];
21011
21789
  for (const input of [...inputs].sort((a, b) => a.id.localeCompare(b.id))) {
21012
21790
  const expected = await measureImageQuality(input.file);
@@ -21033,8 +21811,8 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
21033
21811
  cases
21034
21812
  });
21035
21813
  const data = Buffer.from(JSON.stringify(baseline, null, 2) + "\n");
21036
- if (existsSync34(absolute) && !options.force) {
21037
- const current = await readFile30(absolute);
21814
+ if (existsSync35(absolute) && !options.force) {
21815
+ const current = await readFile31(absolute);
21038
21816
  if (!current.equals(data)) {
21039
21817
  throw new OverwriteRefusedError(
21040
21818
  `Quality baseline already exists with different content: ${absolute}. Pass --force to replace it.`,
@@ -21046,10 +21824,10 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
21046
21824
  return { baseline, path: absolute, changed: result.changed.length > 0 };
21047
21825
  }
21048
21826
  async function readQualityBaseline(baselinePath) {
21049
- const absolute = path45.resolve(baselinePath);
21827
+ const absolute = path46.resolve(baselinePath);
21050
21828
  let raw;
21051
21829
  try {
21052
- raw = JSON.parse(await readFile30(absolute, "utf8"));
21830
+ raw = JSON.parse(await readFile31(absolute, "utf8"));
21053
21831
  } catch (error) {
21054
21832
  throw new Error(`Could not read quality baseline ${absolute}: ${message4(error)}`, { cause: error });
21055
21833
  }
@@ -21119,12 +21897,12 @@ function regressionViolations(expected, actual, tolerances) {
21119
21897
  return { violations, warnings };
21120
21898
  }
21121
21899
  async function checkQualityBaseline(baselinePath) {
21122
- const absolute = path45.resolve(baselinePath);
21900
+ const absolute = path46.resolve(baselinePath);
21123
21901
  const baseline = await readQualityBaseline(absolute);
21124
- const root = path45.dirname(absolute);
21902
+ const root = path46.dirname(absolute);
21125
21903
  const cases = [];
21126
21904
  for (const entry of baseline.cases) {
21127
- const file = path45.resolve(root, entry.file);
21905
+ const file = path46.resolve(root, entry.file);
21128
21906
  let actual = null;
21129
21907
  const violations = [];
21130
21908
  const warnings = [];
@@ -21137,7 +21915,7 @@ async function checkQualityBaseline(baselinePath) {
21137
21915
  violations.push(message4(error));
21138
21916
  }
21139
21917
  if (entry.record) {
21140
- const recordPath = path45.resolve(root, entry.record.path);
21918
+ const recordPath = path46.resolve(root, entry.record.path);
21141
21919
  try {
21142
21920
  const recordHash = await sha256File(recordPath);
21143
21921
  if (recordHash !== entry.record.sha256) violations.push("quality record hash changed");
@@ -21179,15 +21957,15 @@ async function runQuality(args) {
21179
21957
  if (!args.out) throw new Error("quality snapshot needs --out <pixelkiln.quality.json>.");
21180
21958
  let raw;
21181
21959
  try {
21182
- raw = JSON.parse(await readFile31(path46.resolve(args.inputs), "utf8"));
21960
+ raw = JSON.parse(await readFile32(path47.resolve(args.inputs), "utf8"));
21183
21961
  } catch (error) {
21184
21962
  throw new Error(
21185
- `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)}`,
21186
21964
  { cause: error }
21187
21965
  );
21188
21966
  }
21189
21967
  const inputs = resolveQualityInputs(raw, args.inputs);
21190
- if (path46.resolve(args.inputs) === path46.resolve(args.out)) {
21968
+ if (path47.resolve(args.inputs) === path47.resolve(args.out)) {
21191
21969
  throw new Error("quality snapshot --out must not overwrite its --inputs file.");
21192
21970
  }
21193
21971
  const result = await snapshotQualityBaseline(inputs, args.out, { force: args.force });
@@ -21200,7 +21978,7 @@ async function runQuality(args) {
21200
21978
  }, null, 2));
21201
21979
  } else {
21202
21980
  log(
21203
- ` ${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))`
21204
21982
  );
21205
21983
  log(` Review the tolerances, commit the baseline, then run quality check in CI.`);
21206
21984
  }
@@ -21226,12 +22004,12 @@ async function runQuality(args) {
21226
22004
  }
21227
22005
 
21228
22006
  // src/cli/commands/recipe.ts
21229
- import path48 from "path";
22007
+ import path49 from "path";
21230
22008
 
21231
22009
  // src/recipes.ts
21232
- import path47 from "path";
21233
- import { existsSync as existsSync35 } from "fs";
21234
- 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";
21235
22013
  import { fileURLToPath as fileURLToPath2 } from "url";
21236
22014
  import { z as z14 } from "zod";
21237
22015
  var SHA256_RE2 = /^[0-9a-f]{64}$/;
@@ -21240,7 +22018,7 @@ var VERSION_RE = /^\d+\.\d+\.\d+$/;
21240
22018
  var RECIPE_FILE = "pixelkiln.recipe.json";
21241
22019
  var RECIPE_DIR_TOKEN = "{{recipeDir}}";
21242
22020
  var PortablePathSchema = z14.string().min(1).refine((value) => {
21243
- if (value.includes("\\") || path47.posix.isAbsolute(value)) return false;
22021
+ if (value.includes("\\") || path48.posix.isAbsolute(value)) return false;
21244
22022
  const parts = value.split("/");
21245
22023
  return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
21246
22024
  }, "expected a portable relative path without '.', '..', or backslashes");
@@ -21416,10 +22194,10 @@ function message5(error) {
21416
22194
  return error instanceof Error ? error.message : String(error);
21417
22195
  }
21418
22196
  async function readRecipeFile(recipePath, bundled) {
21419
- const absolute = path47.resolve(recipePath);
22197
+ const absolute = path48.resolve(recipePath);
21420
22198
  let raw;
21421
22199
  try {
21422
- raw = JSON.parse(await readFile32(absolute, "utf8"));
22200
+ raw = JSON.parse(await readFile33(absolute, "utf8"));
21423
22201
  } catch (error) {
21424
22202
  throw new Error(`Could not read recipe ${absolute}: ${message5(error)}`, { cause: error });
21425
22203
  }
@@ -21430,13 +22208,13 @@ async function readRecipeFile(recipePath, bundled) {
21430
22208
  ` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
21431
22209
  );
21432
22210
  }
21433
- 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 };
21434
22212
  }
21435
22213
  async function walkRecipeFiles(root) {
21436
- if (!existsSync35(root)) return [];
22214
+ if (!existsSync36(root)) return [];
21437
22215
  const out = [];
21438
22216
  for (const entry of await readdir3(root, { withFileTypes: true })) {
21439
- const resolved = path47.join(root, entry.name);
22217
+ const resolved = path48.join(root, entry.name);
21440
22218
  if (entry.isDirectory()) out.push(...await walkRecipeFiles(resolved));
21441
22219
  else if (entry.isFile() && entry.name === RECIPE_FILE) out.push(resolved);
21442
22220
  }
@@ -21447,7 +22225,7 @@ function bundledRecipeRoot() {
21447
22225
  }
21448
22226
  async function listBundledRecipes(root = bundledRecipeRoot()) {
21449
22227
  const recipes = await Promise.all(
21450
- (await walkRecipeFiles(path47.resolve(root))).map((file) => readRecipeFile(file, true))
22228
+ (await walkRecipeFiles(path48.resolve(root))).map((file) => readRecipeFile(file, true))
21451
22229
  );
21452
22230
  return recipes.sort(
21453
22231
  (a, b) => a.recipe.id.localeCompare(b.recipe.id) || compareVersions(b.recipe.version, a.recipe.version)
@@ -21470,9 +22248,9 @@ function parseSelector(selector) {
21470
22248
  return { id: match[1], version: match[2] };
21471
22249
  }
21472
22250
  async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
21473
- const local = path47.resolve(target);
21474
- if (existsSync35(local)) {
21475
- 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);
21476
22254
  return readRecipeFile(recipePath, false);
21477
22255
  }
21478
22256
  if (target.startsWith(".") || target.startsWith("/") || target.includes("\\")) {
@@ -21486,8 +22264,8 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
21486
22264
  return matches[0];
21487
22265
  }
21488
22266
  async function verifyFile(root, file) {
21489
- const absolute = path47.resolve(root, ...file.path.split("/"));
21490
- if (!existsSync35(absolute)) {
22267
+ const absolute = path48.resolve(root, ...file.path.split("/"));
22268
+ if (!existsSync36(absolute)) {
21491
22269
  return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
21492
22270
  }
21493
22271
  const actualSha256 = await sha256File(absolute);
@@ -21502,7 +22280,7 @@ async function verifyRecipe(target, options = {}) {
21502
22280
  const loaded = await resolveRecipe(target, options.bundledRoot);
21503
22281
  const actualIntegrity = recipeDigest(loaded.authored);
21504
22282
  const files = await Promise.all(loaded.recipe.files.map((file) => verifyFile(loaded.dir, file)));
21505
- const modelRoot = options.modelRoot ? path47.resolve(options.modelRoot) : null;
22283
+ const modelRoot = options.modelRoot ? path48.resolve(options.modelRoot) : null;
21506
22284
  const models = modelRoot ? await Promise.all(loaded.recipe.models.map(async (model) => ({
21507
22285
  ...await verifyFile(modelRoot, model),
21508
22286
  source: model.source,
@@ -21536,7 +22314,7 @@ async function verifyRecipe(target, options = {}) {
21536
22314
  };
21537
22315
  }
21538
22316
  function renderStyle(style, destination, cwd) {
21539
- const portableDir = path47.relative(cwd, destination).split(path47.sep).join("/") || ".";
22317
+ const portableDir = path48.relative(cwd, destination).split(path48.sep).join("/") || ".";
21540
22318
  const replace = (value) => {
21541
22319
  if (typeof value === "string") return value.replaceAll(RECIPE_DIR_TOKEN, portableDir);
21542
22320
  if (Array.isArray(value)) return value.map(replace);
@@ -21548,25 +22326,25 @@ function renderStyle(style, destination, cwd) {
21548
22326
  return replace(style);
21549
22327
  }
21550
22328
  async function installRecipe(target, options = {}) {
21551
- const cwd = path47.resolve(options.cwd ?? process.cwd());
22329
+ const cwd = path48.resolve(options.cwd ?? process.cwd());
21552
22330
  const loaded = await resolveRecipe(target, options.bundledRoot);
21553
22331
  const verification = await verifyRecipe(loaded.path);
21554
22332
  if (!verification.ok) {
21555
22333
  throw new Error(`Recipe ${loaded.recipe.id}@${loaded.recipe.version} failed integrity verification.`);
21556
22334
  }
21557
- 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);
21558
22336
  const sources = [
21559
22337
  { path: RECIPE_FILE, source: loaded.path },
21560
- ...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("/")) }))
21561
22339
  ];
21562
22340
  const files = await Promise.all(sources.map(async (file) => ({
21563
- path: path47.join(destination, ...file.path.split("/")),
21564
- data: await readFile32(file.source)
22341
+ path: path48.join(destination, ...file.path.split("/")),
22342
+ data: await readFile33(file.source)
21565
22343
  })));
21566
22344
  if (!options.force) {
21567
22345
  for (const file of files) {
21568
- if (!existsSync35(file.path)) continue;
21569
- const current = await readFile32(file.path);
22346
+ if (!existsSync36(file.path)) continue;
22347
+ const current = await readFile33(file.path);
21570
22348
  if (!current.equals(file.data)) {
21571
22349
  throw new OverwriteRefusedError(
21572
22350
  `Recipe destination has local changes: ${file.path}. Choose another --out or pass --force to replace declared recipe files.`,
@@ -21660,14 +22438,14 @@ async function runRecipe(args) {
21660
22438
  }, null, 2));
21661
22439
  } else {
21662
22440
  log(` installed ${result.recipe.id}@${result.recipe.version}`);
21663
- log(` ${path48.relative(process.cwd(), result.destination) || "."}`);
22441
+ log(` ${path49.relative(process.cwd(), result.destination) || "."}`);
21664
22442
  log(`
21665
22443
  Add this entry under your manifest's styles object:`);
21666
22444
  log(JSON.stringify({ [result.styleId]: result.style }, null, 2));
21667
22445
  if (result.recipe.models.length) {
21668
22446
  log(`
21669
22447
  Models are not downloaded automatically. Verify them with:`);
21670
- 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>`);
21671
22449
  }
21672
22450
  }
21673
22451
  return;
@@ -21675,7 +22453,7 @@ async function runRecipe(args) {
21675
22453
  }
21676
22454
 
21677
22455
  // src/cli/commands/refine.ts
21678
- import path49 from "path";
22456
+ import path50 from "path";
21679
22457
  async function runRefine(args) {
21680
22458
  if (args.from || args.subcommand === "approve") {
21681
22459
  if (!args.from) {
@@ -21705,12 +22483,12 @@ async function runRefine(args) {
21705
22483
  ` recovered ${result3.detection.columns}x${result3.detection.rows} native grid (${result3.detection.confidence}: ${result3.detection.consensus})`
21706
22484
  );
21707
22485
  log(` enforced ${result3.palette.length}-color palette without dithering`);
21708
- log(` wrote ${path49.relative(process.cwd(), result3.output)}`);
21709
- 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)}`);
21710
22488
  log(`
21711
22489
  Automated checks passed. Human 1\xD7 review is still required:`);
21712
22490
  log(
21713
- ` 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"`
21714
22492
  );
21715
22493
  }
21716
22494
  return;
@@ -21738,7 +22516,7 @@ async function runRefine(args) {
21738
22516
  if (args.json) log(JSON.stringify(result3, null, 2));
21739
22517
  else {
21740
22518
  log(
21741
- 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()}`
21742
22520
  );
21743
22521
  }
21744
22522
  return;
@@ -21747,7 +22525,7 @@ async function runRefine(args) {
21747
22525
  if (args.json) {
21748
22526
  log(JSON.stringify(result2, null, 2));
21749
22527
  } else {
21750
- 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)}`);
21751
22529
  for (const reason of result2.reasons) log(` ${reason}`);
21752
22530
  }
21753
22531
  if (!result2.safe) process.exitCode = 1;
@@ -21793,7 +22571,7 @@ async function runRefine(args) {
21793
22571
  log(` ${item.state.padEnd(16)} ${item.key} ${item.reason}`);
21794
22572
  if (item.state === "needs-approval") {
21795
22573
  log(
21796
- ` 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"`
21797
22575
  );
21798
22576
  }
21799
22577
  }
@@ -21804,9 +22582,9 @@ async function runRefine(args) {
21804
22582
  }
21805
22583
 
21806
22584
  // src/cli/commands/skeleton.ts
21807
- import path50 from "path";
21808
- import { existsSync as existsSync36 } from "fs";
21809
- 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";
21810
22588
 
21811
22589
  // src/skeleton-preview.ts
21812
22590
  var RIGHT = [255, 140, 40, 255];
@@ -21917,12 +22695,12 @@ var Canvas = class {
21917
22695
  async function runEstimateSkeleton(args) {
21918
22696
  const image = args.target;
21919
22697
  if (!image) throw new UsageError("estimate-skeleton needs an image path: pixelkiln estimate-skeleton <image> [--out keypoints.json]");
21920
- if (args.out && existsSync36(path50.resolve(args.out)) && !args.force) {
22698
+ if (args.out && existsSync37(path51.resolve(args.out)) && !args.force) {
21921
22699
  throw new UsageError(`${args.out} already exists; pass --force to replace it`);
21922
22700
  }
21923
- loadEnvFiles(path50.dirname(path50.resolve(args.manifest)));
22701
+ loadEnvFiles(path51.dirname(path51.resolve(args.manifest)));
21924
22702
  loadEnvFiles(process.cwd());
21925
- const bytes = await readFile33(path50.resolve(image));
22703
+ const bytes = await readFile34(path51.resolve(image));
21926
22704
  const metadata = imageMetadata(bytes);
21927
22705
  if (!metadata) throw new UsageError(`${image} is not a readable PNG or JPEG`);
21928
22706
  const sizeProblem = estimateSkeletonSizeProblem(metadata.width, metadata.height);
@@ -21934,7 +22712,7 @@ async function runEstimateSkeleton(args) {
21934
22712
  const frames = args.frames ?? DEFAULT_SCAFFOLD_FRAMES;
21935
22713
  const json = JSON.stringify(scaffoldSkeletonSet(res.keypoints, frames), null, 2);
21936
22714
  if (args.out) {
21937
- await writeFile19(path50.resolve(args.out), json + "\n");
22715
+ await writeFile20(path51.resolve(args.out), json + "\n");
21938
22716
  log(` wrote ${args.out}: the estimated pose and ${frames} frames to edit`);
21939
22717
  log(` preview it with: pixelkiln skeleton-preview ${args.out} --from ${image}`);
21940
22718
  } else {
@@ -21947,8 +22725,8 @@ async function runSkeletonPreview(args) {
21947
22725
  let sourceFile;
21948
22726
  let label;
21949
22727
  if (target.endsWith(".json")) {
21950
- keypointsFile = path50.resolve(target);
21951
- sourceFile = args.from ? path50.resolve(args.from) : void 0;
22728
+ keypointsFile = path51.resolve(target);
22729
+ sourceFile = args.from ? path51.resolve(args.from) : void 0;
21952
22730
  label = target;
21953
22731
  } else {
21954
22732
  const loaded = await loadManifest(args.manifest);
@@ -21960,31 +22738,31 @@ async function runSkeletonPreview(args) {
21960
22738
  );
21961
22739
  }
21962
22740
  keypointsFile = spec.revision.keypointsFile;
21963
- sourceFile = args.from ? path50.resolve(args.from) : spec.revision.sourceFile;
22741
+ sourceFile = args.from ? path51.resolve(args.from) : spec.revision.sourceFile;
21964
22742
  label = `${spec.styleId}/${spec.assetId}`;
21965
22743
  }
21966
- 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}`);
21967
22745
  let json;
21968
22746
  try {
21969
- json = JSON.parse(await readFile33(keypointsFile, "utf8"));
22747
+ json = JSON.parse(await readFile34(keypointsFile, "utf8"));
21970
22748
  } catch (error) {
21971
22749
  throw new UsageError(`${keypointsFile} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
21972
22750
  }
21973
22751
  const set = parseSkeletonSet(json, keypointsFile);
21974
22752
  let source;
21975
22753
  if (sourceFile) {
21976
- if (!existsSync36(sourceFile)) {
22754
+ if (!existsSync37(sourceFile)) {
21977
22755
  log(` source image does not exist yet, drawing on a plain ground: ${sourceFile}`);
21978
22756
  } else {
21979
- const bytes = await readFile33(sourceFile);
22757
+ const bytes = await readFile34(sourceFile);
21980
22758
  if (imageMetadata(bytes)?.format !== "png") throw new UsageError(`${sourceFile} must be a PNG to preview over`);
21981
22759
  source = decodePng(bytes);
21982
22760
  }
21983
22761
  }
21984
- const out = path50.resolve(args.out ?? keypointsFile.replace(/\.json$/, "") + ".preview.png");
21985
- 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 }));
21986
22764
  log(` ${label}: ${set.frames.length} frames after the starting pose`);
21987
- const shown = path50.relative(process.cwd(), out);
22765
+ const shown = path51.relative(process.cwd(), out);
21988
22766
  log(` wrote ${shown.startsWith("..") ? out : shown}`);
21989
22767
  }
21990
22768
 
@@ -22030,15 +22808,15 @@ async function runTools(args) {
22030
22808
  }
22031
22809
 
22032
22810
  // src/cli/commands/workspace.ts
22033
- import path51 from "path";
22034
- import { existsSync as existsSync37 } from "fs";
22811
+ import path52 from "path";
22812
+ import { existsSync as existsSync38 } from "fs";
22035
22813
  async function runWorkspace(args) {
22036
- const workspacePath = path51.resolve(args.workspace ?? "pixelkiln.workspace.json");
22037
- const dir = path51.dirname(workspacePath);
22814
+ const workspacePath = path52.resolve(args.workspace ?? "pixelkiln.workspace.json");
22815
+ const dir = path52.dirname(workspacePath);
22038
22816
  if (args.subcommand === "add") {
22039
- const manifestPath = path51.resolve(args.target);
22817
+ const manifestPath = path52.resolve(args.target);
22040
22818
  const loadedTarget = await loadManifest(manifestPath);
22041
- 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");
22042
22820
  const ws = await loadWorkspace(workspacePath);
22043
22821
  const id = args.name ?? loadedTarget.manifest.name;
22044
22822
  if (ws.projects.some((p) => p.id === id)) {
@@ -22058,10 +22836,10 @@ async function runWorkspace(args) {
22058
22836
  ...args.account ? { account: args.account } : {}
22059
22837
  };
22060
22838
  await saveWorkspace(workspacePath, { version: 1, projects: [...ws.projects, project] });
22061
- log(` registered "${id}" in ${path51.relative(process.cwd(), workspacePath)}`);
22839
+ log(` registered "${id}" in ${path52.relative(process.cwd(), workspacePath)}`);
22062
22840
  log(` manifest: ${project.manifest}`);
22063
22841
  log(` lock: ${project.lock}`);
22064
- if (!existsSync37(lockPath)) {
22842
+ if (!existsSync38(lockPath)) {
22065
22843
  log(
22066
22844
  ` warning: no lockfile there yet; this project contributes no claims until one is generated`
22067
22845
  );
@@ -22070,7 +22848,7 @@ async function runWorkspace(args) {
22070
22848
  }
22071
22849
  if (args.subcommand === "remove") {
22072
22850
  const ws = await loadWorkspace(workspacePath);
22073
- const resolvedTarget = path51.resolve(args.target);
22851
+ const resolvedTarget = path52.resolve(args.target);
22074
22852
  const match = ws.projects.find(
22075
22853
  (p) => p.id === args.target || resolveProject(dir, p).manifestPath === resolvedTarget
22076
22854
  );
@@ -22081,11 +22859,11 @@ async function runWorkspace(args) {
22081
22859
  version: 1,
22082
22860
  projects: ws.projects.filter((p) => p !== match)
22083
22861
  });
22084
- log(` removed "${match.id}" from ${path51.relative(process.cwd(), workspacePath)}`);
22862
+ log(` removed "${match.id}" from ${path52.relative(process.cwd(), workspacePath)}`);
22085
22863
  return;
22086
22864
  }
22087
22865
  if (args.subcommand === "list") {
22088
- if (!existsSync37(workspacePath)) {
22866
+ if (!existsSync38(workspacePath)) {
22089
22867
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
22090
22868
  }
22091
22869
  const ws = await loadWorkspace(workspacePath);
@@ -22093,9 +22871,9 @@ async function runWorkspace(args) {
22093
22871
  if (args.json) {
22094
22872
  log(JSON.stringify({ version: 1, workspace: workspacePath, projects: ws.projects, diagnostics }, null, 2));
22095
22873
  } else if (!ws.projects.length) {
22096
- log(` no projects registered in ${path51.relative(process.cwd(), workspacePath)}`);
22874
+ log(` no projects registered in ${path52.relative(process.cwd(), workspacePath)}`);
22097
22875
  } else {
22098
- 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)}:`);
22099
22877
  for (const p of ws.projects) {
22100
22878
  log(` ${p.id.padEnd(24)} ${p.manifest.padEnd(40)} (${p.provider}${p.account ? `, ${p.account}` : ""})`);
22101
22879
  }
@@ -22105,7 +22883,7 @@ async function runWorkspace(args) {
22105
22883
  return;
22106
22884
  }
22107
22885
  if (args.subcommand === "status") {
22108
- if (!existsSync37(workspacePath)) {
22886
+ if (!existsSync38(workspacePath)) {
22109
22887
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
22110
22888
  }
22111
22889
  const ws = await loadWorkspace(workspacePath);
@@ -22113,7 +22891,7 @@ async function runWorkspace(args) {
22113
22891
  if (args.json) {
22114
22892
  log(JSON.stringify({ ...report, workspace: workspacePath }, null, 2));
22115
22893
  } else {
22116
- log(` workspace: ${path51.relative(process.cwd(), workspacePath)}`);
22894
+ log(` workspace: ${path52.relative(process.cwd(), workspacePath)}`);
22117
22895
  for (const p of report.projects) {
22118
22896
  if (p.error) {
22119
22897
  log(`
@@ -22167,8 +22945,8 @@ async function runHelp() {
22167
22945
  async function runVersion() {
22168
22946
  for (const up of ["../package.json", "../../package.json"]) {
22169
22947
  const url = new URL(up, import.meta.url);
22170
- if (!existsSync38(url)) continue;
22171
- const pkg = JSON.parse(await readFile34(url, "utf8"));
22948
+ if (!existsSync39(url)) continue;
22949
+ const pkg = JSON.parse(await readFile35(url, "utf8"));
22172
22950
  if (pkg.name === "pixelkiln") {
22173
22951
  log(`${pkg.name} ${pkg.version}`);
22174
22952
  return;
@@ -22215,6 +22993,8 @@ var RUNNERS = {
22215
22993
  "estimate-skeleton": runEstimateSkeleton,
22216
22994
  "skeleton-preview": runSkeletonPreview,
22217
22995
  unzoom: runUnzoom,
22996
+ pixelate: runPixelate,
22997
+ vfx: runVfx,
22218
22998
  font: runFont
22219
22999
  };
22220
23000
  async function main(argv = process.argv.slice(2)) {