pixelkiln 0.41.0 → 0.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -80,7 +80,28 @@ var CHARACTER_DIRECTIONS_8 = [
80
80
  "south-west"
81
81
  ];
82
82
  var CHARACTER_DIRECTIONS_4 = ["south", "west", "east", "north"];
83
+ var MIRRORED_DIRECTION = {
84
+ south: "south",
85
+ "south-east": "south-west",
86
+ east: "west",
87
+ "north-east": "north-west",
88
+ north: "north",
89
+ "north-west": "north-east",
90
+ west: "east",
91
+ "south-west": "south-east"
92
+ };
83
93
  var CharacterModeSchema = z.enum(["standard", "v3", "pro"]);
94
+ var CHARACTER_PROPORTION_PRESETS = ["default", "chibi", "cartoon", "stylized", "realistic_male", "realistic_female", "heroic"];
95
+ var CharacterProportionsSchema = z.union([
96
+ z.enum(CHARACTER_PROPORTION_PRESETS),
97
+ z.object({
98
+ headSize: z.number().min(0.5).max(2).optional(),
99
+ armsLength: z.number().min(0.5).max(2).optional(),
100
+ legsLength: z.number().min(0.5).max(2).optional(),
101
+ shoulderWidth: z.number().min(0.5).max(2).optional(),
102
+ hipWidth: z.number().min(0.5).max(2).optional()
103
+ }).strict()
104
+ ]);
84
105
  var CharacterAnimationModeSchema = z.enum(["template", "v3", "pro"]);
85
106
  var CharacterStateSchema = z.object({
86
107
  /** The base character, or another state, in the same style. */
@@ -239,6 +260,16 @@ var StyleObjectSchema = z.object({
239
260
  directions: z.union([z.literal(4), z.literal(8)]).optional(),
240
261
  /** `character` only. Body template: `mannequin` (default) or a quadruped (`bear`, `cat`, `dog`, `horse`, `lion`). */
241
262
  template: z.string().min(1).optional(),
263
+ /** `character` only, `standard` humanoid bases. A preset or multipliers; an asset may override it. */
264
+ proportions: CharacterProportionsSchema.optional(),
265
+ /**
266
+ * `character` only. How closely a `standard` base and a template loop
267
+ * follow their text, 1 to 20; PixelLab's default is 8. The v3 and pro
268
+ * engines do not take it.
269
+ */
270
+ textGuidanceScale: z.number().min(1).max(20).optional(),
271
+ /** `character` only. Draw `standard` bases and every loop in isometric view. */
272
+ isometric: z.boolean().optional(),
242
273
  /** Fixed seed for reproducibility where the endpoint supports it. */
243
274
  seed: z.number().int().optional(),
244
275
  /**
@@ -339,8 +370,8 @@ var InheritedStyleSchema = StyleObjectSchema.partial().extend({
339
370
  }).strict();
340
371
  var StyleInputSchema = z.union([StyleSchema, InheritedStyleSchema]);
341
372
  var AssetSchema = z.object({
342
- /** The subject. Style wrapping comes from the style's prefix/suffix. */
343
- prompt: z.string(),
373
+ /** The subject. Style wrapping comes from the style's prefix/suffix. Only a `mirror` may leave it out. */
374
+ prompt: z.string().optional(),
344
375
  /** Subdirectory under the style's outDir. Optional. */
345
376
  category: z.string().optional(),
346
377
  /** Overrides the style default. `map` generator only. */
@@ -398,6 +429,32 @@ var AssetSchema = z.object({
398
429
  state: CharacterStateSchema.optional(),
399
430
  /** `character` styles: this asset is a loop of another character asset in one direction. */
400
431
  animation: CharacterAnimationSchema.optional(),
432
+ /** `character` styles, `standard` humanoid bases: this character's proportions, over the style's. */
433
+ proportions: CharacterProportionsSchema.optional(),
434
+ /**
435
+ * `character` styles, bases only: the character's own sprite, which
436
+ * PixelLab rotates into the other directions instead of drawing from
437
+ * the prompt. A manifest-relative PNG or JPEG of the south-facing
438
+ * sprite, or an object keyed by direction (`{ "south": ..., "east":
439
+ * ... }`); quadrupeds in `standard` mode need south and east, and only
440
+ * `standard` takes more than south. `standard` wants each image at the
441
+ * style's size; v3 accepts up to 256px, pro up to 168px. The prompt
442
+ * still guides the result.
443
+ */
444
+ reference: z.union([z.string().min(1), z.record(CharacterDirectionSchema, z.string().min(1))]).optional(),
445
+ /**
446
+ * Another asset of the same style flipped left to right, made locally
447
+ * from that asset's downloaded files at no generation cost.
448
+ *
449
+ * The saving is in character loops, where each direction is its own
450
+ * generation: a walk facing west mirrored is the walk facing east, and a
451
+ * diagonal mirrors to the other diagonal. South and north mirror onto
452
+ * themselves and are refused. A single image or a frame set from any
453
+ * generator can be mirrored too; a tile set cannot, since its edges
454
+ * carry meaning. Mirroring swaps handedness, so an asymmetric character
455
+ * (a sword hand, a parted fringe) comes out the other way round.
456
+ */
457
+ mirror: z.string().min(1).optional(),
401
458
  /**
402
459
  * Generated output role to place in `cell` when this asset expands to
403
460
  * several files. Omit for ordinary single-output assets. A structural set
@@ -437,7 +494,7 @@ var AssetSchema = z.object({
437
494
  path: ["revision"]
438
495
  });
439
496
  }
440
- const shapes = [asset.revision && "revision", asset.state && "state", asset.animation && "animation"].filter(Boolean);
497
+ const shapes = [asset.revision && "revision", asset.state && "state", asset.animation && "animation", asset.mirror && "mirror"].filter(Boolean);
441
498
  if (shapes.length > 1) {
442
499
  context.addIssue({
443
500
  code: z.ZodIssueCode.custom,
@@ -445,6 +502,30 @@ var AssetSchema = z.object({
445
502
  path: [shapes[1]]
446
503
  });
447
504
  }
505
+ if (asset.mirror && (asset.source || Object.keys(asset.sourceByStyle).length)) {
506
+ context.addIssue({
507
+ code: z.ZodIssueCode.custom,
508
+ message: "a mirror is made from another asset's files; it cannot also declare a source",
509
+ path: ["mirror"]
510
+ });
511
+ }
512
+ if (asset.prompt === void 0 && !asset.mirror) {
513
+ context.addIssue({
514
+ code: z.ZodIssueCode.custom,
515
+ message: "Required",
516
+ path: ["prompt"]
517
+ });
518
+ }
519
+ if (asset.reference && (asset.state || asset.animation || asset.mirror)) {
520
+ context.addIssue({
521
+ code: z.ZodIssueCode.custom,
522
+ message: "a reference sprite belongs on a base; a state, animation, or mirror takes its look from its parent",
523
+ path: ["reference"]
524
+ });
525
+ }
526
+ if (typeof asset.reference === "object" && !Object.keys(asset.reference).length) {
527
+ context.addIssue({ code: z.ZodIssueCode.custom, message: "a reference needs at least a south image", path: ["reference"] });
528
+ }
448
529
  });
449
530
  var DEFAULT_HISTORY_LIMIT = 5;
450
531
  var HistoryLimitSchema = z.number().int().min(0).max(100);
@@ -534,6 +615,11 @@ var LockEntrySchema = z.object({
534
615
  maskSha256: z.string().regex(/^[0-9a-f]{64}$/).optional(),
535
616
  strength: z.number().min(0).max(1).optional()
536
617
  }).strict().nullable().default(null),
618
+ /** For a mirror: the asset it flips and a hash over that asset's output hashes when this was made. */
619
+ mirror: z.object({
620
+ sourceAssetId: z.string().min(1),
621
+ sourceSha256: z.string().regex(/^[0-9a-f]{64}$/)
622
+ }).strict().nullable().default(null),
537
623
  /**
538
624
  * Output hashes owned by the previous generation while its replacement is
539
625
  * pending. They authorize replacing only unchanged PixelKiln-owned files.
@@ -865,6 +951,17 @@ var PixelLabError = class extends ProviderError {
865
951
  }
866
952
  body;
867
953
  };
954
+ function proportionsPayload(proportions) {
955
+ if (typeof proportions === "string") return { type: "preset", name: proportions };
956
+ return {
957
+ type: "custom",
958
+ ...proportions.headSize !== void 0 ? { head_size: proportions.headSize } : {},
959
+ ...proportions.armsLength !== void 0 ? { arms_length: proportions.armsLength } : {},
960
+ ...proportions.legsLength !== void 0 ? { legs_length: proportions.legsLength } : {},
961
+ ...proportions.shoulderWidth !== void 0 ? { shoulder_width: proportions.shoulderWidth } : {},
962
+ ...proportions.hipWidth !== void 0 ? { hip_width: proportions.hipWidth } : {}
963
+ };
964
+ }
868
965
  var PixelLabClient = class {
869
966
  constructor(apiKey, timeoutMs = 12e4) {
870
967
  this.apiKey = apiKey;
@@ -874,9 +971,9 @@ var PixelLabClient = class {
874
971
  apiKey;
875
972
  http;
876
973
  /** Retries transport failures, 429, and 5xx; see http.ts for the policy. */
877
- async request(path32, init) {
974
+ async request(path33, init) {
878
975
  const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
879
- const res = await this.http(`${BASE}${path32}`, {
976
+ const res = await this.http(`${BASE}${path33}`, {
880
977
  ...init,
881
978
  headers: {
882
979
  Authorization: auth,
@@ -886,13 +983,13 @@ var PixelLabClient = class {
886
983
  });
887
984
  const text = await res.text();
888
985
  if (!res.ok) {
889
- throw new PixelLabError(`${init?.method ?? "GET"} ${path32} \u2192 ${res.status}`, res.status, text);
986
+ throw new PixelLabError(`${init?.method ?? "GET"} ${path33} \u2192 ${res.status}`, res.status, text);
890
987
  }
891
988
  if (!text) return {};
892
989
  try {
893
990
  return JSON.parse(text);
894
991
  } catch {
895
- throw new Error(`${init?.method ?? "GET"} ${path32} returned invalid JSON`);
992
+ throw new Error(`${init?.method ?? "GET"} ${path33} returned invalid JSON`);
896
993
  }
897
994
  }
898
995
  async balance() {
@@ -1073,14 +1170,21 @@ var PixelLabClient = class {
1073
1170
  * A base character. Standard mode picks the 4- or 8-direction template
1074
1171
  * endpoint; v3 and pro have their own and always draw 8. Every one answers
1075
1172
  * at once with the character id and a background job.
1173
+ *
1174
+ * A `reference` is the author's own sprite: standard takes one per
1175
+ * direction and draws the rest, v3 rotates the south one, and pro
1176
+ * switches to its rotate method for it. A `styleReference` is pro's style
1177
+ * anchor; the other engines have no such input.
1076
1178
  */
1077
1179
  async createCharacter(args) {
1078
1180
  const imageSize = { width: args.size, height: args.size };
1079
1181
  const palette = args.paletteSwatchBase64 ? { color_image: { type: "base64", base64: args.paletteSwatchBase64, format: "png" }, force_colors: true } : {};
1080
- let path32;
1182
+ const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
1183
+ const south = args.reference?.south;
1184
+ let path33;
1081
1185
  let body;
1082
1186
  if (args.mode === "v3") {
1083
- path32 = "/create-character-v3";
1187
+ path33 = "/create-character-v3";
1084
1188
  body = {
1085
1189
  description: args.description,
1086
1190
  image_size: imageSize,
@@ -1089,20 +1193,23 @@ var PixelLabClient = class {
1089
1193
  ...args.view ? { view: args.view } : {},
1090
1194
  ...args.outline ? { outline: args.outline } : {},
1091
1195
  ...args.detail ? { detail: args.detail } : {},
1092
- ...args.seed != null ? { seed: args.seed } : {}
1196
+ ...args.seed != null ? { seed: args.seed } : {},
1197
+ ...south ? { reference_image: encode(south) } : {}
1093
1198
  };
1094
1199
  } else if (args.mode === "pro") {
1095
- path32 = "/create-character-pro";
1200
+ path33 = "/create-character-pro";
1096
1201
  body = {
1097
1202
  description: args.description,
1098
1203
  image_size: imageSize,
1099
1204
  template_id: args.template,
1100
1205
  no_background: args.noBackground ?? true,
1206
+ method: south ? "rotate_character" : "create_with_style",
1101
1207
  ...args.view ? { view: args.view } : {},
1102
- ...args.seed != null ? { seed: args.seed } : {}
1208
+ ...args.seed != null ? { seed: args.seed } : {},
1209
+ ...south ? { reference_image: encode(south) } : args.styleReference ? { reference_image: encode(args.styleReference) } : {}
1103
1210
  };
1104
1211
  } else {
1105
- path32 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
1212
+ path33 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
1106
1213
  body = {
1107
1214
  description: args.description,
1108
1215
  image_size: imageSize,
@@ -1112,11 +1219,15 @@ var PixelLabClient = class {
1112
1219
  ...args.shading ? { shading: args.shading } : {},
1113
1220
  ...args.detail ? { detail: args.detail } : {},
1114
1221
  ...args.seed != null ? { seed: args.seed } : {},
1222
+ ...args.proportions !== void 0 ? { proportions: proportionsPayload(args.proportions) } : {},
1223
+ ...args.textGuidanceScale !== void 0 ? { text_guidance_scale: args.textGuidanceScale } : {},
1224
+ ...args.isometric !== void 0 ? { isometric: args.isometric } : {},
1225
+ ...args.reference && Object.keys(args.reference).length ? { directions: Object.fromEntries(Object.entries(args.reference).flatMap(([direction, image]) => image ? [[direction, encode(image)]] : [])) } : {},
1115
1226
  ...palette
1116
1227
  };
1117
1228
  }
1118
- const raw = await this.request(path32, { method: "POST", body: JSON.stringify(body) });
1119
- return validateResponse(CharacterSubmitSchema, raw, path32.slice(1));
1229
+ const raw = await this.request(path33, { method: "POST", body: JSON.stringify(body) });
1230
+ return validateResponse(CharacterSubmitSchema, raw, path33.slice(1));
1120
1231
  }
1121
1232
  /** A text edit of an existing character, applied to every direction and stored as a sibling. */
1122
1233
  async createCharacterState(args) {
@@ -1151,6 +1262,8 @@ var PixelLabClient = class {
1151
1262
  ...args.actionDescription ? { action_description: args.actionDescription } : {},
1152
1263
  ...args.mode === "v3" && args.frameCount ? { frame_count: args.frameCount } : {},
1153
1264
  ...args.mode === "v3" && args.keepFirstFrame === false ? { keep_first_frame: false } : {},
1265
+ ...args.mode === "template" && args.textGuidanceScale !== void 0 ? { text_guidance_scale: args.textGuidanceScale } : {},
1266
+ ...args.isometric !== void 0 ? { isometric: args.isometric } : {},
1154
1267
  ...args.seed != null ? { seed: args.seed } : {}
1155
1268
  })
1156
1269
  });
@@ -1753,6 +1866,82 @@ import { mkdirSync, existsSync, readFileSync, writeFileSync } from "fs";
1753
1866
  import { randomUUID } from "crypto";
1754
1867
  import os from "os";
1755
1868
  import path from "path";
1869
+
1870
+ // src/hash.ts
1871
+ import { createHash } from "crypto";
1872
+ import { createReadStream } from "fs";
1873
+ function sha256(data) {
1874
+ return createHash("sha256").update(data).digest("hex");
1875
+ }
1876
+ async function sha256File(path33) {
1877
+ return new Promise((resolve, reject) => {
1878
+ const hash = createHash("sha256");
1879
+ const stream = createReadStream(path33);
1880
+ stream.on("data", (chunk2) => hash.update(chunk2));
1881
+ stream.on("error", reject);
1882
+ stream.on("end", () => resolve(hash.digest("hex")));
1883
+ });
1884
+ }
1885
+ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.providerOptions, providerInputIdentity = spec.providerInputs ?? {}) {
1886
+ return sha256(
1887
+ JSON.stringify({
1888
+ // Preserve every existing PixelLab hash while making a provider switch
1889
+ // invalidate the spec. Older manifests implicitly mean pixellab.
1890
+ provider: spec.provider === "pixellab" ? void 0 : spec.provider,
1891
+ providerOptions: providerOptionIdentity && (typeof providerOptionIdentity !== "object" || Object.keys(providerOptionIdentity).length > 0) ? providerOptionIdentity : void 0,
1892
+ providerInputs: providerInputIdentity && (typeof providerInputIdentity !== "object" || Object.keys(providerInputIdentity).length > 0) ? providerInputIdentity : void 0,
1893
+ generator: spec.generator,
1894
+ prompt: spec.prompt,
1895
+ width: spec.width,
1896
+ height: spec.height,
1897
+ view: spec.view,
1898
+ outline: spec.outline ?? null,
1899
+ shading: spec.shading ?? null,
1900
+ detail: spec.detail ?? null,
1901
+ seed: spec.seed ?? null,
1902
+ palette: spec.palette,
1903
+ // `noBackground` only reaches the wire for pixflux; the tile fields are
1904
+ // undefined for every other generator. `tileSize` is intentionally
1905
+ // absent; width/height are derived from it, so it is already covered.
1906
+ noBackground: spec.generator === "pixflux" || spec.provider !== "pixellab" ? spec.noBackground : void 0,
1907
+ tileType: spec.tileType,
1908
+ tileView: spec.tileView,
1909
+ tileFeature: spec.tileFeature,
1910
+ outlineMode: spec.outlineMode,
1911
+ styleImages: styleImageHashes,
1912
+ // A character family: the engine and rotations of a base, the parent's
1913
+ // generated bytes for a state or animation (so a regenerated parent
1914
+ // makes its children stale), and each shape's own settings.
1915
+ character: spec.character ? {
1916
+ kind: spec.character.kind,
1917
+ mode: spec.character.mode,
1918
+ directions: spec.character.directions,
1919
+ template: spec.character.template,
1920
+ parent: spec.character.parentAssetId,
1921
+ parentSha256: spec.character.parentSha256 ?? null,
1922
+ state: spec.character.state,
1923
+ animation: spec.character.animation,
1924
+ // Absent keys keep the hashes of manifests that never set these.
1925
+ proportions: spec.character.proportions,
1926
+ textGuidanceScale: spec.character.textGuidanceScale,
1927
+ isometric: spec.character.isometric,
1928
+ reference: spec.character.reference ? Object.fromEntries(Object.entries(spec.character.reference).map(([direction, image]) => [direction, image.sha256])) : void 0
1929
+ } : void 0,
1930
+ // A mirror's bytes come from its source's recorded outputs; the plan
1931
+ // compares those directly, so only the choice of source is identity.
1932
+ mirror: spec.mirror ? { of: spec.mirror.sourceAssetId } : void 0,
1933
+ revision: spec.revision ? {
1934
+ mode: spec.revision.mode,
1935
+ from: spec.revision.sourceAssetId,
1936
+ sourceSha256: spec.revision.sourceSha256,
1937
+ maskSha256: spec.revision.maskSha256,
1938
+ strength: spec.revision.strength
1939
+ } : void 0
1940
+ })
1941
+ );
1942
+ }
1943
+
1944
+ // src/providers/pixellab.ts
1756
1945
  function pixelLabObjectUrl(generator, objectId) {
1757
1946
  if (!objectId) return null;
1758
1947
  if (generator === "map" || generator === "1dir") {
@@ -1875,8 +2064,8 @@ var PixelLabProvider = class _PixelLabProvider {
1875
2064
  "high detail"
1876
2065
  ]);
1877
2066
  }
1878
- if (spec.generator === "character") this.validateCharacter(spec);
1879
- if ((spec.generator === "map" || spec.generator === "pixflux" || spec.generator === "character") && styleImages.length) {
2067
+ if (spec.generator === "character") this.validateCharacter(spec, styleImages);
2068
+ if ((spec.generator === "map" || spec.generator === "pixflux") && styleImages.length) {
1880
2069
  throw new Error(`PixelLab ${spec.generator} does not support style images`);
1881
2070
  }
1882
2071
  for (const image of styleImages) {
@@ -1892,9 +2081,61 @@ var PixelLabProvider = class _PixelLabProvider {
1892
2081
  );
1893
2082
  }
1894
2083
  }
1895
- validateCharacter(spec) {
2084
+ validateCharacter(spec, styleImages = []) {
1896
2085
  const character = spec.character;
1897
2086
  if (!character) throw new Error(`${spec.styleId}/${spec.assetId} has no character shape`);
2087
+ const label = `${spec.styleId}/${spec.assetId}`;
2088
+ if (character.kind === "base" && character.proportions !== void 0) {
2089
+ if (character.mode !== "standard") {
2090
+ throw new Error(`${label}: proportions apply to standard bases; the ${character.mode} engine has none to set`);
2091
+ }
2092
+ if (character.template !== "mannequin") {
2093
+ throw new Error(`${label}: proportions apply to the mannequin template; ${character.template} is a quadruped`);
2094
+ }
2095
+ }
2096
+ if (character.reference) {
2097
+ const given = Object.keys(character.reference);
2098
+ if (!character.reference.south) throw new Error(`${label}: a reference needs a south-facing sprite`);
2099
+ if (character.mode !== "standard") {
2100
+ if (given.length > 1) throw new Error(`${label}: PixelLab ${character.mode} rotates one south-facing reference; drop the other directions`);
2101
+ const limit = character.mode === "v3" ? 256 : 168;
2102
+ const south = character.reference.south;
2103
+ if (south.width > limit || south.height > limit) {
2104
+ throw new Error(`${label}: reference is ${south.width}x${south.height}; PixelLab ${character.mode} takes up to ${limit}px`);
2105
+ }
2106
+ } else {
2107
+ const allowed = character.directions === 4 ? CHARACTER_DIRECTIONS_4 : CHARACTER_DIRECTIONS_8;
2108
+ for (const direction of given) {
2109
+ if (!allowed.includes(direction)) {
2110
+ throw new Error(`${label}: reference "${direction}" is not one of this character's ${character.directions} directions`);
2111
+ }
2112
+ const image = character.reference[direction];
2113
+ if (image.width !== spec.width || image.height !== spec.height) {
2114
+ throw new Error(
2115
+ `${label}: reference (${direction}) is ${image.width}x${image.height}; standard mode wants each image at the style's size, ${spec.width}x${spec.height}`
2116
+ );
2117
+ }
2118
+ }
2119
+ if (character.template !== "mannequin" && !character.reference.east) {
2120
+ throw new Error(`${label}: a ${character.template} reference needs south and east images`);
2121
+ }
2122
+ }
2123
+ }
2124
+ if (styleImages.length) {
2125
+ if (character.mode !== "pro") {
2126
+ throw new Error(
2127
+ `PixelLab ${character.mode} characters take no style images; give a base its own sprite with \`reference\`, or use mode pro for a style anchor`
2128
+ );
2129
+ }
2130
+ if (styleImages.length > 1) throw new Error("PixelLab pro characters take one style image");
2131
+ if (character.kind === "base" && character.reference) {
2132
+ throw new Error(`${label}: a pro base rotating its own reference takes no style image`);
2133
+ }
2134
+ const image = styleImages[0];
2135
+ if (image.width > 168 || image.height > 168) {
2136
+ throw new Error(`PixelLab pro character style image is ${image.width}x${image.height}; the limit is 168px`);
2137
+ }
2138
+ }
1898
2139
  const maxSize = character.mode === "v3" ? 256 : 128;
1899
2140
  if (spec.width < 16 || spec.height < 16 || spec.width > maxSize || spec.height > maxSize) {
1900
2141
  throw new Error(`PixelLab ${character.mode} characters must be between 16 and ${maxSize} pixels`);
@@ -1930,10 +2171,11 @@ var PixelLabProvider = class _PixelLabProvider {
1930
2171
  * asset so a later run can find it, and clears its own earlier take for
1931
2172
  * that direction first, since PixelLab skips a direction that exists.
1932
2173
  */
1933
- async submitCharacter(spec, context) {
2174
+ async submitCharacter(spec, styleImages, context) {
1934
2175
  const character = spec.character;
1935
2176
  if (character.kind === "base") {
1936
2177
  const swatch = spec.palette.length && character.mode === "standard" ? paletteSwatch(spec.palette).toString("base64") : void 0;
2178
+ const styleImage = styleImages[0];
1937
2179
  const res2 = await this.client.createCharacter({
1938
2180
  mode: character.mode,
1939
2181
  description: spec.prompt,
@@ -1946,7 +2188,12 @@ var PixelLabProvider = class _PixelLabProvider {
1946
2188
  detail: spec.detail,
1947
2189
  seed: spec.seed,
1948
2190
  noBackground: spec.noBackground,
1949
- paletteSwatchBase64: swatch
2191
+ paletteSwatchBase64: swatch,
2192
+ proportions: character.proportions,
2193
+ textGuidanceScale: character.textGuidanceScale,
2194
+ isometric: character.isometric,
2195
+ reference: character.reference ? readReference(spec, character.reference) : void 0,
2196
+ styleReference: styleImage ? { base64: styleImage.base64, format: styleImage.format } : void 0
1950
2197
  });
1951
2198
  return { jobId: res2.character_id, metadata: { character: { kind: "base", characterId: res2.character_id, mode: character.mode, directions: character.directions, backgroundJobId: res2.background_job_id } } };
1952
2199
  }
@@ -1988,7 +2235,9 @@ var PixelLabProvider = class _PixelLabProvider {
1988
2235
  frameCount: animation.frames,
1989
2236
  keepFirstFrame: animation.keepFirstFrame,
1990
2237
  directions: [animation.direction],
1991
- seed: spec.seed
2238
+ seed: spec.seed,
2239
+ textGuidanceScale: character.textGuidanceScale,
2240
+ isometric: character.isometric
1992
2241
  });
1993
2242
  const job = { characterId: parentId, name, direction: animation.direction, jobIds: res.background_job_ids };
1994
2243
  return {
@@ -2078,7 +2327,7 @@ var PixelLabProvider = class _PixelLabProvider {
2078
2327
  }
2079
2328
  async submit(spec, styleImages, context) {
2080
2329
  this.validate(spec, styleImages);
2081
- if (spec.generator === "character") return this.submitCharacter(spec, context);
2330
+ if (spec.generator === "character") return this.submitCharacter(spec, styleImages, context);
2082
2331
  if (spec.generator === "pixflux") {
2083
2332
  const swatch = spec.palette.length ? paletteSwatch(spec.palette).toString("base64") : void 0;
2084
2333
  const { png } = await this.client.createImagePixflux({
@@ -2345,6 +2594,17 @@ function firstUrl(urls) {
2345
2594
  if (!urls) return null;
2346
2595
  return Object.values(urls).find((u) => typeof u === "string") ?? null;
2347
2596
  }
2597
+ function readReference(spec, reference) {
2598
+ const images = {};
2599
+ for (const [direction, image] of Object.entries(reference)) {
2600
+ const bytes = readFileSync(image.path);
2601
+ if (sha256(bytes) !== image.sha256) {
2602
+ throw new Error(`${spec.styleId}/${spec.assetId}: reference (${direction}) changed after the manifest was resolved: ${image.path}`);
2603
+ }
2604
+ images[direction] = { base64: bytes.toString("base64"), format: image.format };
2605
+ }
2606
+ return images;
2607
+ }
2348
2608
  function rotationSources(character) {
2349
2609
  const order = character.directions === 4 ? CHARACTER_DIRECTIONS_4 : CHARACTER_DIRECTIONS_8;
2350
2610
  const sources = [];
@@ -2415,9 +2675,9 @@ var RetroDiffusionClient = class {
2415
2675
  if (!response.ok) throw new ProviderError("retrodiffusion", `Retro Diffusion download failed (${response.status})`, { status: response.status });
2416
2676
  return Buffer.from(await response.arrayBuffer());
2417
2677
  }
2418
- async call(path32, init = {}) {
2678
+ async call(path33, init = {}) {
2419
2679
  if (!this.token) throw new Error("RD_API_KEY is not set");
2420
- const response = await this.request(`${this.baseUrl}${path32}`, {
2680
+ const response = await this.request(`${this.baseUrl}${path33}`, {
2421
2681
  ...init,
2422
2682
  headers: {
2423
2683
  "Content-Type": "application/json",
@@ -3216,74 +3476,6 @@ function redactScenarioError(value, secrets) {
3216
3476
  import { randomUUID as randomUUID2 } from "crypto";
3217
3477
  import { readFile } from "fs/promises";
3218
3478
  import path2 from "path";
3219
-
3220
- // src/hash.ts
3221
- import { createHash } from "crypto";
3222
- import { createReadStream } from "fs";
3223
- function sha256(data) {
3224
- return createHash("sha256").update(data).digest("hex");
3225
- }
3226
- async function sha256File(path32) {
3227
- return new Promise((resolve, reject) => {
3228
- const hash = createHash("sha256");
3229
- const stream = createReadStream(path32);
3230
- stream.on("data", (chunk2) => hash.update(chunk2));
3231
- stream.on("error", reject);
3232
- stream.on("end", () => resolve(hash.digest("hex")));
3233
- });
3234
- }
3235
- function specHash(spec, styleImageHashes, providerOptionIdentity = spec.providerOptions, providerInputIdentity = spec.providerInputs ?? {}) {
3236
- return sha256(
3237
- JSON.stringify({
3238
- // Preserve every existing PixelLab hash while making a provider switch
3239
- // invalidate the spec. Older manifests implicitly mean pixellab.
3240
- provider: spec.provider === "pixellab" ? void 0 : spec.provider,
3241
- providerOptions: providerOptionIdentity && (typeof providerOptionIdentity !== "object" || Object.keys(providerOptionIdentity).length > 0) ? providerOptionIdentity : void 0,
3242
- providerInputs: providerInputIdentity && (typeof providerInputIdentity !== "object" || Object.keys(providerInputIdentity).length > 0) ? providerInputIdentity : void 0,
3243
- generator: spec.generator,
3244
- prompt: spec.prompt,
3245
- width: spec.width,
3246
- height: spec.height,
3247
- view: spec.view,
3248
- outline: spec.outline ?? null,
3249
- shading: spec.shading ?? null,
3250
- detail: spec.detail ?? null,
3251
- seed: spec.seed ?? null,
3252
- palette: spec.palette,
3253
- // `noBackground` only reaches the wire for pixflux; the tile fields are
3254
- // undefined for every other generator. `tileSize` is intentionally
3255
- // absent; width/height are derived from it, so it is already covered.
3256
- noBackground: spec.generator === "pixflux" || spec.provider !== "pixellab" ? spec.noBackground : void 0,
3257
- tileType: spec.tileType,
3258
- tileView: spec.tileView,
3259
- tileFeature: spec.tileFeature,
3260
- outlineMode: spec.outlineMode,
3261
- styleImages: styleImageHashes,
3262
- // A character family: the engine and rotations of a base, the parent's
3263
- // generated bytes for a state or animation (so a regenerated parent
3264
- // makes its children stale), and each shape's own settings.
3265
- character: spec.character ? {
3266
- kind: spec.character.kind,
3267
- mode: spec.character.mode,
3268
- directions: spec.character.directions,
3269
- template: spec.character.template,
3270
- parent: spec.character.parentAssetId,
3271
- parentSha256: spec.character.parentSha256 ?? null,
3272
- state: spec.character.state,
3273
- animation: spec.character.animation
3274
- } : void 0,
3275
- revision: spec.revision ? {
3276
- mode: spec.revision.mode,
3277
- from: spec.revision.sourceAssetId,
3278
- sourceSha256: spec.revision.sourceSha256,
3279
- maskSha256: spec.revision.maskSha256,
3280
- strength: spec.revision.strength
3281
- } : void 0
3282
- })
3283
- );
3284
- }
3285
-
3286
- // src/providers/comfyui.ts
3287
3479
  var DEFAULT_BASE_URL3 = "http://127.0.0.1:8188";
3288
3480
  var BUILTIN_BINDINGS = /* @__PURE__ */ new Set([
3289
3481
  "prompt",
@@ -4234,6 +4426,18 @@ var FakeProvider = class {
4234
4426
  const objectId = job.objectId ?? `obj-${jobId}`;
4235
4427
  job.objectId = objectId;
4236
4428
  this.register(objectId, job.spec);
4429
+ const character = job.spec.character;
4430
+ if (character) {
4431
+ const roles = character.kind === "animation" ? Array.from({ length: character.animation.frames }, (_, i) => `frame-${String(i).padStart(2, "0")}`) : character.directions === 4 ? CHARACTER_DIRECTIONS_4 : CHARACTER_DIRECTIONS_8;
4432
+ const sources = roles.map((role) => ({ url: `fake://${objectId}/${role}.png`, role }));
4433
+ return {
4434
+ status: "ready",
4435
+ objectId,
4436
+ sourceUrl: sources[0].url,
4437
+ sources,
4438
+ ...character.kind === "animation" ? { metadata: { frameSet: { fps: character.animation.fps, count: roles.length } } } : {}
4439
+ };
4440
+ }
4237
4441
  const sourceUrl = `fake://${objectId}.png`;
4238
4442
  return { status: "ready", objectId, sourceUrl, sources: [{ url: sourceUrl }] };
4239
4443
  }
@@ -4898,16 +5102,16 @@ async function resolveSpecs(loaded, filter) {
4898
5102
  }
4899
5103
  }
4900
5104
  const styleImageCache = /* @__PURE__ */ new Map();
4901
- async function loadStyleImage(rel) {
5105
+ async function loadStyleImage(rel, what = "Style image") {
4902
5106
  const abs = path5.resolve(root, rel);
4903
5107
  let hit = styleImageCache.get(abs);
4904
5108
  if (!hit) {
4905
- if (!existsSync4(abs)) throw new Error(`Style image not found: ${abs}`);
5109
+ if (!existsSync4(abs)) throw new Error(`${what} not found: ${abs}`);
4906
5110
  const buf = await readFile3(abs);
4907
5111
  const metadata = imageMetadata(buf);
4908
- if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${abs}`);
5112
+ if (!metadata) throw new Error(`${what} is not a readable PNG or JPEG: ${abs}`);
4909
5113
  if (metadata.width < 1 || metadata.height < 1) {
4910
- throw new Error(`Style image has invalid dimensions: ${abs}`);
5114
+ throw new Error(`${what} has invalid dimensions: ${abs}`);
4911
5115
  }
4912
5116
  hit = { base64: buf.toString("base64"), hash: sha256(buf), ...metadata };
4913
5117
  styleImageCache.set(abs, hit);
@@ -4982,7 +5186,7 @@ async function resolveSpecs(loaded, filter) {
4982
5186
  );
4983
5187
  }
4984
5188
  const characterKind = asset.animation ? "animation" : asset.state ? "state" : "base";
4985
- const subject = asset.promptByStyle[styleId] ?? asset.prompt;
5189
+ const subject = asset.promptByStyle[styleId] ?? asset.prompt ?? "";
4986
5190
  const prompt = generator === "character" && characterKind !== "base" ? subject.trim() : [style.promptPrefix, subject, style.promptSuffix].map((p) => p.trim()).filter(Boolean).join(", ");
4987
5191
  const relFile = asset.file ?? path5.join(asset.category ?? "", `${assetId}.png`);
4988
5192
  const outFile = path5.resolve(root, style.outDir, relFile);
@@ -5014,7 +5218,7 @@ async function resolveSpecs(loaded, filter) {
5014
5218
  tileView: generator === "tiles" ? style.tileView : void 0,
5015
5219
  tileFeature: generator === "tiles" ? style.tileFeature : void 0,
5016
5220
  outlineMode: generator === "tiles" ? style.outlineMode : void 0,
5017
- ...generator === "character" ? { character: resolveCharacterShape(asset, style, characterKind) } : {},
5221
+ ...generator === "character" ? { character: await resolveCharacterShape(asset, style, characterKind, { root, load: loadStyleImage }) } : {},
5018
5222
  cost: generator === "tiles" ? tilesCost(tileSize, tileVariations) : generationCost(width, height, generator),
5019
5223
  costUnit: "generations",
5020
5224
  candidates: generator === "tiles" ? tileVariations : generator === "1dir" ? candidateCount(size) : 1
@@ -5057,11 +5261,42 @@ async function resolveSpecs(loaded, filter) {
5057
5261
  const resolved = styleSpecs.get(assetId);
5058
5262
  if (!resolved) {
5059
5263
  throw new Error(
5060
- `Asset "${assetId}" is not available in style "${styleId}"; revision parents must participate in the same style.`
5264
+ `Asset "${assetId}" is not available in style "${styleId}"; revision parents, character parents, and mirror sources must be in the same style.`
5061
5265
  );
5062
5266
  }
5063
5267
  if (finalized.has(assetId)) return resolved;
5064
5268
  const asset = manifest.assets[assetId];
5269
+ if (asset.mirror) {
5270
+ if (asset.mirror === assetId) throw new Error(`assets.${assetId}: an asset cannot mirror itself`);
5271
+ const sourceSpec = await finalize(asset.mirror);
5272
+ if (sourceSpec.generator === "tiles") {
5273
+ throw new Error(`assets.${assetId}: a tile set cannot be mirrored; its edges carry meaning`);
5274
+ }
5275
+ if (sourceSpec.character?.kind === "animation") {
5276
+ const facing = sourceSpec.character.animation.direction;
5277
+ const mirrored = MIRRORED_DIRECTION[facing];
5278
+ if (mirrored === facing) {
5279
+ throw new Error(
5280
+ `assets.${assetId}: mirroring ${asset.mirror} gives another ${facing}-facing loop; mirror a loop facing east, west, or a diagonal`
5281
+ );
5282
+ }
5283
+ resolved.character = { ...sourceSpec.character, animation: { ...sourceSpec.character.animation, direction: mirrored } };
5284
+ } else if (sourceSpec.character) {
5285
+ resolved.character = { ...sourceSpec.character };
5286
+ }
5287
+ resolved.mirror = { sourceAssetId: asset.mirror, sourceSpec };
5288
+ resolved.generator = sourceSpec.generator;
5289
+ resolved.width = sourceSpec.width;
5290
+ resolved.height = sourceSpec.height;
5291
+ resolved.size = sourceSpec.size;
5292
+ resolved.prompt = "";
5293
+ resolved.specHash = specHash(resolved, styleImageHashes, providerOptionIdentity, providerInputIdentities.get(assetId));
5294
+ resolved.cost = 0;
5295
+ resolved.costUnit = sourceSpec.costUnit;
5296
+ resolved.candidates = 1;
5297
+ finalized.add(assetId);
5298
+ return resolved;
5299
+ }
5065
5300
  const characterParent = asset.state?.of ?? asset.animation?.of;
5066
5301
  if (resolved.character && characterParent) {
5067
5302
  const parentSpec = await finalize(characterParent);
@@ -5228,15 +5463,33 @@ function imageMetadata(buf) {
5228
5463
  }
5229
5464
  return null;
5230
5465
  }
5231
- function resolveCharacterShape(asset, style, kind) {
5466
+ async function resolveCharacterShape(asset, style, kind, files) {
5232
5467
  const mode2 = style.mode ?? "standard";
5233
5468
  const directions = mode2 === "standard" ? style.directions ?? 8 : 8;
5469
+ const proportions = asset.proportions ?? style.proportions;
5234
5470
  const shape = {
5235
5471
  kind,
5236
5472
  mode: mode2,
5237
5473
  directions,
5238
- template: style.template ?? "mannequin"
5474
+ template: style.template ?? "mannequin",
5475
+ ...proportions !== void 0 ? { proportions } : {},
5476
+ ...style.textGuidanceScale !== void 0 ? { textGuidanceScale: style.textGuidanceScale } : {},
5477
+ ...style.isometric !== void 0 ? { isometric: style.isometric } : {}
5239
5478
  };
5479
+ if (asset.reference) {
5480
+ const byDirection = typeof asset.reference === "string" ? { south: asset.reference } : asset.reference;
5481
+ shape.reference = {};
5482
+ for (const [direction, rel] of Object.entries(byDirection)) {
5483
+ const image = await files.load(rel, `Reference sprite (${direction})`);
5484
+ shape.reference[direction] = {
5485
+ path: path5.resolve(files.root, rel),
5486
+ sha256: image.hash,
5487
+ width: image.width,
5488
+ height: image.height,
5489
+ format: image.format
5490
+ };
5491
+ }
5492
+ }
5240
5493
  if (asset.state) {
5241
5494
  shape.state = {
5242
5495
  paletteFromReference: asset.state.paletteFromReference,
@@ -5261,7 +5514,7 @@ function characterDirections(directions) {
5261
5514
  }
5262
5515
 
5263
5516
  // src/project.ts
5264
- import path12 from "path";
5517
+ import path13 from "path";
5265
5518
 
5266
5519
  // src/env.ts
5267
5520
  import { readFileSync as readFileSync2, existsSync as existsSync5 } from "fs";
@@ -5309,23 +5562,225 @@ function parseEnv(contents) {
5309
5562
 
5310
5563
  // src/pipeline/plan.ts
5311
5564
  import { existsSync as existsSync8 } from "fs";
5312
- import path11 from "path";
5565
+ import path12 from "path";
5566
+
5567
+ // src/pipeline/mirror.ts
5568
+ import { mkdir as mkdir2, readFile as readFile4, rename as rename2, writeFile as writeFile2 } from "fs/promises";
5569
+ import path7 from "path";
5570
+
5571
+ // src/palette.ts
5572
+ function normalizePalette(values) {
5573
+ const colors = /* @__PURE__ */ new Map();
5574
+ for (const value of values) {
5575
+ const rgb = parseHex(value);
5576
+ const hex2 = `#${rgb.r.toString(16).padStart(2, "0")}${rgb.g.toString(16).padStart(2, "0")}${rgb.b.toString(16).padStart(2, "0")}`;
5577
+ if (!colors.has(hex2)) colors.set(hex2, { hex: hex2, ...rgb });
5578
+ }
5579
+ if (colors.size < 2 || colors.size > 256) {
5580
+ throw new Error(`A palette must contain 2\u2013256 unique colors; got ${colors.size}.`);
5581
+ }
5582
+ return [...colors.values()];
5583
+ }
5584
+ function colorDistance(a, b) {
5585
+ const rmean = (a.r + b.r) / 2;
5586
+ const dr = a.r - b.r;
5587
+ const dg = a.g - b.g;
5588
+ const db = a.b - b.b;
5589
+ return Math.sqrt(
5590
+ (512 + rmean) * dr * dr / 256 + 4 * dg * dg + (767 - rmean) * db * db / 256
5591
+ );
5592
+ }
5593
+ function quantize(png, palette) {
5594
+ const colors = normalizePalette(palette);
5595
+ const pixels = Buffer.from(png.pixels);
5596
+ const nearestOf = /* @__PURE__ */ new Map();
5597
+ let changed = 0;
5598
+ for (let i = 0; i < pixels.length; i += 4) {
5599
+ const alpha = pixels[i + 3];
5600
+ if (alpha === 0) {
5601
+ if (pixels[i] || pixels[i + 1] || pixels[i + 2]) changed++;
5602
+ pixels[i] = 0;
5603
+ pixels[i + 1] = 0;
5604
+ pixels[i + 2] = 0;
5605
+ continue;
5606
+ }
5607
+ const key = pixels[i] << 16 | pixels[i + 1] << 8 | pixels[i + 2];
5608
+ let nearest = nearestOf.get(key);
5609
+ if (!nearest) {
5610
+ const pixel = { r: pixels[i], g: pixels[i + 1], b: pixels[i + 2] };
5611
+ nearest = colors[0];
5612
+ let distance = colorDistance(pixel, nearest);
5613
+ for (let j = 1; j < colors.length; j++) {
5614
+ const candidate = colors[j];
5615
+ const candidateDistance = colorDistance(pixel, candidate);
5616
+ if (candidateDistance < distance) {
5617
+ nearest = candidate;
5618
+ distance = candidateDistance;
5619
+ }
5620
+ }
5621
+ nearestOf.set(key, nearest);
5622
+ }
5623
+ if (pixels[i] !== nearest.r || pixels[i + 1] !== nearest.g || pixels[i + 2] !== nearest.b) changed++;
5624
+ pixels[i] = nearest.r;
5625
+ pixels[i + 1] = nearest.g;
5626
+ pixels[i + 2] = nearest.b;
5627
+ }
5628
+ return { png: { width: png.width, height: png.height, pixels }, changed };
5629
+ }
5630
+ function quantizeToPalette(png, palette) {
5631
+ return quantize(png, palette).png;
5632
+ }
5633
+
5634
+ // src/pipeline/postprocess.ts
5635
+ function postprocessFor(spec) {
5636
+ if (!spec.enforcePalette || spec.palette.length < 2) return void 0;
5637
+ return {
5638
+ palette: {
5639
+ colors: normalizePalette(spec.palette).map((color) => color.hex),
5640
+ dither: "none",
5641
+ distance: "redmean"
5642
+ }
5643
+ };
5644
+ }
5645
+ function samePostprocess(a, b) {
5646
+ const pa = a?.palette;
5647
+ const pb = b?.palette;
5648
+ if (!pa && !pb) return true;
5649
+ if (!pa || !pb) return false;
5650
+ return pa.dither === pb.dither && pa.distance === pb.distance && pa.colors.length === pb.colors.length && pa.colors.every((color, i) => color === pb.colors[i]);
5651
+ }
5652
+ function postprocessCurrent(entry, spec) {
5653
+ return samePostprocess(entry.postprocess, postprocessFor(spec));
5654
+ }
5655
+ function applyPostprocess(bytes, mediaType, record) {
5656
+ if (!record?.palette || mediaType !== MediaType.PNG) return { bytes, changed: false };
5657
+ const png = decodePng(bytes);
5658
+ const { png: snapped, changed } = quantize(png, record.palette.colors);
5659
+ if (!changed) return { bytes, changed: false };
5660
+ return { bytes: encodeRgbaPng(snapped.width, snapped.height, snapped.pixels), changed: true };
5661
+ }
5662
+
5663
+ // src/pipeline/mirror.ts
5664
+ function mirrorSourceHash(source) {
5665
+ return sha256(JSON.stringify(source.outputs.map((output) => output.sha256)));
5666
+ }
5667
+ function mirrorCurrent(entry, source) {
5668
+ return Boolean(entry.mirror && source && entry.mirror.sourceSha256 === mirrorSourceHash(source));
5669
+ }
5670
+ function mirroredRole(role) {
5671
+ if (role && role in MIRRORED_DIRECTION) return MIRRORED_DIRECTION[role];
5672
+ return role;
5673
+ }
5674
+ function flipHorizontal(png) {
5675
+ const { width, height, pixels } = decodePng(png);
5676
+ const flipped = Buffer.alloc(pixels.length);
5677
+ const stride = width * 4;
5678
+ for (let y = 0; y < height; y++) {
5679
+ const row = y * stride;
5680
+ for (let x = 0; x < width; x++) {
5681
+ pixels.copy(flipped, row + (width - 1 - x) * 4, row + x * 4, row + x * 4 + 4);
5682
+ }
5683
+ }
5684
+ return encodeRgbaPng(width, height, flipped);
5685
+ }
5686
+ async function deriveMirror(spec, lock, lockPath, opts = {}) {
5687
+ const log = opts.onProgress ?? (() => {
5688
+ });
5689
+ const mirror = spec.mirror;
5690
+ const key = lockKey(spec.styleId, spec.assetId);
5691
+ const sourceKey = lockKey(spec.styleId, mirror.sourceAssetId);
5692
+ const source = lock.entries[sourceKey];
5693
+ if (!source || source.status !== "downloaded" || !source.outputs.length) {
5694
+ throw new Error(`mirror source ${sourceKey} has no downloaded outputs`);
5695
+ }
5696
+ const members = source.outputs.map((output, index) => ({
5697
+ file: currentEntryOutputPath(source, mirror.sourceSpec, index),
5698
+ role: mirroredRole(output.role)
5699
+ }));
5700
+ if (members.every((member) => member.role && CHARACTER_DIRECTIONS_8.includes(member.role))) {
5701
+ members.sort((a, b) => CHARACTER_DIRECTIONS_8.indexOf(a.role) - CHARACTER_DIRECTIONS_8.indexOf(b.role));
5702
+ }
5703
+ const outputs = [];
5704
+ const written = [];
5705
+ for (const [index, member] of members.entries()) {
5706
+ const bytes = flipHorizontal(await readFile4(member.file));
5707
+ const target = expectedOutputPath(spec, member.role, index, members.length, MediaType.PNG);
5708
+ await mkdir2(path7.dirname(target), { recursive: true });
5709
+ const tmp = `${target}.pixelkiln.tmp`;
5710
+ await writeFile2(tmp, bytes);
5711
+ await rename2(tmp, target);
5712
+ written.push(target);
5713
+ outputs.push({
5714
+ path: portableOutputPath(target, spec.root),
5715
+ sha256: sha256(bytes),
5716
+ ...member.role ? { role: member.role } : {},
5717
+ mediaType: MediaType.PNG
5718
+ });
5719
+ }
5720
+ const sourceMetadata = source.providerMetadata?.[source.provider] ?? {};
5721
+ const character = spec.character;
5722
+ const entry = upsert(lock, key, {
5723
+ styleId: spec.styleId,
5724
+ assetId: spec.assetId,
5725
+ specHash: spec.specHash,
5726
+ generator: spec.generator,
5727
+ tileFeature: null,
5728
+ prompt: "",
5729
+ width: spec.width,
5730
+ height: spec.height,
5731
+ revision: null,
5732
+ mirror: { sourceAssetId: mirror.sourceAssetId, sourceSha256: mirrorSourceHash(source) },
5733
+ supersededOutputs: [],
5734
+ jobId: null,
5735
+ submissionComplete: void 0,
5736
+ reviewObjectId: null,
5737
+ objectId: null,
5738
+ candidateIndex: null,
5739
+ status: "downloaded",
5740
+ error: null,
5741
+ outputs,
5742
+ providerMetadata: {
5743
+ [source.provider]: {
5744
+ ...sourceMetadata.frameSet ? { frameSet: sourceMetadata.frameSet } : {},
5745
+ ...character ? {
5746
+ character: {
5747
+ kind: character.kind,
5748
+ mirrorOf: mirror.sourceAssetId,
5749
+ ...character.animation ? { direction: character.animation.direction } : {}
5750
+ }
5751
+ } : {}
5752
+ }
5753
+ },
5754
+ sourceUrl: null,
5755
+ sourceUrls: [],
5756
+ submittedAt: (/* @__PURE__ */ new Date()).toISOString(),
5757
+ downloadedAt: (/* @__PURE__ */ new Date()).toISOString(),
5758
+ history: [],
5759
+ cost: 0,
5760
+ costUnit: spec.costUnit,
5761
+ provider: source.provider,
5762
+ postprocess: postprocessFor(spec)
5763
+ });
5764
+ await saveLock(lockPath, lock);
5765
+ log(` mirror ${key} \u2190 ${sourceKey} (${written.length} file${written.length === 1 ? "" : "s"} flipped, no generation cost)`);
5766
+ return entry;
5767
+ }
5313
5768
 
5314
5769
  // src/pipeline/quality-profile.ts
5315
5770
  import { existsSync as existsSync6 } from "fs";
5316
- import path9 from "path";
5771
+ import path10 from "path";
5317
5772
 
5318
5773
  // src/pipeline/refine.ts
5319
- import path8 from "path";
5774
+ import path9 from "path";
5320
5775
  import { execFile } from "child_process";
5321
- import { mkdtemp, readFile as readFile5, rm as rm3 } from "fs/promises";
5776
+ import { mkdtemp, readFile as readFile6, rm as rm3 } from "fs/promises";
5322
5777
  import { tmpdir } from "os";
5323
5778
  import { promisify } from "util";
5324
5779
 
5325
5780
  // src/artifacts.ts
5326
- import path7 from "path";
5781
+ import path8 from "path";
5327
5782
  import { createHash as createHash3, randomUUID as randomUUID3 } from "crypto";
5328
- import { mkdir as mkdir2, readFile as readFile4, rename as rename2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
5783
+ import { mkdir as mkdir3, readFile as readFile5, rename as rename3, rm as rm2, writeFile as writeFile3 } from "fs/promises";
5329
5784
  var activeTransactions = /* @__PURE__ */ new Set();
5330
5785
  function message(error) {
5331
5786
  return error instanceof Error ? error.message : String(error);
@@ -5334,7 +5789,7 @@ function digest(data) {
5334
5789
  return createHash3("sha256").update(data).digest("hex");
5335
5790
  }
5336
5791
  function portableRelative(from, to) {
5337
- return path7.relative(from, path7.resolve(to)).split(path7.sep).join("/") || ".";
5792
+ return path8.relative(from, path8.resolve(to)).split(path8.sep).join("/") || ".";
5338
5793
  }
5339
5794
  function canonical2(value) {
5340
5795
  if (value === null || typeof value === "string" || typeof value === "boolean") return value;
@@ -5384,12 +5839,12 @@ function parseArtifactManifest(absolute, data) {
5384
5839
  return raw;
5385
5840
  }
5386
5841
  async function readArtifactBundleManifest(manifestPath) {
5387
- const absolute = path7.resolve(manifestPath);
5388
- return parseArtifactManifest(absolute, await readFile4(absolute, "utf8"));
5842
+ const absolute = path8.resolve(manifestPath);
5843
+ return parseArtifactManifest(absolute, await readFile5(absolute, "utf8"));
5389
5844
  }
5390
5845
  async function readOptional(file) {
5391
5846
  try {
5392
- return await readFile4(file);
5847
+ return await readFile5(file);
5393
5848
  } catch (error) {
5394
5849
  if (isCode(error, "ENOENT")) return null;
5395
5850
  throw error;
@@ -5408,8 +5863,8 @@ function processIsAlive(pid) {
5408
5863
  }
5409
5864
  }
5410
5865
  function validTemporaryPath(candidate, destination, type) {
5411
- return path7.dirname(candidate) === path7.dirname(destination) && path7.basename(candidate).startsWith(
5412
- `.${path7.basename(destination)}.pixelkiln-${type}-`
5866
+ return path8.dirname(candidate) === path8.dirname(destination) && path8.basename(candidate).startsWith(
5867
+ `.${path8.basename(destination)}.pixelkiln-${type}-`
5413
5868
  );
5414
5869
  }
5415
5870
  function parseTransaction(journal, data) {
@@ -5429,7 +5884,7 @@ async function removeTransactionFiles(journal) {
5429
5884
  await rm2(transactionMarker(journal), { force: true });
5430
5885
  }
5431
5886
  async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
5432
- const journal = path7.resolve(recoveryFile);
5887
+ const journal = path8.resolve(recoveryFile);
5433
5888
  const bytes = await readOptional(journal);
5434
5889
  if (!bytes) {
5435
5890
  await rm2(transactionMarker(journal), { force: true });
@@ -5445,7 +5900,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
5445
5900
  );
5446
5901
  }
5447
5902
  for (const entry of transaction.entries) {
5448
- if (typeof entry.destination !== "string" || typeof entry.stage !== "string" || typeof entry.sha256 !== "string" || entry.backup !== void 0 && typeof entry.backup !== "string" || !allowedDestinations.has(path7.resolve(entry.destination)) || !validTemporaryPath(entry.stage, entry.destination, "stage") || entry.backup !== void 0 && !validTemporaryPath(entry.backup, entry.destination, "backup")) {
5903
+ if (typeof entry.destination !== "string" || typeof entry.stage !== "string" || typeof entry.sha256 !== "string" || entry.backup !== void 0 && typeof entry.backup !== "string" || !allowedDestinations.has(path8.resolve(entry.destination)) || !validTemporaryPath(entry.stage, entry.destination, "stage") || entry.backup !== void 0 && !validTemporaryPath(entry.backup, entry.destination, "backup")) {
5449
5904
  throw new Error(
5450
5905
  `Refusing unsafe artifact recovery from ${journal}; its destinations or temporary paths do not match the current bundle.`
5451
5906
  );
@@ -5471,7 +5926,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
5471
5926
  }
5472
5927
  await remove2(entry.destination, errors);
5473
5928
  try {
5474
- await rename2(entry.backup, entry.destination);
5929
+ await rename3(entry.backup, entry.destination);
5475
5930
  } catch (error) {
5476
5931
  errors.push(
5477
5932
  `could not restore ${entry.destination}; previous file remains at ${entry.backup}: ${message(error)}`
@@ -5491,7 +5946,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
5491
5946
  await removeTransactionFiles(journal);
5492
5947
  }
5493
5948
  function createArtifactBundleManifest(manifestPath, outputs, provenance) {
5494
- const root = path7.dirname(path7.resolve(manifestPath));
5949
+ const root = path8.dirname(path8.resolve(manifestPath));
5495
5950
  const sources = provenance.sources.map((source) => ({
5496
5951
  ...source,
5497
5952
  path: portableRelative(root, source.path)
@@ -5521,11 +5976,11 @@ function withArtifactManifest(manifestPath, outputs, provenance) {
5521
5976
  ];
5522
5977
  }
5523
5978
  async function writeManagedArtifactBundle(manifestPath, outputs, provenance, options = {}) {
5524
- const absoluteManifest = path7.resolve(manifestPath);
5979
+ const absoluteManifest = path8.resolve(manifestPath);
5525
5980
  const recoveryFile = `${absoluteManifest}.transaction`;
5526
5981
  const allowedDestinations = /* @__PURE__ */ new Set([
5527
5982
  absoluteManifest,
5528
- ...outputs.map((output) => path7.resolve(output.path))
5983
+ ...outputs.map((output) => path8.resolve(output.path))
5529
5984
  ]);
5530
5985
  await recoverArtifactTransaction(recoveryFile, allowedDestinations);
5531
5986
  const existingManifestBytes = await readOptional(absoluteManifest);
@@ -5543,13 +5998,13 @@ async function writeManagedArtifactBundle(manifestPath, outputs, provenance, opt
5543
5998
  }
5544
5999
  }
5545
6000
  if (!options.force) {
5546
- const root = path7.dirname(absoluteManifest);
6001
+ const root = path8.dirname(absoluteManifest);
5547
6002
  const recorded = new Map(
5548
- previous?.outputs.map((output) => [path7.resolve(root, output.path), output.sha256]) ?? []
6003
+ previous?.outputs.map((output) => [path8.resolve(root, output.path), output.sha256]) ?? []
5549
6004
  );
5550
6005
  const conflicts = [];
5551
6006
  for (const output of outputs) {
5552
- const destination = path7.resolve(output.path);
6007
+ const destination = path8.resolve(output.path);
5553
6008
  const current = await readOptional(destination);
5554
6009
  if (!current || digest(current) === digest(output.data)) continue;
5555
6010
  const expected = recorded.get(destination);
@@ -5571,15 +6026,15 @@ Pass { force: true } (CLI: --force) to take ownership and replace the complete b
5571
6026
  );
5572
6027
  }
5573
6028
  async function verifyArtifactBundle(manifestPath) {
5574
- const absolute = path7.resolve(manifestPath);
6029
+ const absolute = path8.resolve(manifestPath);
5575
6030
  const raw = await readArtifactBundleManifest(absolute);
5576
- const root = path7.dirname(absolute);
6031
+ const root = path8.dirname(absolute);
5577
6032
  const changedSources = [];
5578
6033
  const changedOutputs = [];
5579
6034
  for (const source of raw.sources) {
5580
6035
  let actual = null;
5581
6036
  try {
5582
- actual = digest(await readFile4(path7.resolve(root, source.path)));
6037
+ actual = digest(await readFile5(path8.resolve(root, source.path)));
5583
6038
  } catch (error) {
5584
6039
  if (!isCode(error, "ENOENT")) throw error;
5585
6040
  }
@@ -5587,7 +6042,7 @@ async function verifyArtifactBundle(manifestPath) {
5587
6042
  }
5588
6043
  for (const output of raw.outputs) {
5589
6044
  try {
5590
- if (digest(await readFile4(path7.resolve(root, output.path))) !== output.sha256) {
6045
+ if (digest(await readFile5(path8.resolve(root, output.path))) !== output.sha256) {
5591
6046
  changedOutputs.push(output.path);
5592
6047
  }
5593
6048
  } catch (error) {
@@ -5626,7 +6081,7 @@ async function rollback(prepared) {
5626
6081
  for (const artifact of [...prepared].reverse()) {
5627
6082
  if (!artifact.backup || !artifact.backedUp) continue;
5628
6083
  try {
5629
- await rename2(artifact.backup, artifact.destination);
6084
+ await rename3(artifact.backup, artifact.destination);
5630
6085
  artifact.backedUp = false;
5631
6086
  } catch (error) {
5632
6087
  errors.push(
@@ -5643,7 +6098,7 @@ async function writeArtifactBundle(files, options = {}) {
5643
6098
  if (!files.length) throw new Error("An artifact bundle must contain at least one file.");
5644
6099
  const normalized = files.map((file) => {
5645
6100
  if (!file.path.trim()) throw new Error("Artifact paths cannot be empty.");
5646
- return { destination: path7.resolve(file.path), data: Buffer.from(file.data) };
6101
+ return { destination: path8.resolve(file.path), data: Buffer.from(file.data) };
5647
6102
  });
5648
6103
  const destinations = /* @__PURE__ */ new Set();
5649
6104
  for (const artifact of normalized) {
@@ -5659,7 +6114,7 @@ async function writeArtifactBundle(files, options = {}) {
5659
6114
  const unchanged = [];
5660
6115
  for (const artifact of normalized) {
5661
6116
  try {
5662
- const current = await readFile4(artifact.destination);
6117
+ const current = await readFile5(artifact.destination);
5663
6118
  if (current.equals(artifact.data)) {
5664
6119
  unchanged.push(artifact.destination);
5665
6120
  } else {
@@ -5673,21 +6128,21 @@ async function writeArtifactBundle(files, options = {}) {
5673
6128
  if (!changed.length) return { changed: [], unchanged };
5674
6129
  const token = randomUUID3();
5675
6130
  for (const [index, artifact] of changed.entries()) {
5676
- const basename = path7.basename(artifact.destination);
5677
- artifact.stage = path7.join(
5678
- path7.dirname(artifact.destination),
6131
+ const basename = path8.basename(artifact.destination);
6132
+ artifact.stage = path8.join(
6133
+ path8.dirname(artifact.destination),
5679
6134
  `.${basename}.pixelkiln-stage-${token}-${index}`
5680
6135
  );
5681
6136
  if (artifact.existed) {
5682
- artifact.backup = path7.join(
5683
- path7.dirname(artifact.destination),
6137
+ artifact.backup = path8.join(
6138
+ path8.dirname(artifact.destination),
5684
6139
  `.${basename}.pixelkiln-backup-${token}-${index}`
5685
6140
  );
5686
6141
  }
5687
6142
  }
5688
- const journal = options.recoveryFile ? path7.resolve(options.recoveryFile) : null;
6143
+ const journal = options.recoveryFile ? path8.resolve(options.recoveryFile) : null;
5689
6144
  if (journal) {
5690
- await mkdir2(path7.dirname(journal), { recursive: true });
6145
+ await mkdir3(path8.dirname(journal), { recursive: true });
5691
6146
  const transaction = {
5692
6147
  format: "pixelkiln-artifact-transaction",
5693
6148
  version: 1,
@@ -5701,7 +6156,7 @@ async function writeArtifactBundle(files, options = {}) {
5701
6156
  }))
5702
6157
  };
5703
6158
  try {
5704
- await writeFile2(journal, JSON.stringify(transaction, null, 2) + "\n", { flag: "wx" });
6159
+ await writeFile3(journal, JSON.stringify(transaction, null, 2) + "\n", { flag: "wx" });
5705
6160
  activeTransactions.add(journal);
5706
6161
  } catch (error) {
5707
6162
  throw new Error(
@@ -5712,9 +6167,9 @@ async function writeArtifactBundle(files, options = {}) {
5712
6167
  }
5713
6168
  try {
5714
6169
  for (const [index, artifact] of changed.entries()) {
5715
- await mkdir2(path7.dirname(artifact.destination), { recursive: true });
6170
+ await mkdir3(path8.dirname(artifact.destination), { recursive: true });
5716
6171
  await options.beforeStage?.(artifact.destination, index);
5717
- await writeFile2(artifact.stage, artifact.data, { flag: "wx" });
6172
+ await writeFile3(artifact.stage, artifact.data, { flag: "wx" });
5718
6173
  }
5719
6174
  } catch (error) {
5720
6175
  const cleanupErrors2 = await rollback(changed);
@@ -5727,17 +6182,17 @@ async function writeArtifactBundle(files, options = {}) {
5727
6182
  try {
5728
6183
  for (const artifact of changed) {
5729
6184
  if (!artifact.existed) continue;
5730
- await rename2(artifact.destination, artifact.backup);
6185
+ await rename3(artifact.destination, artifact.backup);
5731
6186
  artifact.backedUp = true;
5732
6187
  }
5733
6188
  for (const [index, artifact] of changed.entries()) {
5734
6189
  await options.beforePromote?.(artifact.destination, index);
5735
- await rename2(artifact.stage, artifact.destination);
6190
+ await rename3(artifact.stage, artifact.destination);
5736
6191
  artifact.stage = void 0;
5737
6192
  artifact.promoted = true;
5738
6193
  }
5739
6194
  if (journal) {
5740
- await writeFile2(transactionMarker(journal), `${token}
6195
+ await writeFile3(transactionMarker(journal), `${token}
5741
6196
  `, { flag: "wx" });
5742
6197
  durableCommit = true;
5743
6198
  await options.afterCommit?.();
@@ -5779,69 +6234,6 @@ async function writeArtifactBundle(files, options = {}) {
5779
6234
  };
5780
6235
  }
5781
6236
 
5782
- // src/palette.ts
5783
- function normalizePalette(values) {
5784
- const colors = /* @__PURE__ */ new Map();
5785
- for (const value of values) {
5786
- const rgb = parseHex(value);
5787
- const hex2 = `#${rgb.r.toString(16).padStart(2, "0")}${rgb.g.toString(16).padStart(2, "0")}${rgb.b.toString(16).padStart(2, "0")}`;
5788
- if (!colors.has(hex2)) colors.set(hex2, { hex: hex2, ...rgb });
5789
- }
5790
- if (colors.size < 2 || colors.size > 256) {
5791
- throw new Error(`A palette must contain 2\u2013256 unique colors; got ${colors.size}.`);
5792
- }
5793
- return [...colors.values()];
5794
- }
5795
- function colorDistance(a, b) {
5796
- const rmean = (a.r + b.r) / 2;
5797
- const dr = a.r - b.r;
5798
- const dg = a.g - b.g;
5799
- const db = a.b - b.b;
5800
- return Math.sqrt(
5801
- (512 + rmean) * dr * dr / 256 + 4 * dg * dg + (767 - rmean) * db * db / 256
5802
- );
5803
- }
5804
- function quantize(png, palette) {
5805
- const colors = normalizePalette(palette);
5806
- const pixels = Buffer.from(png.pixels);
5807
- const nearestOf = /* @__PURE__ */ new Map();
5808
- let changed = 0;
5809
- for (let i = 0; i < pixels.length; i += 4) {
5810
- const alpha = pixels[i + 3];
5811
- if (alpha === 0) {
5812
- if (pixels[i] || pixels[i + 1] || pixels[i + 2]) changed++;
5813
- pixels[i] = 0;
5814
- pixels[i + 1] = 0;
5815
- pixels[i + 2] = 0;
5816
- continue;
5817
- }
5818
- const key = pixels[i] << 16 | pixels[i + 1] << 8 | pixels[i + 2];
5819
- let nearest = nearestOf.get(key);
5820
- if (!nearest) {
5821
- const pixel = { r: pixels[i], g: pixels[i + 1], b: pixels[i + 2] };
5822
- nearest = colors[0];
5823
- let distance = colorDistance(pixel, nearest);
5824
- for (let j = 1; j < colors.length; j++) {
5825
- const candidate = colors[j];
5826
- const candidateDistance = colorDistance(pixel, candidate);
5827
- if (candidateDistance < distance) {
5828
- nearest = candidate;
5829
- distance = candidateDistance;
5830
- }
5831
- }
5832
- nearestOf.set(key, nearest);
5833
- }
5834
- if (pixels[i] !== nearest.r || pixels[i + 1] !== nearest.g || pixels[i + 2] !== nearest.b) changed++;
5835
- pixels[i] = nearest.r;
5836
- pixels[i + 1] = nearest.g;
5837
- pixels[i + 2] = nearest.b;
5838
- }
5839
- return { png: { width: png.width, height: png.height, pixels }, changed };
5840
- }
5841
- function quantizeToPalette(png, palette) {
5842
- return quantize(png, palette).png;
5843
- }
5844
-
5845
6237
  // src/pipeline/refine.ts
5846
6238
  var execFileAsync = promisify(execFile);
5847
6239
  var PIXEL_ART_FIXER_MIN_SIDE = 16;
@@ -6041,16 +6433,16 @@ function sameFrameGrid(first, next) {
6041
6433
  return next.detection.stepX === first.detection.stepX && next.detection.stepY === first.detection.stepY && next.detection.columns === first.detection.columns && next.detection.rows === first.detection.rows && phaseMatches(next.detection.phaseX, first.detection.phaseX, first.detection.stepX) && phaseMatches(next.detection.phaseY, first.detection.phaseY, first.detection.stepY) && next.audit.width === first.audit.width && next.audit.height === first.audit.height && next.sourceWidth === first.sourceWidth && next.sourceHeight === first.sourceHeight;
6042
6434
  }
6043
6435
  async function refineAsset(options) {
6044
- const source = path8.resolve(options.source);
6045
- const output = path8.resolve(options.output);
6436
+ const source = path9.resolve(options.source);
6437
+ const output = path9.resolve(options.output);
6046
6438
  if (source === output) throw new Error("Refinement source and output must be different files.");
6047
- const record = path8.resolve(qualityRecordPath(output));
6439
+ const record = path9.resolve(qualityRecordPath(output));
6048
6440
  const palette = normalizePalette(options.palette).map((color) => color.hex);
6049
6441
  const minGridConfidence = requireConfidence(options.minGridConfidence ?? "high");
6050
6442
  if (options.minTransparency !== void 0 && (!Number.isFinite(options.minTransparency) || options.minTransparency < 0 || options.minTransparency > 1)) throw new Error("minTransparency must be between 0 and 1.");
6051
6443
  let sourcePng;
6052
6444
  try {
6053
- sourcePng = decodePng(await readFile5(source));
6445
+ sourcePng = decodePng(await readFile6(source));
6054
6446
  } catch (error) {
6055
6447
  throw new Error(`Cannot read refinement source ${source}: ${message2(error)}`, { cause: error });
6056
6448
  }
@@ -6067,8 +6459,8 @@ async function refineAsset(options) {
6067
6459
  );
6068
6460
  }
6069
6461
  }
6070
- const temp = await mkdtemp(path8.join(tmpdir(), "pixelkiln-refine-"));
6071
- const recovered = path8.join(temp, "native.png");
6462
+ const temp = await mkdtemp(path9.join(tmpdir(), "pixelkiln-refine-"));
6463
+ const recovered = path9.join(temp, "native.png");
6072
6464
  const fixerCommand = compatibilityCommand ?? options.fixerPython ?? process.env.PIXELKILN_PIXEL_FIXER_PYTHON ?? "python3";
6073
6465
  const args = compatibilityCommand ? [...options.fixerArgsPrefix ?? [], source, "--extract", recovered] : ["-c", PYTHON_FIXER_BRIDGE, source, recovered];
6074
6466
  try {
@@ -6089,7 +6481,7 @@ async function refineAsset(options) {
6089
6481
  );
6090
6482
  }
6091
6483
  const detection = parseDetection(firstJsonObject(stdout));
6092
- const native = decodePng(await readFile5(recovered));
6484
+ const native = decodePng(await readFile6(recovered));
6093
6485
  const quantized = quantizeToPalette(native, palette);
6094
6486
  const png = encodeRgbaPng(quantized.width, quantized.height, quantized.pixels);
6095
6487
  const audit = auditRefinement(
@@ -6153,18 +6545,18 @@ async function refineFrameSet(options) {
6153
6545
  if (roles.some((role) => !role.trim()) || new Set(roles).size !== roles.length) {
6154
6546
  throw new Error("Frame-set roles must be non-empty and unique.");
6155
6547
  }
6156
- const output = path8.resolve(options.output);
6157
- const record = path8.resolve(qualityRecordPath(output));
6548
+ const output = path9.resolve(options.output);
6549
+ const record = path9.resolve(qualityRecordPath(output));
6158
6550
  const palette = normalizeRefinementPalette(options.palette);
6159
6551
  const finalOutputs = options.sources.map((source, index) => qualityFrameOutputPath(output, source.role, index));
6160
6552
  if (new Set(finalOutputs).size !== finalOutputs.length) {
6161
6553
  throw new Error("Frame-set roles resolve to duplicate output filenames.");
6162
6554
  }
6163
- const sourceSet = new Set(options.sources.map((source) => path8.resolve(source.path)));
6555
+ const sourceSet = new Set(options.sources.map((source) => path9.resolve(source.path)));
6164
6556
  if (finalOutputs.some((file) => sourceSet.has(file))) {
6165
6557
  throw new Error("Frame-set refinement sources and outputs must be different files.");
6166
6558
  }
6167
- const temp = await mkdtemp(path8.join(tmpdir(), "pixelkiln-refine-frames-"));
6559
+ const temp = await mkdtemp(path9.join(tmpdir(), "pixelkiln-refine-frames-"));
6168
6560
  try {
6169
6561
  const frames = [];
6170
6562
  const outputs = [];
@@ -6172,8 +6564,8 @@ async function refineFrameSet(options) {
6172
6564
  const sourcePaths = [];
6173
6565
  let firstOptions = null;
6174
6566
  for (const [index, sourceEntry] of options.sources.entries()) {
6175
- const source = path8.resolve(sourceEntry.path);
6176
- const temporaryOutput = path8.join(temp, `${String(index).padStart(2, "0")}.png`);
6567
+ const source = path9.resolve(sourceEntry.path);
6568
+ const temporaryOutput = path9.join(temp, `${String(index).padStart(2, "0")}.png`);
6177
6569
  const refined = await refineAsset({
6178
6570
  source,
6179
6571
  output: temporaryOutput,
@@ -6205,7 +6597,7 @@ async function refineFrameSet(options) {
6205
6597
  });
6206
6598
  outputs.push({
6207
6599
  path: finalOutputs[index],
6208
- data: await readFile5(temporaryOutput)
6600
+ data: await readFile6(temporaryOutput)
6209
6601
  });
6210
6602
  }
6211
6603
  const first = frames[0];
@@ -6248,7 +6640,7 @@ async function refineFrameSet(options) {
6248
6640
  );
6249
6641
  return {
6250
6642
  sources: sourcePaths,
6251
- outputs: outputs.map((item) => path8.resolve(item.path)),
6643
+ outputs: outputs.map((item) => path9.resolve(item.path)),
6252
6644
  record,
6253
6645
  frames,
6254
6646
  palette,
@@ -6259,12 +6651,12 @@ async function refineFrameSet(options) {
6259
6651
  }
6260
6652
  }
6261
6653
  async function checkQualityRecord(recordPath) {
6262
- const record = path8.resolve(recordPath);
6654
+ const record = path9.resolve(recordPath);
6263
6655
  const manifest = await readArtifactBundleManifest(record);
6264
6656
  const options = parseRecordOptions(manifest, record);
6265
6657
  const verification = await verifyArtifactBundle(record);
6266
- const sources = manifest.sources.map((source2) => path8.resolve(path8.dirname(record), source2.path));
6267
- const outputs = manifest.outputs.map((output2) => path8.resolve(path8.dirname(record), output2.path));
6658
+ const sources = manifest.sources.map((source2) => path9.resolve(path9.dirname(record), source2.path));
6659
+ const outputs = manifest.outputs.map((output2) => path9.resolve(path9.dirname(record), output2.path));
6268
6660
  const source = sources[0] ?? "";
6269
6661
  const output = outputs[0] ?? "";
6270
6662
  const expectedCount = options.frameSet?.count ?? 1;
@@ -6297,7 +6689,7 @@ async function checkQualityRecord(recordPath) {
6297
6689
  async function approveQualityRecord(recordPath, approval) {
6298
6690
  const reviewer = approval.reviewer.trim();
6299
6691
  if (!reviewer) throw new Error("A human reviewer name is required.");
6300
- const record = path8.resolve(recordPath);
6692
+ const record = path9.resolve(recordPath);
6301
6693
  const manifest = await readArtifactBundleManifest(record);
6302
6694
  const options = parseRecordOptions(manifest, record);
6303
6695
  const verification = await verifyArtifactBundle(record);
@@ -6318,14 +6710,14 @@ async function approveQualityRecord(recordPath, approval) {
6318
6710
  `A refinement record must contain exactly ${expectedCount} source(s) and output(s).`
6319
6711
  );
6320
6712
  }
6321
- const root = path8.dirname(record);
6713
+ const root = path9.dirname(record);
6322
6714
  const outputs = await Promise.all(manifest.outputs.map(async (entry) => {
6323
- const outputPath = path8.resolve(root, entry.path);
6324
- return { path: outputPath, data: await readFile5(outputPath) };
6715
+ const outputPath = path9.resolve(root, entry.path);
6716
+ return { path: outputPath, data: await readFile6(outputPath) };
6325
6717
  }));
6326
6718
  const sources = manifest.sources.map((source) => ({
6327
6719
  ...source,
6328
- path: path8.resolve(root, source.path)
6720
+ path: path9.resolve(root, source.path)
6329
6721
  }));
6330
6722
  const approved = {
6331
6723
  ...options,
@@ -6357,7 +6749,7 @@ async function qualitySource(spec, lock) {
6357
6749
  if (spec.generator === "frames") {
6358
6750
  return { items: null, reason: "frame-set quality requires ordered downloaded provider outputs" };
6359
6751
  }
6360
- const source = path9.resolve(spec.root, spec.source);
6752
+ const source = path10.resolve(spec.root, spec.source);
6361
6753
  return existsSync6(source) ? { items: [{ role: "source", path: source }] } : { items: null, reason: `declared source is missing: ${spec.source}` };
6362
6754
  }
6363
6755
  const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
@@ -6424,7 +6816,7 @@ async function inspectQualityProfile(spec, lock) {
6424
6816
  const expectedRevision = spec.quality.fixerRevision ?? PIXEL_ART_FIXER_REVISION;
6425
6817
  const expectedTransparency = spec.quality.minTransparency ?? null;
6426
6818
  const expectedFrameSet = spec.generator === "frames" ? { fps: spec.quality.fps ?? 12, count: source.items.length, roles: source.items.map((item) => item.role) } : null;
6427
- const contractChanged = JSON.stringify(check.sources.map((item) => path9.resolve(item))) !== JSON.stringify(sourcePaths.map((item) => path9.resolve(item))) || JSON.stringify(check.outputs.map((item) => path9.resolve(item))) !== JSON.stringify(expectedOutputs.map((item) => path9.resolve(item))) || JSON.stringify(check.options.palette.colors) !== JSON.stringify(expectedPalette) || check.options.nativeGrid.revision !== expectedRevision || check.options.audit.thresholds.minGridConfidence !== spec.quality.minGridConfidence || check.options.audit.thresholds.minTransparency !== expectedTransparency || (expectedFrameSet ? !check.options.frameSet || check.options.frameSet.fps !== expectedFrameSet.fps || check.options.frameSet.count !== expectedFrameSet.count || JSON.stringify(check.options.frameSet.roles) !== JSON.stringify(expectedFrameSet.roles) : check.options.frameSet !== void 0);
6819
+ const contractChanged = JSON.stringify(check.sources.map((item) => path10.resolve(item))) !== JSON.stringify(sourcePaths.map((item) => path10.resolve(item))) || JSON.stringify(check.outputs.map((item) => path10.resolve(item))) !== JSON.stringify(expectedOutputs.map((item) => path10.resolve(item))) || JSON.stringify(check.options.palette.colors) !== JSON.stringify(expectedPalette) || check.options.nativeGrid.revision !== expectedRevision || check.options.audit.thresholds.minGridConfidence !== spec.quality.minGridConfidence || check.options.audit.thresholds.minTransparency !== expectedTransparency || (expectedFrameSet ? !check.options.frameSet || check.options.frameSet.fps !== expectedFrameSet.fps || check.options.frameSet.count !== expectedFrameSet.count || JSON.stringify(check.options.frameSet.roles) !== JSON.stringify(expectedFrameSet.roles) : check.options.frameSet !== void 0);
6428
6820
  if (contractChanged) {
6429
6821
  return {
6430
6822
  key,
@@ -6586,43 +6978,21 @@ async function requireApprovedQualitySources(specs, lock, assetIds, outputRoles)
6586
6978
  return sources;
6587
6979
  }
6588
6980
 
6589
- // src/pipeline/postprocess.ts
6590
- function postprocessFor(spec) {
6591
- if (!spec.enforcePalette || spec.palette.length < 2) return void 0;
6592
- return {
6593
- palette: {
6594
- colors: normalizePalette(spec.palette).map((color) => color.hex),
6595
- dither: "none",
6596
- distance: "redmean"
6597
- }
6598
- };
6599
- }
6600
- function samePostprocess(a, b) {
6601
- const pa = a?.palette;
6602
- const pb = b?.palette;
6603
- if (!pa && !pb) return true;
6604
- if (!pa || !pb) return false;
6605
- return pa.dither === pb.dither && pa.distance === pb.distance && pa.colors.length === pb.colors.length && pa.colors.every((color, i) => color === pb.colors[i]);
6606
- }
6607
- function postprocessCurrent(entry, spec) {
6608
- return samePostprocess(entry.postprocess, postprocessFor(spec));
6609
- }
6610
- function applyPostprocess(bytes, mediaType, record) {
6611
- if (!record?.palette || mediaType !== MediaType.PNG) return { bytes, changed: false };
6612
- const png = decodePng(bytes);
6613
- const { png: snapped, changed } = quantize(png, record.palette.colors);
6614
- if (!changed) return { bytes, changed: false };
6615
- return { bytes: encodeRgbaPng(snapped.width, snapped.height, snapped.pixels), changed: true };
6616
- }
6617
-
6618
6981
  // src/pipeline/revision.ts
6619
6982
  import { existsSync as existsSync7 } from "fs";
6620
- import path10 from "path";
6983
+ import path11 from "path";
6621
6984
  async function inspectRevisionReadiness(spec, lock) {
6985
+ if (spec.mirror) return inspectMirror(spec, lock, /* @__PURE__ */ new Set());
6622
6986
  if (spec.character?.parentSpec) return inspectCharacterParent(spec, lock, /* @__PURE__ */ new Set());
6623
6987
  if (!spec.revision) return null;
6624
6988
  return inspectRevision(spec, lock, /* @__PURE__ */ new Set());
6625
6989
  }
6990
+ async function inspectMirror(spec, lock, seen) {
6991
+ const source = spec.mirror.sourceSpec;
6992
+ const label = `mirror source ${spec.styleId}/${spec.mirror.sourceAssetId}`;
6993
+ const dependency = await inspectParent(source, lock, seen);
6994
+ return dependency.ready ? { ready: true, reason: `${label} is current` } : { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
6995
+ }
6626
6996
  async function inspectCharacterParent(spec, lock, seen) {
6627
6997
  const character = spec.character;
6628
6998
  const parentSpec = character.parentSpec;
@@ -6676,6 +7046,10 @@ async function inspectParent(spec, lock, seen) {
6676
7046
  if (seen.has(key)) return { ready: false, reason: `revision dependency cycle at ${key}` };
6677
7047
  const nextSeen = new Set(seen);
6678
7048
  nextSeen.add(key);
7049
+ if (spec.mirror) {
7050
+ const inputs = await inspectMirror(spec, lock, nextSeen);
7051
+ if (!inputs.ready) return inputs;
7052
+ }
6679
7053
  if (spec.revision) {
6680
7054
  const inputs = await inspectRevision(spec, lock, nextSeen);
6681
7055
  if (!inputs.ready) return inputs;
@@ -6692,7 +7066,7 @@ async function inspectParent(spec, lock, seen) {
6692
7066
  };
6693
7067
  }
6694
7068
  if (spec.source) {
6695
- const source = path10.resolve(spec.root, spec.source);
7069
+ const source = path11.resolve(spec.root, spec.source);
6696
7070
  return existsSync7(source) ? { ready: true, reason: "declared source is present" } : { ready: false, reason: `declared source is missing: ${spec.source}` };
6697
7071
  }
6698
7072
  const entry = lock.entries[key];
@@ -6762,7 +7136,7 @@ async function buildPlan(specs, lock, opts = {}) {
6762
7136
  state = "missing";
6763
7137
  reason = "--force";
6764
7138
  } else if (!entry && spec.source && spec.generator !== "frames") {
6765
- if (existsSync8(path11.resolve(spec.root, spec.source))) {
7139
+ if (existsSync8(path12.resolve(spec.root, spec.source))) {
6766
7140
  state = "ok";
6767
7141
  reason = `placed from ${spec.source}; not generated`;
6768
7142
  } else {
@@ -6775,11 +7149,17 @@ async function buildPlan(specs, lock, opts = {}) {
6775
7149
  reason = "file on disk, no provenance recorded";
6776
7150
  } else {
6777
7151
  state = "missing";
6778
- reason = "not in lockfile";
7152
+ reason = spec.mirror ? `not made yet; flipped from ${spec.mirror.sourceAssetId} (no generation cost)` : "not in lockfile";
6779
7153
  }
6780
7154
  } else if (entry.specHash !== spec.specHash) {
6781
7155
  state = "stale";
6782
7156
  reason = "prompt, size, or style changed";
7157
+ } else if (spec.mirror && !mirrorCurrent(entry, lock.entries[lockKey(spec.styleId, spec.mirror.sourceAssetId)])) {
7158
+ state = "stale";
7159
+ reason = `mirror source ${spec.mirror.sourceAssetId} changed; flip it again (no generation cost)`;
7160
+ } else if (spec.mirror && entry.status === "downloaded" && entry.outputs.some((_output, index) => !existsSync8(currentEntryOutputPath(entry, spec, index)))) {
7161
+ state = "stale";
7162
+ reason = "mirror output missing on disk; flip it again (no generation cost)";
6783
7163
  } else if (entry.status === "download-failed") {
6784
7164
  state = "recoverable";
6785
7165
  const force = entry.error?.includes("pass --force") ? " --force" : "";
@@ -6808,6 +7188,9 @@ async function buildPlan(specs, lock, opts = {}) {
6808
7188
  } else if (await anyOutputModified(entry, spec)) {
6809
7189
  state = "orphaned";
6810
7190
  reason = "output modified since download";
7191
+ } else if (spec.mirror && !postprocessCurrent(entry, spec)) {
7192
+ state = "stale";
7193
+ reason = "palette enforcement changed; flip the source again once it is refreshed (no generation cost)";
6811
7194
  } else if (!postprocessCurrent(entry, spec)) {
6812
7195
  state = "recoverable";
6813
7196
  reason = spec.enforcePalette ? "palette enforcement turned on; run pixelkiln fetch to snap the file (no generation cost)" : "palette enforcement turned off; run pixelkiln fetch to restore the provider's bytes (no generation cost)";
@@ -6819,15 +7202,19 @@ async function buildPlan(specs, lock, opts = {}) {
6819
7202
  items.push({ spec, key, state, reason, ...quality ? { quality } : {} });
6820
7203
  }
6821
7204
  const isActionable = (i) => i.state === "missing" || i.state === "stale" || i.state === "failed";
6822
- const actionableKeys = new Set(items.filter(isActionable).map((i) => i.key));
7205
+ const stateOf = new Map(items.map((i) => [i.key, i.state]));
6823
7206
  for (const item of items) {
6824
7207
  if (!isActionable(item)) continue;
6825
- const parent = item.spec.revision?.sourceSpec ?? item.spec.character?.parentSpec;
7208
+ const parent = item.spec.mirror?.sourceSpec ?? item.spec.revision?.sourceSpec ?? item.spec.character?.parentSpec;
6826
7209
  if (!parent) continue;
6827
7210
  const parentKey = lockKey(parent.styleId, parent.assetId);
6828
- if (actionableKeys.has(parentKey)) {
7211
+ const parentState = stateOf.get(parentKey);
7212
+ if (parentState && isActionable({ state: parentState })) {
6829
7213
  item.state = "blocked";
6830
7214
  item.reason = `parent ${parentKey} is being generated in this run; this follows in the next wave`;
7215
+ } else if (item.spec.mirror && (parentState === "recoverable" || parentState === "in-flight")) {
7216
+ item.state = "blocked";
7217
+ item.reason = `mirror source ${parentKey} is ${parentState}; this follows in the next wave`;
6831
7218
  }
6832
7219
  }
6833
7220
  const actionable = items.filter(isActionable);
@@ -6885,13 +7272,13 @@ function summarize(plan) {
6885
7272
 
6886
7273
  // src/project.ts
6887
7274
  async function openProject(manifestPath = "pixelkiln.manifest.json", opts = {}) {
6888
- const absolute = path12.resolve(manifestPath);
6889
- const root = path12.dirname(absolute);
7275
+ const absolute = path13.resolve(manifestPath);
7276
+ const root = path13.dirname(absolute);
6890
7277
  if (opts.env !== false) {
6891
7278
  loadEnvFiles(root);
6892
- if (path12.resolve(process.cwd()) !== root) loadEnvFiles(process.cwd());
7279
+ if (path13.resolve(process.cwd()) !== root) loadEnvFiles(process.cwd());
6893
7280
  }
6894
- const lockPath = path12.resolve(opts.lockPath ?? path12.join(root, "pixelkiln.lock.json"));
7281
+ const lockPath = path13.resolve(opts.lockPath ?? path13.join(root, "pixelkiln.lock.json"));
6895
7282
  const loaded = await loadManifest(absolute);
6896
7283
  const specs = await resolveSpecs(loaded, { styles: opts.styles ?? [], assets: opts.assets ?? [] });
6897
7284
  const lock = await loadLock(lockPath);
@@ -6958,9 +7345,9 @@ function shouldPersistSourceUrl(value) {
6958
7345
  }
6959
7346
 
6960
7347
  // src/recipes.ts
6961
- import path13 from "path";
7348
+ import path14 from "path";
6962
7349
  import { existsSync as existsSync9 } from "fs";
6963
- import { readdir, readFile as readFile6 } from "fs/promises";
7350
+ import { readdir, readFile as readFile7 } from "fs/promises";
6964
7351
  import { fileURLToPath } from "url";
6965
7352
  import { z as z3 } from "zod";
6966
7353
  var SHA256_RE = /^[0-9a-f]{64}$/;
@@ -6969,7 +7356,7 @@ var VERSION_RE = /^\d+\.\d+\.\d+$/;
6969
7356
  var RECIPE_FILE = "pixelkiln.recipe.json";
6970
7357
  var RECIPE_DIR_TOKEN = "{{recipeDir}}";
6971
7358
  var PortablePathSchema = z3.string().min(1).refine((value) => {
6972
- if (value.includes("\\") || path13.posix.isAbsolute(value)) return false;
7359
+ if (value.includes("\\") || path14.posix.isAbsolute(value)) return false;
6973
7360
  const parts = value.split("/");
6974
7361
  return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
6975
7362
  }, "expected a portable relative path without '.', '..', or backslashes");
@@ -7145,10 +7532,10 @@ function message3(error) {
7145
7532
  return error instanceof Error ? error.message : String(error);
7146
7533
  }
7147
7534
  async function readRecipeFile(recipePath, bundled) {
7148
- const absolute = path13.resolve(recipePath);
7535
+ const absolute = path14.resolve(recipePath);
7149
7536
  let raw;
7150
7537
  try {
7151
- raw = JSON.parse(await readFile6(absolute, "utf8"));
7538
+ raw = JSON.parse(await readFile7(absolute, "utf8"));
7152
7539
  } catch (error) {
7153
7540
  throw new Error(`Could not read recipe ${absolute}: ${message3(error)}`, { cause: error });
7154
7541
  }
@@ -7159,13 +7546,13 @@ async function readRecipeFile(recipePath, bundled) {
7159
7546
  ` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
7160
7547
  );
7161
7548
  }
7162
- return { recipe: parsed.data, authored: raw, path: absolute, dir: path13.dirname(absolute), bundled };
7549
+ return { recipe: parsed.data, authored: raw, path: absolute, dir: path14.dirname(absolute), bundled };
7163
7550
  }
7164
7551
  async function walkRecipeFiles(root) {
7165
7552
  if (!existsSync9(root)) return [];
7166
7553
  const out = [];
7167
7554
  for (const entry of await readdir(root, { withFileTypes: true })) {
7168
- const resolved = path13.join(root, entry.name);
7555
+ const resolved = path14.join(root, entry.name);
7169
7556
  if (entry.isDirectory()) out.push(...await walkRecipeFiles(resolved));
7170
7557
  else if (entry.isFile() && entry.name === RECIPE_FILE) out.push(resolved);
7171
7558
  }
@@ -7176,7 +7563,7 @@ function bundledRecipeRoot() {
7176
7563
  }
7177
7564
  async function listBundledRecipes(root = bundledRecipeRoot()) {
7178
7565
  const recipes = await Promise.all(
7179
- (await walkRecipeFiles(path13.resolve(root))).map((file) => readRecipeFile(file, true))
7566
+ (await walkRecipeFiles(path14.resolve(root))).map((file) => readRecipeFile(file, true))
7180
7567
  );
7181
7568
  return recipes.sort(
7182
7569
  (a, b) => a.recipe.id.localeCompare(b.recipe.id) || compareVersions(b.recipe.version, a.recipe.version)
@@ -7199,9 +7586,9 @@ function parseSelector(selector) {
7199
7586
  return { id: match[1], version: match[2] };
7200
7587
  }
7201
7588
  async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
7202
- const local = path13.resolve(target);
7589
+ const local = path14.resolve(target);
7203
7590
  if (existsSync9(local)) {
7204
- const recipePath = path13.basename(local) === RECIPE_FILE ? local : path13.join(local, RECIPE_FILE);
7591
+ const recipePath = path14.basename(local) === RECIPE_FILE ? local : path14.join(local, RECIPE_FILE);
7205
7592
  return readRecipeFile(recipePath, false);
7206
7593
  }
7207
7594
  if (target.startsWith(".") || target.startsWith("/") || target.includes("\\")) {
@@ -7215,7 +7602,7 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
7215
7602
  return matches[0];
7216
7603
  }
7217
7604
  async function verifyFile(root, file) {
7218
- const absolute = path13.resolve(root, ...file.path.split("/"));
7605
+ const absolute = path14.resolve(root, ...file.path.split("/"));
7219
7606
  if (!existsSync9(absolute)) {
7220
7607
  return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
7221
7608
  }
@@ -7231,7 +7618,7 @@ async function verifyRecipe(target, options = {}) {
7231
7618
  const loaded = await resolveRecipe(target, options.bundledRoot);
7232
7619
  const actualIntegrity = recipeDigest(loaded.authored);
7233
7620
  const files = await Promise.all(loaded.recipe.files.map((file) => verifyFile(loaded.dir, file)));
7234
- const modelRoot = options.modelRoot ? path13.resolve(options.modelRoot) : null;
7621
+ const modelRoot = options.modelRoot ? path14.resolve(options.modelRoot) : null;
7235
7622
  const models = modelRoot ? await Promise.all(loaded.recipe.models.map(async (model) => ({
7236
7623
  ...await verifyFile(modelRoot, model),
7237
7624
  source: model.source,
@@ -7265,7 +7652,7 @@ async function verifyRecipe(target, options = {}) {
7265
7652
  };
7266
7653
  }
7267
7654
  function renderStyle(style, destination, cwd) {
7268
- const portableDir = path13.relative(cwd, destination).split(path13.sep).join("/") || ".";
7655
+ const portableDir = path14.relative(cwd, destination).split(path14.sep).join("/") || ".";
7269
7656
  const replace = (value) => {
7270
7657
  if (typeof value === "string") return value.replaceAll(RECIPE_DIR_TOKEN, portableDir);
7271
7658
  if (Array.isArray(value)) return value.map(replace);
@@ -7277,25 +7664,25 @@ function renderStyle(style, destination, cwd) {
7277
7664
  return replace(style);
7278
7665
  }
7279
7666
  async function installRecipe(target, options = {}) {
7280
- const cwd = path13.resolve(options.cwd ?? process.cwd());
7667
+ const cwd = path14.resolve(options.cwd ?? process.cwd());
7281
7668
  const loaded = await resolveRecipe(target, options.bundledRoot);
7282
7669
  const verification = await verifyRecipe(loaded.path);
7283
7670
  if (!verification.ok) {
7284
7671
  throw new Error(`Recipe ${loaded.recipe.id}@${loaded.recipe.version} failed integrity verification.`);
7285
7672
  }
7286
- const destination = options.out ? path13.resolve(cwd, options.out) : path13.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
7673
+ const destination = options.out ? path14.resolve(cwd, options.out) : path14.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
7287
7674
  const sources = [
7288
7675
  { path: RECIPE_FILE, source: loaded.path },
7289
- ...loaded.recipe.files.map((file) => ({ path: file.path, source: path13.join(loaded.dir, ...file.path.split("/")) }))
7676
+ ...loaded.recipe.files.map((file) => ({ path: file.path, source: path14.join(loaded.dir, ...file.path.split("/")) }))
7290
7677
  ];
7291
7678
  const files = await Promise.all(sources.map(async (file) => ({
7292
- path: path13.join(destination, ...file.path.split("/")),
7293
- data: await readFile6(file.source)
7679
+ path: path14.join(destination, ...file.path.split("/")),
7680
+ data: await readFile7(file.source)
7294
7681
  })));
7295
7682
  if (!options.force) {
7296
7683
  for (const file of files) {
7297
7684
  if (!existsSync9(file.path)) continue;
7298
- const current = await readFile6(file.path);
7685
+ const current = await readFile7(file.path);
7299
7686
  if (!current.equals(file.data)) {
7300
7687
  throw new OverwriteRefusedError(
7301
7688
  `Recipe destination has local changes: ${file.path}. Choose another --out or pass --force to replace declared recipe files.`,
@@ -7319,9 +7706,9 @@ async function installRecipe(target, options = {}) {
7319
7706
  }
7320
7707
 
7321
7708
  // src/pipeline/audit.ts
7322
- import { readFile as readFile7 } from "fs/promises";
7709
+ import { readFile as readFile8 } from "fs/promises";
7323
7710
  import { existsSync as existsSync10 } from "fs";
7324
- import path14 from "path";
7711
+ import path15 from "path";
7325
7712
  function paletteDistance(asset, reference) {
7326
7713
  if (!asset.length || !reference.length) return 0;
7327
7714
  let total = 0;
@@ -7362,7 +7749,7 @@ async function auditStyle(loaded, specs, styleId, lock) {
7362
7749
  continue;
7363
7750
  }
7364
7751
  try {
7365
- const png = decodePng(await readFile7(output.absolutePath));
7752
+ const png = decodePng(await readFile8(output.absolutePath));
7366
7753
  const palette = extractPalette(png, 12);
7367
7754
  assets.push({
7368
7755
  assetId: spec.assetId,
@@ -7386,10 +7773,10 @@ async function auditStyle(loaded, specs, styleId, lock) {
7386
7773
  let referenceFromStyleImages = false;
7387
7774
  const refPalettes = [];
7388
7775
  for (const img of style.styleImages) {
7389
- const abs = path14.resolve(loaded.root, img.path);
7776
+ const abs = path15.resolve(loaded.root, img.path);
7390
7777
  if (!existsSync10(abs)) continue;
7391
7778
  try {
7392
- refPalettes.push(extractPalette(decodePng(await readFile7(abs)), 12));
7779
+ refPalettes.push(extractPalette(decodePng(await readFile8(abs)), 12));
7393
7780
  } catch {
7394
7781
  }
7395
7782
  }
@@ -7465,15 +7852,15 @@ function hex(c) {
7465
7852
  }
7466
7853
 
7467
7854
  // src/pipeline/quality-regression.ts
7468
- import path15 from "path";
7855
+ import path16 from "path";
7469
7856
  import { existsSync as existsSync11 } from "fs";
7470
- import { readFile as readFile8 } from "fs/promises";
7857
+ import { readFile as readFile9 } from "fs/promises";
7471
7858
  import { z as z4 } from "zod";
7472
7859
  var SHA256_RE2 = /^[0-9a-f]{64}$/;
7473
7860
  var HEX_RE = /^#[0-9a-f]{6}$/;
7474
7861
  var MAX_REDMEAN_DISTANCE = 765;
7475
7862
  var QualityPathSchema = z4.string().min(1).refine(
7476
- (value) => !path15.posix.isAbsolute(value) && !value.includes("\\"),
7863
+ (value) => !path16.posix.isAbsolute(value) && !value.includes("\\"),
7477
7864
  "expected a portable relative path"
7478
7865
  );
7479
7866
  var DEFAULT_QUALITY_TOLERANCES = {
@@ -7620,10 +8007,10 @@ function measureDecodedImageQuality(png, digest2) {
7620
8007
  };
7621
8008
  }
7622
8009
  async function measureImageQuality(file) {
7623
- const absolute = path15.resolve(file);
8010
+ const absolute = path16.resolve(file);
7624
8011
  let bytes;
7625
8012
  try {
7626
- bytes = await readFile8(absolute);
8013
+ bytes = await readFile9(absolute);
7627
8014
  } catch (error) {
7628
8015
  throw new Error(`Cannot read quality image ${absolute}: ${message4(error)}`, { cause: error });
7629
8016
  }
@@ -7642,11 +8029,11 @@ function resolveQualityInputs(raw, inputsFilePath) {
7642
8029
  }
7643
8030
  const ids = parsed.data.map((entry) => entry.id);
7644
8031
  if (new Set(ids).size !== ids.length) throw new Error("--inputs contains a duplicate quality case id.");
7645
- const root = path15.dirname(path15.resolve(inputsFilePath));
8032
+ const root = path16.dirname(path16.resolve(inputsFilePath));
7646
8033
  return parsed.data.map((entry) => ({
7647
8034
  id: entry.id,
7648
- file: path15.resolve(root, entry.path),
7649
- ...entry.record ? { record: path15.resolve(root, entry.record) } : {},
8035
+ file: path16.resolve(root, entry.path),
8036
+ ...entry.record ? { record: path16.resolve(root, entry.record) } : {},
7650
8037
  tolerances: QualityToleranceSchema.parse({
7651
8038
  ...DEFAULT_QUALITY_TOLERANCES,
7652
8039
  ...entry.tolerances
@@ -7654,7 +8041,7 @@ function resolveQualityInputs(raw, inputsFilePath) {
7654
8041
  }));
7655
8042
  }
7656
8043
  function portableRelative2(from, to) {
7657
- return path15.relative(from, path15.resolve(to)).split(path15.sep).join("/") || ".";
8044
+ return path16.relative(from, path16.resolve(to)).split(path16.sep).join("/") || ".";
7658
8045
  }
7659
8046
  async function verifyRecordForImage(recordPath, imagePath) {
7660
8047
  const verification = await verifyArtifactBundle(recordPath);
@@ -7669,20 +8056,20 @@ async function verifyRecordForImage(recordPath, imagePath) {
7669
8056
  if (record.kind !== "refine" || options === null || typeof options !== "object" || options.schema !== "pixelkiln-quality") {
7670
8057
  throw new Error(`Quality record ${recordPath} is not a PixelKiln refinement record.`);
7671
8058
  }
7672
- const outputs = record.outputs.map((output) => path15.resolve(path15.dirname(recordPath), output.path));
7673
- if (!outputs.includes(path15.resolve(imagePath))) {
8059
+ const outputs = record.outputs.map((output) => path16.resolve(path16.dirname(recordPath), output.path));
8060
+ if (!outputs.includes(path16.resolve(imagePath))) {
7674
8061
  throw new Error(`Quality record ${recordPath} does not own ${imagePath}.`);
7675
8062
  }
7676
8063
  }
7677
8064
  async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
7678
8065
  if (!inputs.length) throw new Error("Cannot snapshot an empty quality baseline.");
7679
- const absolute = path15.resolve(baselinePath);
8066
+ const absolute = path16.resolve(baselinePath);
7680
8067
  for (const input of inputs) {
7681
- if (path15.resolve(input.file) === absolute || input.record && path15.resolve(input.record) === absolute) {
8068
+ if (path16.resolve(input.file) === absolute || input.record && path16.resolve(input.record) === absolute) {
7682
8069
  throw new Error(`Quality baseline output would overwrite an input: ${absolute}.`);
7683
8070
  }
7684
8071
  }
7685
- const root = path15.dirname(absolute);
8072
+ const root = path16.dirname(absolute);
7686
8073
  const cases = [];
7687
8074
  for (const input of [...inputs].sort((a, b) => a.id.localeCompare(b.id))) {
7688
8075
  const expected = await measureImageQuality(input.file);
@@ -7710,7 +8097,7 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
7710
8097
  });
7711
8098
  const data = Buffer.from(JSON.stringify(baseline, null, 2) + "\n");
7712
8099
  if (existsSync11(absolute) && !options.force) {
7713
- const current = await readFile8(absolute);
8100
+ const current = await readFile9(absolute);
7714
8101
  if (!current.equals(data)) {
7715
8102
  throw new OverwriteRefusedError(
7716
8103
  `Quality baseline already exists with different content: ${absolute}. Pass --force to replace it.`,
@@ -7722,10 +8109,10 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
7722
8109
  return { baseline, path: absolute, changed: result.changed.length > 0 };
7723
8110
  }
7724
8111
  async function readQualityBaseline(baselinePath) {
7725
- const absolute = path15.resolve(baselinePath);
8112
+ const absolute = path16.resolve(baselinePath);
7726
8113
  let raw;
7727
8114
  try {
7728
- raw = JSON.parse(await readFile8(absolute, "utf8"));
8115
+ raw = JSON.parse(await readFile9(absolute, "utf8"));
7729
8116
  } catch (error) {
7730
8117
  throw new Error(`Could not read quality baseline ${absolute}: ${message4(error)}`, { cause: error });
7731
8118
  }
@@ -7795,12 +8182,12 @@ function regressionViolations(expected, actual, tolerances) {
7795
8182
  return { violations, warnings };
7796
8183
  }
7797
8184
  async function checkQualityBaseline(baselinePath) {
7798
- const absolute = path15.resolve(baselinePath);
8185
+ const absolute = path16.resolve(baselinePath);
7799
8186
  const baseline = await readQualityBaseline(absolute);
7800
- const root = path15.dirname(absolute);
8187
+ const root = path16.dirname(absolute);
7801
8188
  const cases = [];
7802
8189
  for (const entry of baseline.cases) {
7803
- const file = path15.resolve(root, entry.file);
8190
+ const file = path16.resolve(root, entry.file);
7804
8191
  let actual = null;
7805
8192
  const violations = [];
7806
8193
  const warnings = [];
@@ -7813,7 +8200,7 @@ async function checkQualityBaseline(baselinePath) {
7813
8200
  violations.push(message4(error));
7814
8201
  }
7815
8202
  if (entry.record) {
7816
- const recordPath = path15.resolve(root, entry.record.path);
8203
+ const recordPath = path16.resolve(root, entry.record.path);
7817
8204
  try {
7818
8205
  const recordHash = await sha256File(recordPath);
7819
8206
  if (recordHash !== entry.record.sha256) violations.push("quality record hash changed");
@@ -7850,11 +8237,11 @@ async function checkQualityBaseline(baselinePath) {
7850
8237
 
7851
8238
  // src/pipeline/cache-health.ts
7852
8239
  import { existsSync as existsSync15 } from "fs";
7853
- import { readFile as readFile11, readdir as readdir2, rm as rm6 } from "fs/promises";
7854
- import path18 from "path";
8240
+ import { readFile as readFile12, readdir as readdir2, rm as rm6 } from "fs/promises";
8241
+ import path19 from "path";
7855
8242
 
7856
8243
  // src/cache.ts
7857
- import { mkdir as mkdir3, readFile as readFile9, rename as rename3, rm as rm4, writeFile as writeFile3 } from "fs/promises";
8244
+ import { mkdir as mkdir4, readFile as readFile10, rename as rename4, rm as rm4, writeFile as writeFile4 } from "fs/promises";
7858
8245
  import { existsSync as existsSync12 } from "fs";
7859
8246
  import pathModule from "path";
7860
8247
  import { z as z5 } from "zod";
@@ -7870,10 +8257,10 @@ function isSha256Hash(value) {
7870
8257
  function parseCache(value) {
7871
8258
  return HashCacheSchema.parse(value);
7872
8259
  }
7873
- async function loadCache(path32) {
7874
- if (!existsSync12(path32)) return { version: 1, hashes: {} };
8260
+ async function loadCache(path33) {
8261
+ if (!existsSync12(path33)) return { version: 1, hashes: {} };
7875
8262
  try {
7876
- const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile9(path32, "utf8")));
8263
+ const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile10(path33, "utf8")));
7877
8264
  if (!parsed.success) return { version: 1, hashes: {} };
7878
8265
  return {
7879
8266
  version: 1,
@@ -7885,18 +8272,18 @@ async function loadCache(path32) {
7885
8272
  return { version: 1, hashes: {} };
7886
8273
  }
7887
8274
  }
7888
- async function saveCache(path32, cache) {
8275
+ async function saveCache(path33, cache) {
7889
8276
  const sorted = {};
7890
8277
  for (const key of Object.keys(cache.hashes).sort()) {
7891
8278
  const hash = cache.hashes[key];
7892
8279
  if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
7893
8280
  sorted[key] = hash;
7894
8281
  }
7895
- await mkdir3(pathModule.dirname(pathModule.resolve(path32)), { recursive: true });
7896
- const tmp = `${path32}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
8282
+ await mkdir4(pathModule.dirname(pathModule.resolve(path33)), { recursive: true });
8283
+ const tmp = `${path33}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
7897
8284
  try {
7898
- await writeFile3(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
7899
- await rename3(tmp, path32);
8285
+ await writeFile4(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
8286
+ await rename4(tmp, path33);
7900
8287
  } finally {
7901
8288
  await rm4(tmp, { force: true });
7902
8289
  }
@@ -7921,21 +8308,22 @@ function cachePathFor(lockPath) {
7921
8308
 
7922
8309
  // src/pipeline/history.ts
7923
8310
  import { existsSync as existsSync14 } from "fs";
7924
- import path17 from "path";
8311
+ import path18 from "path";
7925
8312
 
7926
8313
  // src/pipeline/fetch.ts
7927
8314
  import { existsSync as existsSync13, readFileSync as readFileSync3 } from "fs";
7928
- import { mkdir as mkdir4, readFile as readFile10, rename as rename4, rm as rm5, writeFile as writeFile4 } from "fs/promises";
7929
- import path16 from "path";
8315
+ import { mkdir as mkdir5, readFile as readFile11, rename as rename5, rm as rm5, writeFile as writeFile5 } from "fs/promises";
8316
+ import path17 from "path";
7930
8317
  async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
7931
8318
  const log = opts.onProgress ?? (() => {
7932
8319
  });
7933
8320
  const result = { downloaded: 0, skipped: 0, failed: 0, ...opts.refresh ? { unchanged: 0 } : {} };
7934
8321
  const specByKey = new Map(specs.map((s) => [lockKey(s.styleId, s.assetId), s]));
7935
8322
  normalizeLockOutputPaths(lock, specs);
7936
- const cacheDir = opts.cacheDir === false ? null : path16.resolve(opts.cacheDir ?? path16.join(path16.dirname(lockPath), ".pixelkiln", "cache"));
8323
+ const cacheDir = opts.cacheDir === false ? null : path17.resolve(opts.cacheDir ?? path17.join(path17.dirname(lockPath), ".pixelkiln", "cache"));
7937
8324
  const pending = Object.entries(lock.entries).filter(([key, e]) => {
7938
8325
  if (e.provider !== provider.id || !specByKey.has(key)) return false;
8326
+ if (specByKey.get(key)?.mirror) return false;
7939
8327
  if (e.status === "selected" || e.status === "download-failed") return true;
7940
8328
  if (e.status !== "downloaded") return false;
7941
8329
  if (opts.refresh) return e.outputs.length > 0 && Boolean(e.sourceUrls?.length || e.sourceUrl);
@@ -8034,7 +8422,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
8034
8422
  if (!source.url) throw new Error(`no durable source URL to refresh ${source.role ?? "asset"} from`);
8035
8423
  fresh = await provider.download(source.url);
8036
8424
  if (recorded && sha256(fresh) === recordedRaw && !reapply) {
8037
- log(` current ${path16.relative(process.cwd(), target)}`);
8425
+ log(` current ${path17.relative(process.cwd(), target)}`);
8038
8426
  outputs.push({ ...recorded, path: portableOutputPath(target, spec.root) });
8039
8427
  continue;
8040
8428
  }
@@ -8048,7 +8436,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
8048
8436
  if (cacheDir) {
8049
8437
  await cacheMedia(
8050
8438
  cacheDir,
8051
- await readFile10(target),
8439
+ await readFile11(target),
8052
8440
  recorded.mediaType ?? MediaType.PNG,
8053
8441
  recorded.sha256
8054
8442
  );
@@ -8057,10 +8445,10 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
8057
8445
  continue;
8058
8446
  }
8059
8447
  if (reapply && !fresh) {
8060
- if (!recorded.raw) raw = await readFile10(target);
8061
- log(` reapply ${path16.relative(process.cwd(), target)}`);
8448
+ if (!recorded.raw) raw = await readFile11(target);
8449
+ log(` reapply ${path17.relative(process.cwd(), target)}`);
8062
8450
  } else {
8063
- log(` changed ${path16.relative(process.cwd(), target)} (upstream)`);
8451
+ log(` changed ${path17.relative(process.cwd(), target)} (upstream)`);
8064
8452
  }
8065
8453
  } else {
8066
8454
  const superseded = (entry.supersededOutputs ?? []).find(
@@ -8079,7 +8467,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
8079
8467
  }
8080
8468
  }
8081
8469
  log(
8082
- ` replace ${path16.relative(process.cwd(), target)}` + (opts.force ? " (--force)" : " (previous tracked generation)")
8470
+ ` replace ${path17.relative(process.cwd(), target)}` + (opts.force ? " (--force)" : " (previous tracked generation)")
8083
8471
  );
8084
8472
  }
8085
8473
  }
@@ -8088,7 +8476,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
8088
8476
  } else if (!raw && recorded && cacheDir) {
8089
8477
  if (!reapply) final = await readCachedMedia(cacheDir, recorded.sha256, expectedMediaType);
8090
8478
  if (!final && recordedRaw) raw = await readCachedMedia(cacheDir, recordedRaw, expectedMediaType);
8091
- if (final || raw) log(` cached ${path16.relative(process.cwd(), target)}`);
8479
+ if (final || raw) log(` cached ${path17.relative(process.cwd(), target)}`);
8092
8480
  }
8093
8481
  if (!final && !raw) {
8094
8482
  if (!source.url) {
@@ -8115,13 +8503,13 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
8115
8503
  buf = processed.bytes;
8116
8504
  if (processed.changed) {
8117
8505
  rawHash = sha256(raw);
8118
- log(` snapped ${path16.relative(process.cwd(), target)} to ${wanted.palette.colors.length} colours`);
8506
+ log(` snapped ${path17.relative(process.cwd(), target)} to ${wanted.palette.colors.length} colours`);
8119
8507
  }
8120
8508
  }
8121
8509
  if (cacheDir) await cacheMedia(cacheDir, buf, mediaType);
8122
- await mkdir4(path16.dirname(target), { recursive: true });
8510
+ await mkdir5(path17.dirname(target), { recursive: true });
8123
8511
  const tmp = `${target}.pixelkiln.tmp`;
8124
- await writeFile4(tmp, buf);
8512
+ await writeFile5(tmp, buf);
8125
8513
  outputs.push({
8126
8514
  path: portableOutputPath(target, spec.root),
8127
8515
  sha256: sha256(buf),
@@ -8131,9 +8519,9 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
8131
8519
  });
8132
8520
  upsert(lock, key, { outputs: mergeOutputs(entry.outputs, outputs) });
8133
8521
  await saveLock(lockPath, lock);
8134
- await rename4(tmp, target);
8522
+ await rename5(tmp, target);
8135
8523
  wrote = true;
8136
- log(` wrote ${path16.relative(process.cwd(), target)}`);
8524
+ log(` wrote ${path17.relative(process.cwd(), target)}`);
8137
8525
  }
8138
8526
  if (opts.refresh && !wrote) {
8139
8527
  if (rotated) {
@@ -8177,10 +8565,10 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
8177
8565
  return result;
8178
8566
  }
8179
8567
  async function readCachedMedia(cacheDir, hash, mediaType) {
8180
- const file = path16.join(cacheDir, cacheFileName(hash, mediaType));
8568
+ const file = path17.join(cacheDir, cacheFileName(hash, mediaType));
8181
8569
  if (!existsSync13(file)) return null;
8182
8570
  try {
8183
- const buf = await readFile10(file);
8571
+ const buf = await readFile11(file);
8184
8572
  if (sha256(buf) !== hash) return null;
8185
8573
  validateMedia(buf, mediaType);
8186
8574
  return buf;
@@ -8191,13 +8579,13 @@ async function readCachedMedia(cacheDir, hash, mediaType) {
8191
8579
  async function cacheMedia(cacheDir, buf, mediaType, knownHash) {
8192
8580
  const hash = knownHash ?? sha256(buf);
8193
8581
  validateMedia(buf, mediaType);
8194
- const file = path16.join(cacheDir, cacheFileName(hash, mediaType));
8582
+ const file = path17.join(cacheDir, cacheFileName(hash, mediaType));
8195
8583
  if (await readCachedMedia(cacheDir, hash, mediaType)) return;
8196
- await mkdir4(cacheDir, { recursive: true });
8584
+ await mkdir5(cacheDir, { recursive: true });
8197
8585
  const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
8198
- await writeFile4(tmp, buf);
8586
+ await writeFile5(tmp, buf);
8199
8587
  try {
8200
- await rename4(tmp, file);
8588
+ await rename5(tmp, file);
8201
8589
  } catch (err) {
8202
8590
  if (!existsSync13(file)) throw err;
8203
8591
  } finally {
@@ -8320,7 +8708,7 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
8320
8708
  if (!existsSync14(file)) continue;
8321
8709
  if (await sha256File(file) !== entry.outputs[i].sha256 && !opts.force) {
8322
8710
  throw new OverwriteRefusedError(
8323
- `${path17.relative(process.cwd(), file)} was changed after download; keep it with pixelkiln edit, or pass --force to replace it`,
8711
+ `${path18.relative(process.cwd(), file)} was changed after download; keep it with pixelkiln edit, or pass --force to replace it`,
8324
8712
  [file]
8325
8713
  );
8326
8714
  }
@@ -8372,10 +8760,10 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
8372
8760
  // src/pipeline/cache-health.ts
8373
8761
  async function inspectCaches(lock, lockPath, options = {}) {
8374
8762
  if (options.prune && !existsSync15(lockPath)) {
8375
- throw new Error(`Refusing to prune without an existing lockfile at ${path18.resolve(lockPath)}`);
8763
+ throw new Error(`Refusing to prune without an existing lockfile at ${path19.resolve(lockPath)}`);
8376
8764
  }
8377
- const contentDir = path18.resolve(path18.dirname(lockPath), ".pixelkiln", "cache");
8378
- const remotePath = path18.resolve(cachePathFor(lockPath));
8765
+ const contentDir = path19.resolve(path19.dirname(lockPath), ".pixelkiln", "cache");
8766
+ const remotePath = path19.resolve(cachePathFor(lockPath));
8379
8767
  const referenced = referencedHashes(lock);
8380
8768
  let content = await inspectContentCache(contentDir, referenced);
8381
8769
  let remoteHashes = await inspectRemoteHashCache(remotePath);
@@ -8387,7 +8775,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
8387
8775
  ]);
8388
8776
  for (const name of names) {
8389
8777
  try {
8390
- await rm6(path18.join(contentDir, name), { force: true });
8778
+ await rm6(path19.join(contentDir, name), { force: true });
8391
8779
  removed.contentFiles++;
8392
8780
  } catch {
8393
8781
  }
@@ -8396,7 +8784,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
8396
8784
  await saveCache(remotePath, { version: 1, hashes: {} });
8397
8785
  removed.resetRemoteHashCache = true;
8398
8786
  } else if (remoteHashes.invalidIds.length) {
8399
- const cache = parseCache(JSON.parse(await readFile11(remotePath, "utf8")));
8787
+ const cache = parseCache(JSON.parse(await readFile12(remotePath, "utf8")));
8400
8788
  for (const id of remoteHashes.invalidIds) delete cache.hashes[id];
8401
8789
  removed.remoteHashEntries = remoteHashes.invalidIds.length;
8402
8790
  await saveCache(remotePath, cache);
@@ -8435,12 +8823,12 @@ async function inspectContentCache(contentDir, referenced) {
8435
8823
  continue;
8436
8824
  }
8437
8825
  report.files++;
8438
- const file = path18.join(contentDir, entry.name);
8826
+ const file = path19.join(contentDir, entry.name);
8439
8827
  const mediaType = mediaTypeFromExtension(entry.name);
8440
8828
  const expected = mediaType ? entry.name.slice(0, -4) : "";
8441
8829
  let bytes;
8442
8830
  try {
8443
- bytes = await readFile11(file);
8831
+ bytes = await readFile12(file);
8444
8832
  report.bytes += bytes.length;
8445
8833
  } catch (err) {
8446
8834
  report.invalid.push({
@@ -8487,7 +8875,7 @@ async function inspectRemoteHashCache(remotePath) {
8487
8875
  if (!report.exists) return report;
8488
8876
  let cache;
8489
8877
  try {
8490
- cache = parseCache(JSON.parse(await readFile11(remotePath, "utf8")));
8878
+ cache = parseCache(JSON.parse(await readFile12(remotePath, "utf8")));
8491
8879
  } catch (err) {
8492
8880
  report.error = err instanceof Error ? err.message : String(err);
8493
8881
  return report;
@@ -8503,19 +8891,19 @@ async function inspectRemoteHashCache(remotePath) {
8503
8891
 
8504
8892
  // src/pipeline/pack.ts
8505
8893
  import { readFileSync as readFileSync4 } from "fs";
8506
- import path19 from "path";
8894
+ import path20 from "path";
8507
8895
  var DEFAULT_FRAME_SET_FPS = 12;
8508
8896
  function resolvePackInputs(raw, inputsFilePath) {
8509
8897
  if (!Array.isArray(raw) || !raw.length) {
8510
8898
  throw new Error("--inputs must be a non-empty JSON array of { id, path }");
8511
8899
  }
8512
- const baseDir = path19.dirname(path19.resolve(inputsFilePath));
8900
+ const baseDir = path20.dirname(path20.resolve(inputsFilePath));
8513
8901
  return raw.map((entry, i) => {
8514
8902
  const e = entry;
8515
8903
  if (typeof e.id !== "string" || typeof e.path !== "string") {
8516
8904
  throw new Error(`--inputs[${i}] needs string "id" and "path"`);
8517
8905
  }
8518
- return { id: e.id, path: path19.resolve(baseDir, e.path) };
8906
+ return { id: e.id, path: path20.resolve(baseDir, e.path) };
8519
8907
  });
8520
8908
  }
8521
8909
  function packSprites(inputs, options = {}) {
@@ -8587,7 +8975,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
8587
8975
  }
8588
8976
  const inputs2 = Object.entries(options.sourceOverrides).map(([id, source]) => ({
8589
8977
  id,
8590
- path: path19.resolve(source)
8978
+ path: path20.resolve(source)
8591
8979
  }));
8592
8980
  if (!inputs2.length) throw new Error(`No approved quality outputs for style "${styleId}".`);
8593
8981
  const packed2 = packSprites(inputs2, options);
@@ -8611,7 +8999,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
8611
8999
  let outputs = resolveEntryOutputs(entry, id, manifestDir);
8612
9000
  const source = sources[id];
8613
9001
  if (source && outputs.length <= 1) {
8614
- inputs.push({ id, path: path19.resolve(manifestDir, source) });
9002
+ inputs.push({ id, path: path20.resolve(manifestDir, source) });
8615
9003
  continue;
8616
9004
  }
8617
9005
  if (source && sourceIsStem(source, entry, manifestDir)) {
@@ -8645,7 +9033,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
8645
9033
  }
8646
9034
  }
8647
9035
  for (const [id, source] of Object.entries(sources)) {
8648
- if (!locked.has(id)) inputs.push({ id, path: path19.resolve(manifestDir, source) });
9036
+ if (!locked.has(id)) inputs.push({ id, path: path20.resolve(manifestDir, source) });
8649
9037
  }
8650
9038
  if (!inputs.length) {
8651
9039
  throw new Error(`No readable sprites for style "${styleId}"; ${noOutput.length} skipped.`);
@@ -8753,7 +9141,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
8753
9141
  const source = sources[id];
8754
9142
  const entry = lock.entries[`${styleId}/${id}`];
8755
9143
  if (source && !(entry && sourceIsStem(source, entry, manifestDir))) {
8756
- placements.push({ id, path: path19.resolve(manifestDir, source), cell });
9144
+ placements.push({ id, path: path20.resolve(manifestDir, source), cell });
8757
9145
  continue;
8758
9146
  }
8759
9147
  if (source && entry) {
@@ -8786,7 +9174,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
8786
9174
  `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}`
8787
9175
  );
8788
9176
  }
8789
- const basePng = mount.base ? readFileSync4(path19.resolve(manifestDir, mount.base)) : void 0;
9177
+ const basePng = mount.base ? readFileSync4(path20.resolve(manifestDir, mount.base)) : void 0;
8790
9178
  const mounted = mountSprites(placements, {
8791
9179
  cellWidth: mount.cellWidth,
8792
9180
  cellHeight: mount.cellHeight,
@@ -8796,7 +9184,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
8796
9184
  ...mounted,
8797
9185
  sources: mount.base ? [{
8798
9186
  id: "$base",
8799
- path: path19.resolve(manifestDir, mount.base),
9187
+ path: path20.resolve(manifestDir, mount.base),
8800
9188
  sha256: sha256(basePng),
8801
9189
  included: true
8802
9190
  }, ...mounted.sources] : mounted.sources,
@@ -9169,7 +9557,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
9169
9557
  });
9170
9558
  const estimates = new Map(items.map((item) => [
9171
9559
  item.key,
9172
- validateCostEstimate(provider.id, provider.estimate(item.spec))
9560
+ item.spec.mirror ? { amount: 0, unit: item.spec.costUnit, candidates: 1 } : validateCostEstimate(provider.id, provider.estimate(item.spec))
9173
9561
  ]));
9174
9562
  const units = new Set([...estimates.values()].map((estimate) => estimate.unit));
9175
9563
  if (units.size > 1) {
@@ -9219,6 +9607,34 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
9219
9607
  }
9220
9608
  for (const { spec, key } of items) {
9221
9609
  const estimate = estimates.get(key);
9610
+ if (spec.mirror) {
9611
+ try {
9612
+ await requireRevisionReady(spec, lock);
9613
+ await deriveMirror(spec, lock, lockPath, { onProgress: log });
9614
+ submitted++;
9615
+ } catch (err) {
9616
+ const message6 = err instanceof Error ? err.message : String(err);
9617
+ failed++;
9618
+ upsert(lock, key, {
9619
+ styleId: spec.styleId,
9620
+ assetId: spec.assetId,
9621
+ specHash: spec.specHash,
9622
+ generator: spec.generator,
9623
+ prompt: "",
9624
+ width: spec.width,
9625
+ height: spec.height,
9626
+ status: "failed",
9627
+ error: message6,
9628
+ outputs: lock.entries[key]?.outputs ?? [],
9629
+ cost: 0,
9630
+ costUnit: spec.costUnit,
9631
+ provider: provider.id
9632
+ });
9633
+ await saveLock(lockPath, lock);
9634
+ log(` FAILED ${key}: ${message6}`);
9635
+ }
9636
+ continue;
9637
+ }
9222
9638
  await waitForSlot();
9223
9639
  const since = Date.now() - lastSubmitAt;
9224
9640
  if (since < spacing) await sleep(spacing - since);
@@ -9430,7 +9846,7 @@ async function poll(provider, lock, lockPath, opts = {}) {
9430
9846
  // src/pipeline/doctor.ts
9431
9847
  import { constants, existsSync as existsSync16 } from "fs";
9432
9848
  import { access, stat as stat2 } from "fs/promises";
9433
- import path20 from "path";
9849
+ import path21 from "path";
9434
9850
  async function doctor(loaded, specs, lock, lockPath, opts = {}) {
9435
9851
  const checks = [];
9436
9852
  const add = (id, level, message6) => checks.push({ id, level, message: message6 });
@@ -9441,8 +9857,8 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
9441
9857
  );
9442
9858
  add("lock", "ok", `${Object.keys(lock.entries).length} valid v2 lock entr(ies)`);
9443
9859
  const dirs = [...new Set(specs.flatMap((spec) => [
9444
- path20.dirname(spec.outFile),
9445
- ...spec.quality ? [path20.dirname(spec.quality.outFile)] : []
9860
+ path21.dirname(spec.outFile),
9861
+ ...spec.quality ? [path21.dirname(spec.quality.outFile)] : []
9446
9862
  ]))];
9447
9863
  const unwritable = [];
9448
9864
  for (const dir of dirs) {
@@ -9465,7 +9881,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
9465
9881
  for (let index = 0; index < entry.outputs.length; index++) {
9466
9882
  const output = entry.outputs[index];
9467
9883
  const spec = specByKey.get(key);
9468
- const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, path20.dirname(lockPath));
9884
+ const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, path21.dirname(lockPath));
9469
9885
  const owner = outputOwners.get(absolute);
9470
9886
  if (owner && owner !== key) duplicateOutputs.push(`${output.path} (${owner}, ${key})`);
9471
9887
  outputOwners.set(absolute, key);
@@ -9480,7 +9896,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
9480
9896
  } else {
9481
9897
  add("lock-outputs", "ok", `${outputOwners.size} recorded output path(s) are uniquely owned`);
9482
9898
  }
9483
- const cacheDir = path20.join(path20.dirname(lockPath), ".pixelkiln", "cache");
9899
+ const cacheDir = path21.join(path21.dirname(lockPath), ".pixelkiln", "cache");
9484
9900
  const stranded = [];
9485
9901
  const invalidState = [];
9486
9902
  const staleMapUrls = [];
@@ -9496,7 +9912,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
9496
9912
  const hasSource = Boolean(entry.sourceUrl || entry.sourceUrls?.length);
9497
9913
  let hasCache = false;
9498
9914
  for (const output of entry.outputs) {
9499
- const cached = path20.join(
9915
+ const cached = path21.join(
9500
9916
  cacheDir,
9501
9917
  cacheFileName(output.sha256, output.mediaType ?? MediaType.PNG)
9502
9918
  );
@@ -9596,7 +10012,7 @@ async function nearestExistingDirectory(start) {
9596
10012
  if ((await stat2(current)).isDirectory()) return current;
9597
10013
  } catch {
9598
10014
  }
9599
- const parent = path20.dirname(current);
10015
+ const parent = path21.dirname(current);
9600
10016
  if (parent === current) return current;
9601
10017
  current = parent;
9602
10018
  }
@@ -9604,8 +10020,8 @@ async function nearestExistingDirectory(start) {
9604
10020
 
9605
10021
  // src/pipeline/adopt.ts
9606
10022
  import { existsSync as existsSync17 } from "fs";
9607
- import { readFile as readFile12, writeFile as writeFile5 } from "fs/promises";
9608
- import path21 from "path";
10023
+ import { readFile as readFile13, writeFile as writeFile6 } from "fs/promises";
10024
+ import path22 from "path";
9609
10025
  async function adopt(provider, specs, lock, lockPath, opts = {}) {
9610
10026
  const log = opts.onProgress ?? (() => {
9611
10027
  });
@@ -9620,7 +10036,7 @@ async function adopt(provider, specs, lock, lockPath, opts = {}) {
9620
10036
  unmatchedLocal.push(`${spec.styleId}/${spec.assetId} (no file at ${spec.outFile})`);
9621
10037
  continue;
9622
10038
  }
9623
- const bytes = await readFile12(spec.outFile);
10039
+ const bytes = await readFile13(spec.outFile);
9624
10040
  try {
9625
10041
  decodePng(bytes);
9626
10042
  } catch (err) {
@@ -9739,12 +10155,12 @@ async function tagAdopted(provider, specs, lock, opts = {}) {
9739
10155
 
9740
10156
  // src/pick/server.ts
9741
10157
  import { existsSync as existsSync19 } from "fs";
9742
- import { readFile as readFile15 } from "fs/promises";
10158
+ import { readFile as readFile16 } from "fs/promises";
9743
10159
 
9744
10160
  // src/pipeline/init.ts
9745
- import { readdir as readdir3, readFile as readFile13, writeFile as writeFile6 } from "fs/promises";
10161
+ import { readdir as readdir3, readFile as readFile14, writeFile as writeFile7 } from "fs/promises";
9746
10162
  import { existsSync as existsSync18 } from "fs";
9747
- import path22 from "path";
10163
+ import path23 from "path";
9748
10164
  function pngSize(buf) {
9749
10165
  if (buf.length < 24) return null;
9750
10166
  const isPng = buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834;
@@ -9758,7 +10174,7 @@ function slugify(value) {
9758
10174
  async function walk(dir, exclude) {
9759
10175
  const out = [];
9760
10176
  for (const entry of await readdir3(dir, { withFileTypes: true })) {
9761
- const full = path22.join(dir, entry.name);
10177
+ const full = path23.join(dir, entry.name);
9762
10178
  if (exclude.some((x) => full.includes(x))) continue;
9763
10179
  if (entry.isDirectory()) out.push(...await walk(full, exclude));
9764
10180
  else if (entry.name.toLowerCase().endsWith(".png")) out.push(full);
@@ -9772,14 +10188,14 @@ async function scanAssets(root, opts = {}) {
9772
10188
  const skipped = [];
9773
10189
  const seen = /* @__PURE__ */ new Map();
9774
10190
  for (const file of files.sort()) {
9775
- const size = pngSize(await readFile13(file));
10191
+ const size = pngSize(await readFile14(file));
9776
10192
  if (!size) {
9777
10193
  skipped.push(`${file} (not a readable PNG)`);
9778
10194
  continue;
9779
10195
  }
9780
- const rel = path22.relative(root, file);
9781
- const category = path22.dirname(rel) === "." ? "" : path22.dirname(rel);
9782
- let id = slugify(path22.basename(rel));
10196
+ const rel = path23.relative(root, file);
10197
+ const category = path23.dirname(rel) === "." ? "" : path23.dirname(rel);
10198
+ let id = slugify(path23.basename(rel));
9783
10199
  if (seen.has(id)) {
9784
10200
  const n = seen.get(id) + 1;
9785
10201
  seen.set(id, n);
@@ -10230,7 +10646,7 @@ document.getElementById('submit').onclick = async () => {
10230
10646
 
10231
10647
  // src/pick/review-server.ts
10232
10648
  import { createServer } from "http";
10233
- import { readFile as readFile14 } from "fs/promises";
10649
+ import { readFile as readFile15 } from "fs/promises";
10234
10650
  function serveReviewPage(opts) {
10235
10651
  const log = opts.onProgress ?? (() => {
10236
10652
  });
@@ -10239,7 +10655,7 @@ function serveReviewPage(opts) {
10239
10655
  if (req.method === "GET" && req.url && opts.assets?.has(req.url)) {
10240
10656
  try {
10241
10657
  const asset = opts.assets.get(req.url);
10242
- const bytes = await readFile14(asset.path);
10658
+ const bytes = await readFile15(asset.path);
10243
10659
  res.writeHead(200, {
10244
10660
  "Content-Type": asset.contentType,
10245
10661
  "Cache-Control": "no-store",
@@ -10321,7 +10737,7 @@ async function currentArt(entry, spec) {
10321
10737
  let width = entry.width;
10322
10738
  let height = entry.height;
10323
10739
  if (contentType === "image/png") {
10324
- const size = pngSize(await readFile15(file));
10740
+ const size = pngSize(await readFile16(file));
10325
10741
  if (size) ({ width, height } = size);
10326
10742
  }
10327
10743
  return { path: file, contentType, width, height };
@@ -10487,13 +10903,13 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
10487
10903
  // src/gallery/snapshot.ts
10488
10904
  import { createHash as createHash4 } from "crypto";
10489
10905
  import { existsSync as existsSync22 } from "fs";
10490
- import { readFile as readFile19, stat as stat3 } from "fs/promises";
10491
- import path27 from "path";
10906
+ import { readFile as readFile20, stat as stat3 } from "fs/promises";
10907
+ import path28 from "path";
10492
10908
 
10493
10909
  // src/workspace.ts
10494
- import { mkdir as mkdir5, readFile as readFile16, rename as rename5, rm as rm7, writeFile as writeFile7 } from "fs/promises";
10910
+ import { mkdir as mkdir6, readFile as readFile17, rename as rename6, rm as rm7, writeFile as writeFile8 } from "fs/promises";
10495
10911
  import { existsSync as existsSync20 } from "fs";
10496
- import path23 from "path";
10912
+ import path24 from "path";
10497
10913
  import { z as z6 } from "zod";
10498
10914
  var WorkspaceProjectSchema = z6.object({
10499
10915
  id: z6.string().min(1),
@@ -10521,7 +10937,7 @@ async function loadWorkspace(workspacePath) {
10521
10937
  if (!existsSync20(workspacePath)) return { version: 1, projects: [] };
10522
10938
  let raw;
10523
10939
  try {
10524
- raw = JSON.parse(await readFile16(workspacePath, "utf8"));
10940
+ raw = JSON.parse(await readFile17(workspacePath, "utf8"));
10525
10941
  } catch (err) {
10526
10942
  throw new ProjectError(
10527
10943
  `Workspace catalog at ${workspacePath} is malformed:
@@ -10535,22 +10951,22 @@ async function saveWorkspace(workspacePath, ws) {
10535
10951
  version: 1,
10536
10952
  projects: [...ws.projects].sort((a, b) => a.id.localeCompare(b.id))
10537
10953
  };
10538
- await mkdir5(path23.dirname(path23.resolve(workspacePath)), { recursive: true });
10954
+ await mkdir6(path24.dirname(path24.resolve(workspacePath)), { recursive: true });
10539
10955
  const tmp = `${workspacePath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
10540
10956
  try {
10541
- await writeFile7(tmp, JSON.stringify(sorted, null, 2) + "\n");
10542
- await rename5(tmp, workspacePath);
10957
+ await writeFile8(tmp, JSON.stringify(sorted, null, 2) + "\n");
10958
+ await rename6(tmp, workspacePath);
10543
10959
  } finally {
10544
10960
  await rm7(tmp, { force: true });
10545
10961
  }
10546
10962
  }
10547
10963
  function toPortablePath(dir, absolute) {
10548
- return path23.relative(dir, absolute).split(path23.sep).join("/");
10964
+ return path24.relative(dir, absolute).split(path24.sep).join("/");
10549
10965
  }
10550
10966
  function resolveProject(dir, project) {
10551
10967
  return {
10552
- manifestPath: path23.resolve(dir, project.manifest.split("/").join(path23.sep)),
10553
- lockPath: path23.resolve(dir, project.lock.split("/").join(path23.sep))
10968
+ manifestPath: path24.resolve(dir, project.manifest.split("/").join(path24.sep)),
10969
+ lockPath: path24.resolve(dir, project.lock.split("/").join(path24.sep))
10554
10970
  };
10555
10971
  }
10556
10972
  function validateWorkspace(ws, dir) {
@@ -10563,7 +10979,7 @@ function validateWorkspace(ws, dir) {
10563
10979
  const { manifestPath, lockPath } = resolveProject(dir, project);
10564
10980
  lockOwners.set(lockPath, [...lockOwners.get(lockPath) ?? [], project.id]);
10565
10981
  manifestOwners.set(manifestPath, [...manifestOwners.get(manifestPath) ?? [], project.id]);
10566
- if (path23.isAbsolute(project.manifest) || path23.isAbsolute(project.lock)) {
10982
+ if (path24.isAbsolute(project.manifest) || path24.isAbsolute(project.lock)) {
10567
10983
  diagnostics.push({
10568
10984
  id: "absolute-path",
10569
10985
  level: "warning",
@@ -10624,12 +11040,12 @@ function validateWorkspace(ws, dir) {
10624
11040
  }
10625
11041
 
10626
11042
  // src/gallery/edit.ts
10627
- import path26 from "path";
11043
+ import path27 from "path";
10628
11044
  import { z as z9 } from "zod";
10629
11045
 
10630
11046
  // src/manifest-edit.ts
10631
- import { readFile as readFile17, rename as rename6, rm as rm8, writeFile as writeFile8 } from "fs/promises";
10632
- import path24 from "path";
11047
+ import { readFile as readFile18, rename as rename7, rm as rm8, writeFile as writeFile9 } from "fs/promises";
11048
+ import path25 from "path";
10633
11049
  import { z as z7 } from "zod";
10634
11050
  var HexSha = z7.string().regex(/^[0-9a-f]{64}$/);
10635
11051
  var AssetPatchSchema = z7.object({
@@ -10844,8 +11260,8 @@ function applyEdit(raw, edit) {
10844
11260
  }
10845
11261
  }
10846
11262
  async function applyManifestEdit(manifestPath, edit) {
10847
- const absolute = path24.resolve(manifestPath);
10848
- const original = await readFile17(absolute, "utf8");
11263
+ const absolute = path25.resolve(manifestPath);
11264
+ const original = await readFile18(absolute, "utf8");
10849
11265
  const before = sha256(original);
10850
11266
  if (before !== edit.expectedSha256) throw new ManifestDriftError(absolute);
10851
11267
  let raw;
@@ -10859,12 +11275,12 @@ async function applyManifestEdit(manifestPath, edit) {
10859
11275
  applyEdit(raw, edit);
10860
11276
  const next = serializeLike(original, raw);
10861
11277
  if (next === original) return { manifestPath: absolute, sha256: before, changed: false };
10862
- const tmp = path24.join(
10863
- path24.dirname(absolute),
10864
- `.${path24.basename(absolute)}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`
11278
+ const tmp = path25.join(
11279
+ path25.dirname(absolute),
11280
+ `.${path25.basename(absolute)}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`
10865
11281
  );
10866
11282
  try {
10867
- await writeFile8(tmp, next);
11283
+ await writeFile9(tmp, next);
10868
11284
  try {
10869
11285
  const loaded = await loadManifest(tmp);
10870
11286
  await resolveSpecs(loaded);
@@ -10872,7 +11288,7 @@ async function applyManifestEdit(manifestPath, edit) {
10872
11288
  const message6 = error instanceof Error ? error.message : String(error);
10873
11289
  throw new ManifestEditError(message6.replace(tmp, absolute));
10874
11290
  }
10875
- await rename6(tmp, absolute);
11291
+ await rename7(tmp, absolute);
10876
11292
  } finally {
10877
11293
  await rm8(tmp, { force: true });
10878
11294
  }
@@ -10881,17 +11297,17 @@ async function applyManifestEdit(manifestPath, edit) {
10881
11297
 
10882
11298
  // src/pipeline/hand-edit.ts
10883
11299
  import { existsSync as existsSync21 } from "fs";
10884
- import { copyFile, mkdir as mkdir6, readFile as readFile18, rename as rename7, rm as rm9, writeFile as writeFile9 } from "fs/promises";
10885
- import path25 from "path";
11300
+ import { copyFile, mkdir as mkdir7, readFile as readFile19, rename as rename8, rm as rm9, writeFile as writeFile10 } from "fs/promises";
11301
+ import path26 from "path";
10886
11302
  import { z as z8 } from "zod";
10887
11303
  var HAND_EDIT_DIR = "edits";
10888
11304
  function handEditPath(loaded, spec) {
10889
11305
  const style = loaded.manifest.styles[spec.styleId];
10890
- const outDir = path25.resolve(loaded.root, style?.outDir ?? ".");
10891
- const relative = path25.relative(outDir, spec.outFile);
10892
- const inside = relative && !relative.startsWith("..") && !path25.isAbsolute(relative);
10893
- const file = inside ? relative : path25.basename(spec.outFile);
10894
- return path25.join(outDir, HAND_EDIT_DIR, file.replace(/\.gif$/i, ".png"));
11306
+ const outDir = path26.resolve(loaded.root, style?.outDir ?? ".");
11307
+ const relative = path26.relative(outDir, spec.outFile);
11308
+ const inside = relative && !relative.startsWith("..") && !path26.isAbsolute(relative);
11309
+ const file = inside ? relative : path26.basename(spec.outFile);
11310
+ return path26.join(outDir, HAND_EDIT_DIR, file.replace(/\.gif$/i, ".png"));
10895
11311
  }
10896
11312
  function handEditBases(spec, lock) {
10897
11313
  const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
@@ -10937,7 +11353,7 @@ async function startHandEdit(loaded, lock, spec, opts = {}) {
10937
11353
  `${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`
10938
11354
  );
10939
11355
  }
10940
- const existing = spec.source ? path25.resolve(loaded.root, spec.source) : null;
11356
+ const existing = spec.source ? path26.resolve(loaded.root, spec.source) : null;
10941
11357
  const editPath = existing ?? handEditPath(loaded, spec);
10942
11358
  const members = handEditMembers(loaded, spec, lock, editPath);
10943
11359
  let created = false;
@@ -10948,7 +11364,7 @@ async function startHandEdit(loaded, lock, spec, opts = {}) {
10948
11364
  `${spec.styleId}/${spec.assetId} has no generated PNG on disk to start from; generate or restore it first`
10949
11365
  );
10950
11366
  }
10951
- await mkdir6(path25.dirname(member.path), { recursive: true });
11367
+ await mkdir7(path26.dirname(member.path), { recursive: true });
10952
11368
  await copyFile(member.base, member.path);
10953
11369
  created = true;
10954
11370
  }
@@ -11027,7 +11443,7 @@ function handEditProjectPath(editPath) {
11027
11443
  async function readHandEditCompanion(editPath) {
11028
11444
  let text;
11029
11445
  try {
11030
- text = await readFile18(handEditCompanionPath(editPath), "utf8");
11446
+ text = await readFile19(handEditCompanionPath(editPath), "utf8");
11031
11447
  } catch {
11032
11448
  return null;
11033
11449
  }
@@ -11070,7 +11486,7 @@ async function saveHandEdit(loaded, lock, spec, input) {
11070
11486
  }
11071
11487
  });
11072
11488
  const bases = handEditBases(spec, lock);
11073
- const expectedSize = bases?.[0] ? decodePng(await readFile18(bases[0].path)) : { width: spec.width, height: spec.height };
11489
+ const expectedSize = bases?.[0] ? decodePng(await readFile19(bases[0].path)) : { width: spec.width, height: spec.height };
11074
11490
  for (const [index, image] of decoded.entries()) {
11075
11491
  if (image.width !== expectedSize.width || image.height !== expectedSize.height) {
11076
11492
  const which = memberSet ? `member ${incoming[index].role ?? index}` : "the edit";
@@ -11086,7 +11502,7 @@ async function saveHandEdit(loaded, lock, spec, input) {
11086
11502
  editor: input.editor,
11087
11503
  protocol: input.protocol,
11088
11504
  savedAt: (input.now ?? /* @__PURE__ */ new Date()).toISOString(),
11089
- project: projectPath ? path25.basename(projectPath) : null,
11505
+ project: projectPath ? path26.basename(projectPath) : null,
11090
11506
  outputs: incoming.map((frame, index) => ({
11091
11507
  role: frame.role,
11092
11508
  basedOn: basedOnHash(entry, index),
@@ -11116,8 +11532,8 @@ function basedOnHash(entry, index) {
11116
11532
  async function replaceFile(file, bytes) {
11117
11533
  const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
11118
11534
  try {
11119
- await writeFile9(tmp, bytes);
11120
- await rename7(tmp, file);
11535
+ await writeFile10(tmp, bytes);
11536
+ await rename8(tmp, file);
11121
11537
  } finally {
11122
11538
  await rm9(tmp, { force: true });
11123
11539
  }
@@ -11216,7 +11632,7 @@ function createGalleryEditHandler(opts) {
11216
11632
  }
11217
11633
  const result = await applyManifestEdit(manifestPath, parsed.data);
11218
11634
  if (result.changed) {
11219
- const relative = path26.relative(process.cwd(), result.manifestPath);
11635
+ const relative = path27.relative(process.cwd(), result.manifestPath);
11220
11636
  const shown = !relative || relative.startsWith("..") ? result.manifestPath : relative;
11221
11637
  const subject = parsed.data.action === "patch-style" ? `style ${parsed.data.styleId}` : parsed.data.action === "set-source" || parsed.data.action === "clear-source" ? `${parsed.data.styleId}/${parsed.data.assetId} source` : parsed.data.assetId;
11222
11638
  log(` manifest edited: ${parsed.data.action === "add-asset" ? "added" : "changed"} ${subject} in ${shown}`);
@@ -11228,7 +11644,7 @@ function createGalleryEditHandler(opts) {
11228
11644
  // src/gallery/snapshot.ts
11229
11645
  var PROJECT_FILE_TYPE = "application/octet-stream";
11230
11646
  function galleryMediaId(absolutePath) {
11231
- return createHash4("sha256").update(path27.resolve(absolutePath)).digest("hex").slice(0, 24);
11647
+ return createHash4("sha256").update(path28.resolve(absolutePath)).digest("hex").slice(0, 24);
11232
11648
  }
11233
11649
  function galleryMediaRoute(id) {
11234
11650
  return `/media/${id}`;
@@ -11247,7 +11663,7 @@ function describeHistory(media, entry, cacheDir) {
11247
11663
  return (entry.history ?? []).map((generation, i) => {
11248
11664
  const outputs = generation.outputs.map((output) => {
11249
11665
  const mediaType = output.mediaType ?? MediaType.PNG;
11250
- const file = path27.join(cacheDir, cacheFileName(output.sha256, mediaType));
11666
+ const file = path28.join(cacheDir, cacheFileName(output.sha256, mediaType));
11251
11667
  let url = null;
11252
11668
  if (existsSync22(file)) {
11253
11669
  const id = galleryMediaId(file);
@@ -11278,7 +11694,7 @@ function describeHistory(media, entry, cacheDir) {
11278
11694
  var MAX_COMPARED_BYTES = 8 * 1024 * 1024;
11279
11695
  async function samePixels(a, b) {
11280
11696
  try {
11281
- const [left, right] = await Promise.all([readFile19(a), readFile19(b)]);
11697
+ const [left, right] = await Promise.all([readFile20(a), readFile20(b)]);
11282
11698
  if (left.length > MAX_COMPARED_BYTES || right.length > MAX_COMPARED_BYTES) return false;
11283
11699
  const x = decodePng(left);
11284
11700
  const y = decodePng(right);
@@ -11348,9 +11764,9 @@ async function describeOutput(media, root, absolutePath, recorded = {}) {
11348
11764
  async function describeQuality(media, root, inspection) {
11349
11765
  const outputPaths = inspection.outputs ?? [inspection.output];
11350
11766
  const outputs = await Promise.all(
11351
- outputPaths.map((file) => describeOutput(media, root, path27.resolve(root, file)))
11767
+ outputPaths.map((file) => describeOutput(media, root, path28.resolve(root, file)))
11352
11768
  );
11353
- const recordPath = path27.resolve(root, inspection.record);
11769
+ const recordPath = path28.resolve(root, inspection.record);
11354
11770
  const recordExists = existsSync22(recordPath);
11355
11771
  let options = null;
11356
11772
  let check = null;
@@ -11370,7 +11786,7 @@ async function describeQuality(media, root, inspection) {
11370
11786
  return {
11371
11787
  state: inspection.state,
11372
11788
  reason: inspection.reason,
11373
- output: portableOutputPath(path27.resolve(root, inspection.output), root),
11789
+ output: portableOutputPath(path28.resolve(root, inspection.output), root),
11374
11790
  record: portableOutputPath(recordPath, root),
11375
11791
  recordExists,
11376
11792
  outputs,
@@ -11407,9 +11823,9 @@ async function buildGallerySnapshot(opts) {
11407
11823
  const media = /* @__PURE__ */ new Map();
11408
11824
  const plan = await buildPlan(specs, lock);
11409
11825
  const items = [];
11410
- const manifestText = await readFile19(loaded.path, "utf8");
11826
+ const manifestText = await readFile20(loaded.path, "utf8");
11411
11827
  const manifestSha256 = sha256(manifestText);
11412
- const cacheDir = path27.resolve(path27.dirname(opts.lockPath), ".pixelkiln", "cache");
11828
+ const cacheDir = path28.resolve(path28.dirname(opts.lockPath), ".pixelkiln", "cache");
11413
11829
  let rawStyles = {};
11414
11830
  try {
11415
11831
  const raw = JSON.parse(manifestText);
@@ -11433,7 +11849,7 @@ async function buildGallerySnapshot(opts) {
11433
11849
  )
11434
11850
  );
11435
11851
  if (spec.source) {
11436
- const editPath = path27.resolve(root, spec.source);
11852
+ const editPath = path28.resolve(root, spec.source);
11437
11853
  const memberCount = sourceIsStem(spec.source, entry, root) ? entry.outputs.length : 1;
11438
11854
  const editShas = await Promise.all(
11439
11855
  Array.from({ length: memberCount }, async (_, index) => {
@@ -11482,7 +11898,7 @@ async function buildGallerySnapshot(opts) {
11482
11898
  editStatus = edits.some((member) => !member.exists) ? "missing" : editChanged.every((changed) => !changed) ? "same" : regenerated ? "regenerated-since" : "edited";
11483
11899
  }
11484
11900
  } else if (spec.source) {
11485
- outputs = [await describeOutput(media, root, path27.resolve(root, spec.source))];
11901
+ outputs = [await describeOutput(media, root, path28.resolve(root, spec.source))];
11486
11902
  } else if (!entry && existsSync22(spec.outFile)) {
11487
11903
  outputs = [await describeOutput(media, root, spec.outFile)];
11488
11904
  } else {
@@ -11522,6 +11938,7 @@ async function buildGallerySnapshot(opts) {
11522
11938
  currentSpecHash: spec.specHash,
11523
11939
  revision: entry?.revision ?? null,
11524
11940
  revisionParentKey: spec.revision ? lockKey(spec.styleId, spec.revision.sourceAssetId) : entry?.revision ? lockKey(spec.styleId, entry.revision.sourceAssetId) : null,
11941
+ mirrorOfKey: spec.mirror ? lockKey(spec.styleId, spec.mirror.sourceAssetId) : entry?.mirror ? lockKey(spec.styleId, entry.mirror.sourceAssetId) : null,
11525
11942
  character: describeCharacter(spec, entry),
11526
11943
  outputs,
11527
11944
  quality,
@@ -11584,6 +12001,7 @@ async function buildGallerySnapshot(opts) {
11584
12001
  currentSpecHash: null,
11585
12002
  revision: entry.revision,
11586
12003
  revisionParentKey: entry.revision ? lockKey(entry.styleId, entry.revision.sourceAssetId) : null,
12004
+ mirrorOfKey: entry.mirror ? lockKey(entry.styleId, entry.mirror.sourceAssetId) : null,
11587
12005
  character: describeCharacter(void 0, entry),
11588
12006
  outputs,
11589
12007
  quality: null,
@@ -11672,7 +12090,7 @@ async function buildGallerySnapshot(opts) {
11672
12090
  id: loaded.manifest.name,
11673
12091
  name: loaded.manifest.name,
11674
12092
  manifest: loaded.path,
11675
- lock: path27.resolve(opts.lockPath),
12093
+ lock: path28.resolve(opts.lockPath),
11676
12094
  root,
11677
12095
  provider: loaded.manifest.provider,
11678
12096
  account: null,
@@ -11698,7 +12116,7 @@ async function buildGallerySnapshot(opts) {
11698
12116
  return { snapshot, media };
11699
12117
  }
11700
12118
  async function buildWorkspaceGallerySnapshot(opts) {
11701
- const dir = path27.dirname(path27.resolve(opts.workspacePath));
12119
+ const dir = path28.dirname(path28.resolve(opts.workspacePath));
11702
12120
  const media = /* @__PURE__ */ new Map();
11703
12121
  const projects = [];
11704
12122
  const items = [];
@@ -11741,11 +12159,11 @@ async function buildWorkspaceGallerySnapshot(opts) {
11741
12159
  id: project.id,
11742
12160
  name: loaded.manifest.name,
11743
12161
  manifest: manifestPath,
11744
- lock: path27.resolve(lockPath),
12162
+ lock: path28.resolve(lockPath),
11745
12163
  root: loaded.root,
11746
12164
  provider: loaded.manifest.provider,
11747
12165
  account: project.account ?? null,
11748
- manifestSha256: manifestSha256 ?? sha256(await readFile19(manifestPath, "utf8")),
12166
+ manifestSha256: manifestSha256 ?? sha256(await readFile20(manifestPath, "utf8")),
11749
12167
  historyLimit: historyLimit(loaded.manifest),
11750
12168
  entries: Object.keys(lock.entries).length,
11751
12169
  items: projectItems.length,
@@ -11757,8 +12175,8 @@ async function buildWorkspaceGallerySnapshot(opts) {
11757
12175
  id: project.id,
11758
12176
  name: project.id,
11759
12177
  manifest: manifestPath,
11760
- lock: path27.resolve(lockPath),
11761
- root: path27.dirname(manifestPath),
12178
+ lock: path28.resolve(lockPath),
12179
+ root: path28.dirname(manifestPath),
11762
12180
  provider: project.provider,
11763
12181
  account: project.account ?? null,
11764
12182
  manifestSha256: null,
@@ -11785,7 +12203,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
11785
12203
  version: 1,
11786
12204
  generatedAt: (opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
11787
12205
  project: null,
11788
- workspace: { path: path27.resolve(opts.workspacePath), dir, projects },
12206
+ workspace: { path: path28.resolve(opts.workspacePath), dir, projects },
11789
12207
  filter,
11790
12208
  totals: {
11791
12209
  entries: projects.reduce((sum, project) => sum + project.entries, 0),
@@ -11935,9 +12353,9 @@ var EDITOR_RELEASE_BASE = "https://github.com/gfargo/pixelkiln/releases/download
11935
12353
  // src/editor/install.ts
11936
12354
  import { createHash as createHash5 } from "crypto";
11937
12355
  import { existsSync as existsSync23 } from "fs";
11938
- import { mkdir as mkdir7, readFile as readFile20, rename as rename8, rm as rm10, stat as stat4, writeFile as writeFile10 } from "fs/promises";
12356
+ import { mkdir as mkdir8, readFile as readFile21, rename as rename9, rm as rm10, stat as stat4, writeFile as writeFile11 } from "fs/promises";
11939
12357
  import os2 from "os";
11940
- import path28 from "path";
12358
+ import path29 from "path";
11941
12359
  var EditorInstallError = class extends Error {
11942
12360
  constructor(message6, file) {
11943
12361
  super(message6);
@@ -11948,15 +12366,15 @@ var EditorInstallError = class extends Error {
11948
12366
  };
11949
12367
  function editorToolsRoot() {
11950
12368
  const override = process.env.PIXELKILN_TOOLS_DIR?.trim();
11951
- if (override) return path28.resolve(override);
11952
- if (process.platform === "darwin") return path28.join(os2.homedir(), "Library", "Caches", "pixelkiln", "tools");
12369
+ if (override) return path29.resolve(override);
12370
+ if (process.platform === "darwin") return path29.join(os2.homedir(), "Library", "Caches", "pixelkiln", "tools");
11953
12371
  if (process.platform === "win32") {
11954
- return path28.join(process.env.LOCALAPPDATA ?? path28.join(os2.homedir(), "AppData", "Local"), "pixelkiln", "tools");
12372
+ return path29.join(process.env.LOCALAPPDATA ?? path29.join(os2.homedir(), "AppData", "Local"), "pixelkiln", "tools");
11955
12373
  }
11956
- return path28.join(process.env.XDG_CACHE_HOME ?? path28.join(os2.homedir(), ".cache"), "pixelkiln", "tools");
12374
+ return path29.join(process.env.XDG_CACHE_HOME ?? path29.join(os2.homedir(), ".cache"), "pixelkiln", "tools");
11957
12375
  }
11958
12376
  function editorDir(pin = EDITOR_PIN, root = editorToolsRoot()) {
11959
- return path28.join(root, "pixelorama", pin.release ?? "unpublished");
12377
+ return path29.join(root, "pixelorama", pin.release ?? "unpublished");
11960
12378
  }
11961
12379
  function editorBaseUrl(pin = EDITOR_PIN) {
11962
12380
  const override = process.env.PIXELKILN_EDITOR_URL?.trim();
@@ -11966,7 +12384,7 @@ function editorBaseUrl(pin = EDITOR_PIN) {
11966
12384
  }
11967
12385
  async function sha256Of(file) {
11968
12386
  try {
11969
- return createHash5("sha256").update(await readFile20(file)).digest("hex");
12387
+ return createHash5("sha256").update(await readFile21(file)).digest("hex");
11970
12388
  } catch {
11971
12389
  return null;
11972
12390
  }
@@ -11979,7 +12397,7 @@ async function editorStatus(opts = {}) {
11979
12397
  let totalBytes = 0;
11980
12398
  for (const [name, expected] of Object.entries(pin.files)) {
11981
12399
  totalBytes += expected.bytes;
11982
- const file = path28.join(dir, name);
12400
+ const file = path29.join(dir, name);
11983
12401
  const info = existsSync23(file) ? await stat4(file).catch(() => null) : null;
11984
12402
  if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
11985
12403
  installedBytes += expected.bytes;
@@ -12009,7 +12427,7 @@ async function installEditor(opts = {}) {
12009
12427
  );
12010
12428
  }
12011
12429
  if (!baseUrl) throw new EditorInstallError("no download location for the editor build");
12012
- await mkdir7(dir, { recursive: true });
12430
+ await mkdir8(dir, { recursive: true });
12013
12431
  const downloaded = [];
12014
12432
  const skipped = Object.keys(pin.files).filter((name) => !before.missing.includes(name));
12015
12433
  let fetchedBytes = before.installedBytes;
@@ -12037,11 +12455,11 @@ async function installEditor(opts = {}) {
12037
12455
  if (digest2 !== expected.sha256) {
12038
12456
  throw new EditorInstallError(`${name}: sha256 ${digest2.slice(0, 12)}\u2026 does not match the pinned ${expected.sha256.slice(0, 12)}\u2026`, name);
12039
12457
  }
12040
- const final = path28.join(dir, name);
12458
+ const final = path29.join(dir, name);
12041
12459
  const tmp = `${final}.${process.pid}.part`;
12042
12460
  try {
12043
- await writeFile10(tmp, bytes);
12044
- await rename8(tmp, final);
12461
+ await writeFile11(tmp, bytes);
12462
+ await rename9(tmp, final);
12045
12463
  } finally {
12046
12464
  await rm10(tmp, { force: true });
12047
12465
  }
@@ -12083,12 +12501,12 @@ var EDITOR_CONTENT_TYPES = {
12083
12501
  ".json": "application/json; charset=utf-8"
12084
12502
  };
12085
12503
  function editorContentType(name) {
12086
- return EDITOR_CONTENT_TYPES[path28.extname(name).toLowerCase()] ?? null;
12504
+ return EDITOR_CONTENT_TYPES[path29.extname(name).toLowerCase()] ?? null;
12087
12505
  }
12088
12506
 
12089
12507
  // src/gallery/editor.ts
12090
- import { readFile as readFile21 } from "fs/promises";
12091
- import path29 from "path";
12508
+ import { readFile as readFile22 } from "fs/promises";
12509
+ import path30 from "path";
12092
12510
  function editorRoute(release, name = "index.html") {
12093
12511
  return `/editor/${encodeURIComponent(release)}/${encodeURIComponent(name)}`;
12094
12512
  }
@@ -12161,7 +12579,7 @@ function createGalleryEditorHandlers(opts = {}) {
12161
12579
  if (!status.installed) return null;
12162
12580
  let bytes;
12163
12581
  try {
12164
- bytes = await readFile21(path29.join(dir, name));
12582
+ bytes = await readFile22(path30.join(dir, name));
12165
12583
  } catch {
12166
12584
  verified = null;
12167
12585
  return null;
@@ -12178,7 +12596,7 @@ function createGalleryEditorHandlers(opts = {}) {
12178
12596
  // src/gallery/server.ts
12179
12597
  import { randomBytes } from "crypto";
12180
12598
  import { createServer as createServer2 } from "http";
12181
- import { readFile as readFile22 } from "fs/promises";
12599
+ import { readFile as readFile23 } from "fs/promises";
12182
12600
  var MAX_EDIT_BYTES = 48 * 1024 * 1024;
12183
12601
  var EDITOR_CSP = [
12184
12602
  "default-src 'self'",
@@ -12337,7 +12755,7 @@ async function serveGallery(opts) {
12337
12755
  const asset = reviewAssets.get(jobId)?.get(url2.pathname);
12338
12756
  if (!asset) return fail(404, "no such review asset");
12339
12757
  try {
12340
- const bytes = await readFile22(asset.path);
12758
+ const bytes = await readFile23(asset.path);
12341
12759
  res.writeHead(200, {
12342
12760
  "Content-Type": asset.contentType,
12343
12761
  "Cache-Control": "no-store",
@@ -12413,7 +12831,7 @@ async function serveGallery(opts) {
12413
12831
  if (!asset) return fail(404, "no such gallery media");
12414
12832
  let bytes;
12415
12833
  try {
12416
- bytes = await readFile22(asset.path);
12834
+ bytes = await readFile23(asset.path);
12417
12835
  } catch {
12418
12836
  return fail(404, "gallery media is no longer on disk; refresh the page");
12419
12837
  }
@@ -12787,9 +13205,9 @@ function createGenerateHandlers(opts) {
12787
13205
  }
12788
13206
 
12789
13207
  // src/pipeline/salvage.ts
12790
- import { readFile as readFile23 } from "fs/promises";
13208
+ import { readFile as readFile24 } from "fs/promises";
12791
13209
  import { existsSync as existsSync24 } from "fs";
12792
- import path30 from "path";
13210
+ import path31 from "path";
12793
13211
  function providerClaimId(provider, objectId) {
12794
13212
  return `${encodeURIComponent(provider)}:${encodeURIComponent(objectId)}`;
12795
13213
  }
@@ -12799,7 +13217,7 @@ async function loadClaims(lockPaths, opts = {}) {
12799
13217
  if (!existsSync24(p)) throw new Error(`Claim lockfile not found: ${p}`);
12800
13218
  let parsed;
12801
13219
  try {
12802
- parsed = parseLock(JSON.parse(await readFile23(p, "utf8")));
13220
+ parsed = parseLock(JSON.parse(await readFile24(p, "utf8")));
12803
13221
  } catch {
12804
13222
  throw new Error(`Claim lockfile is malformed: ${p}`);
12805
13223
  }
@@ -12852,20 +13270,20 @@ function matchOrphanStyle(prompt, manifest) {
12852
13270
  return matchStyleByPattern(prompt, manifest);
12853
13271
  }
12854
13272
  async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, claimPaths) {
12855
- const own = path30.resolve(ownManifestPath);
13273
+ const own = path31.resolve(ownManifestPath);
12856
13274
  const siblingManifestPaths = [
12857
13275
  .../* @__PURE__ */ new Set([
12858
13276
  ...workspaceManifestPaths,
12859
- ...claimPaths.map((c) => path30.join(path30.dirname(path30.resolve(c)), "pixelkiln.manifest.json"))
13277
+ ...claimPaths.map((c) => path31.join(path31.dirname(path31.resolve(c)), "pixelkiln.manifest.json"))
12860
13278
  ])
12861
13279
  ];
12862
13280
  const siblings = [];
12863
13281
  for (const siblingManifestPath of siblingManifestPaths) {
12864
- if (path30.resolve(siblingManifestPath) === own) continue;
13282
+ if (path31.resolve(siblingManifestPath) === own) continue;
12865
13283
  if (!existsSync24(siblingManifestPath)) continue;
12866
13284
  try {
12867
13285
  const { manifest } = await loadManifest(siblingManifestPath);
12868
- siblings.push({ label: path30.basename(path30.dirname(siblingManifestPath)), manifest });
13286
+ siblings.push({ label: path31.basename(path31.dirname(siblingManifestPath)), manifest });
12869
13287
  } catch {
12870
13288
  }
12871
13289
  }
@@ -12981,8 +13399,8 @@ async function applyTags(provider, decisions, existing, opts = {}) {
12981
13399
  }
12982
13400
 
12983
13401
  // src/pick/salvage-server.ts
12984
- import { mkdir as mkdir8, writeFile as writeFile11, readFile as readFile24 } from "fs/promises";
12985
- import path31 from "path";
13402
+ import { mkdir as mkdir9, writeFile as writeFile12, readFile as readFile25 } from "fs/promises";
13403
+ import path32 from "path";
12986
13404
 
12987
13405
  // src/pick/salvage-sheet.ts
12988
13406
  var escapeHtml3 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
@@ -13147,7 +13565,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
13147
13565
  });
13148
13566
  const html = renderSalvageSheet(orphans, {
13149
13567
  styleId: ctx.styleId,
13150
- importDir: path31.relative(process.cwd(), ctx.importDir) || "."
13568
+ importDir: path32.relative(process.cwd(), ctx.importDir) || "."
13151
13569
  });
13152
13570
  const byId = new Map(orphans.map((o) => [o.id, o]));
13153
13571
  const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
@@ -13176,10 +13594,10 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
13176
13594
  if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
13177
13595
  decodePng(buf);
13178
13596
  const assetId = idFromPrompt(orphan.prompt, taken);
13179
- const rel = path31.join("_salvaged", `${assetId}.png`);
13180
- const outFile = path31.resolve(ctx.importDir, rel);
13181
- await mkdir8(path31.dirname(outFile), { recursive: true });
13182
- await writeFile11(outFile, buf);
13597
+ const rel = path32.join("_salvaged", `${assetId}.png`);
13598
+ const outFile = path32.resolve(ctx.importDir, rel);
13599
+ await mkdir9(path32.dirname(outFile), { recursive: true });
13600
+ await writeFile12(outFile, buf);
13183
13601
  ctx.manifest.assets[assetId] = {
13184
13602
  prompt: orphan.prompt,
13185
13603
  promptByStyle: {},
@@ -13211,7 +13629,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
13211
13629
  sourceUrl: durableSource,
13212
13630
  sourceUrls: durableSource ? [{ url: durableSource }] : [],
13213
13631
  outputs: [{
13214
- path: portableOutputPath(outFile, path31.dirname(ctx.manifestPath)),
13632
+ path: portableOutputPath(outFile, path32.dirname(ctx.manifestPath)),
13215
13633
  sha256: sha256(buf)
13216
13634
  }],
13217
13635
  submittedAt: orphan.createdAt,
@@ -13235,9 +13653,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
13235
13653
  }
13236
13654
  }
13237
13655
  await applyTags(provider, decisions, existingTags, { onProgress: log });
13238
- const raw = JSON.parse(await readFile24(ctx.manifestPath, "utf8"));
13656
+ const raw = JSON.parse(await readFile25(ctx.manifestPath, "utf8"));
13239
13657
  for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
13240
- await writeFile11(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
13658
+ await writeFile12(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
13241
13659
  await saveLock(ctx.lockPath, ctx.lock);
13242
13660
  return result;
13243
13661
  }
@@ -13348,10 +13766,12 @@ export {
13348
13766
  BudgetError,
13349
13767
  CHARACTER_DIRECTIONS_4,
13350
13768
  CHARACTER_DIRECTIONS_8,
13769
+ CHARACTER_PROPORTION_PRESETS,
13351
13770
  CharacterAnimationModeSchema,
13352
13771
  CharacterAnimationSchema,
13353
13772
  CharacterDirectionSchema,
13354
13773
  CharacterModeSchema,
13774
+ CharacterProportionsSchema,
13355
13775
  CharacterStateSchema,
13356
13776
  ComfyUIProvider,
13357
13777
  DEFAULT_HISTORY_LIMIT,
@@ -13379,6 +13799,7 @@ export {
13379
13799
  MAX_DOWNLOAD_BYTES,
13380
13800
  MAX_HAND_EDIT_BYTES,
13381
13801
  MAX_RETRIES,
13802
+ MIRRORED_DIRECTION,
13382
13803
  ManifestDriftError,
13383
13804
  ManifestEditError,
13384
13805
  ManifestEditSchema,
@@ -13517,6 +13938,7 @@ export {
13517
13938
  portableOutputPath,
13518
13939
  prepareReview,
13519
13940
  primaryOutput,
13941
+ proportionsPayload,
13520
13942
  providerCredentialEnvs,
13521
13943
  providerFactory,
13522
13944
  pushTags,