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.cjs CHANGED
@@ -34,10 +34,12 @@ __export(index_exports, {
34
34
  BudgetError: () => BudgetError,
35
35
  CHARACTER_DIRECTIONS_4: () => CHARACTER_DIRECTIONS_4,
36
36
  CHARACTER_DIRECTIONS_8: () => CHARACTER_DIRECTIONS_8,
37
+ CHARACTER_PROPORTION_PRESETS: () => CHARACTER_PROPORTION_PRESETS,
37
38
  CharacterAnimationModeSchema: () => CharacterAnimationModeSchema,
38
39
  CharacterAnimationSchema: () => CharacterAnimationSchema,
39
40
  CharacterDirectionSchema: () => CharacterDirectionSchema,
40
41
  CharacterModeSchema: () => CharacterModeSchema,
42
+ CharacterProportionsSchema: () => CharacterProportionsSchema,
41
43
  CharacterStateSchema: () => CharacterStateSchema,
42
44
  ComfyUIProvider: () => ComfyUIProvider,
43
45
  DEFAULT_HISTORY_LIMIT: () => DEFAULT_HISTORY_LIMIT,
@@ -65,6 +67,7 @@ __export(index_exports, {
65
67
  MAX_DOWNLOAD_BYTES: () => MAX_DOWNLOAD_BYTES,
66
68
  MAX_HAND_EDIT_BYTES: () => MAX_HAND_EDIT_BYTES,
67
69
  MAX_RETRIES: () => MAX_RETRIES,
70
+ MIRRORED_DIRECTION: () => MIRRORED_DIRECTION,
68
71
  ManifestDriftError: () => ManifestDriftError,
69
72
  ManifestEditError: () => ManifestEditError,
70
73
  ManifestEditSchema: () => ManifestEditSchema,
@@ -203,6 +206,7 @@ __export(index_exports, {
203
206
  portableOutputPath: () => portableOutputPath,
204
207
  prepareReview: () => prepareReview,
205
208
  primaryOutput: () => primaryOutput,
209
+ proportionsPayload: () => proportionsPayload,
206
210
  providerCredentialEnvs: () => providerCredentialEnvs,
207
211
  providerFactory: () => providerFactory,
208
212
  pushTags: () => pushTags,
@@ -375,7 +379,28 @@ var CHARACTER_DIRECTIONS_8 = [
375
379
  "south-west"
376
380
  ];
377
381
  var CHARACTER_DIRECTIONS_4 = ["south", "west", "east", "north"];
382
+ var MIRRORED_DIRECTION = {
383
+ south: "south",
384
+ "south-east": "south-west",
385
+ east: "west",
386
+ "north-east": "north-west",
387
+ north: "north",
388
+ "north-west": "north-east",
389
+ west: "east",
390
+ "south-west": "south-east"
391
+ };
378
392
  var CharacterModeSchema = import_zod.z.enum(["standard", "v3", "pro"]);
393
+ var CHARACTER_PROPORTION_PRESETS = ["default", "chibi", "cartoon", "stylized", "realistic_male", "realistic_female", "heroic"];
394
+ var CharacterProportionsSchema = import_zod.z.union([
395
+ import_zod.z.enum(CHARACTER_PROPORTION_PRESETS),
396
+ import_zod.z.object({
397
+ headSize: import_zod.z.number().min(0.5).max(2).optional(),
398
+ armsLength: import_zod.z.number().min(0.5).max(2).optional(),
399
+ legsLength: import_zod.z.number().min(0.5).max(2).optional(),
400
+ shoulderWidth: import_zod.z.number().min(0.5).max(2).optional(),
401
+ hipWidth: import_zod.z.number().min(0.5).max(2).optional()
402
+ }).strict()
403
+ ]);
379
404
  var CharacterAnimationModeSchema = import_zod.z.enum(["template", "v3", "pro"]);
380
405
  var CharacterStateSchema = import_zod.z.object({
381
406
  /** The base character, or another state, in the same style. */
@@ -534,6 +559,16 @@ var StyleObjectSchema = import_zod.z.object({
534
559
  directions: import_zod.z.union([import_zod.z.literal(4), import_zod.z.literal(8)]).optional(),
535
560
  /** `character` only. Body template: `mannequin` (default) or a quadruped (`bear`, `cat`, `dog`, `horse`, `lion`). */
536
561
  template: import_zod.z.string().min(1).optional(),
562
+ /** `character` only, `standard` humanoid bases. A preset or multipliers; an asset may override it. */
563
+ proportions: CharacterProportionsSchema.optional(),
564
+ /**
565
+ * `character` only. How closely a `standard` base and a template loop
566
+ * follow their text, 1 to 20; PixelLab's default is 8. The v3 and pro
567
+ * engines do not take it.
568
+ */
569
+ textGuidanceScale: import_zod.z.number().min(1).max(20).optional(),
570
+ /** `character` only. Draw `standard` bases and every loop in isometric view. */
571
+ isometric: import_zod.z.boolean().optional(),
537
572
  /** Fixed seed for reproducibility where the endpoint supports it. */
538
573
  seed: import_zod.z.number().int().optional(),
539
574
  /**
@@ -634,8 +669,8 @@ var InheritedStyleSchema = StyleObjectSchema.partial().extend({
634
669
  }).strict();
635
670
  var StyleInputSchema = import_zod.z.union([StyleSchema, InheritedStyleSchema]);
636
671
  var AssetSchema = import_zod.z.object({
637
- /** The subject. Style wrapping comes from the style's prefix/suffix. */
638
- prompt: import_zod.z.string(),
672
+ /** The subject. Style wrapping comes from the style's prefix/suffix. Only a `mirror` may leave it out. */
673
+ prompt: import_zod.z.string().optional(),
639
674
  /** Subdirectory under the style's outDir. Optional. */
640
675
  category: import_zod.z.string().optional(),
641
676
  /** Overrides the style default. `map` generator only. */
@@ -693,6 +728,32 @@ var AssetSchema = import_zod.z.object({
693
728
  state: CharacterStateSchema.optional(),
694
729
  /** `character` styles: this asset is a loop of another character asset in one direction. */
695
730
  animation: CharacterAnimationSchema.optional(),
731
+ /** `character` styles, `standard` humanoid bases: this character's proportions, over the style's. */
732
+ proportions: CharacterProportionsSchema.optional(),
733
+ /**
734
+ * `character` styles, bases only: the character's own sprite, which
735
+ * PixelLab rotates into the other directions instead of drawing from
736
+ * the prompt. A manifest-relative PNG or JPEG of the south-facing
737
+ * sprite, or an object keyed by direction (`{ "south": ..., "east":
738
+ * ... }`); quadrupeds in `standard` mode need south and east, and only
739
+ * `standard` takes more than south. `standard` wants each image at the
740
+ * style's size; v3 accepts up to 256px, pro up to 168px. The prompt
741
+ * still guides the result.
742
+ */
743
+ reference: import_zod.z.union([import_zod.z.string().min(1), import_zod.z.record(CharacterDirectionSchema, import_zod.z.string().min(1))]).optional(),
744
+ /**
745
+ * Another asset of the same style flipped left to right, made locally
746
+ * from that asset's downloaded files at no generation cost.
747
+ *
748
+ * The saving is in character loops, where each direction is its own
749
+ * generation: a walk facing west mirrored is the walk facing east, and a
750
+ * diagonal mirrors to the other diagonal. South and north mirror onto
751
+ * themselves and are refused. A single image or a frame set from any
752
+ * generator can be mirrored too; a tile set cannot, since its edges
753
+ * carry meaning. Mirroring swaps handedness, so an asymmetric character
754
+ * (a sword hand, a parted fringe) comes out the other way round.
755
+ */
756
+ mirror: import_zod.z.string().min(1).optional(),
696
757
  /**
697
758
  * Generated output role to place in `cell` when this asset expands to
698
759
  * several files. Omit for ordinary single-output assets. A structural set
@@ -732,7 +793,7 @@ var AssetSchema = import_zod.z.object({
732
793
  path: ["revision"]
733
794
  });
734
795
  }
735
- const shapes = [asset.revision && "revision", asset.state && "state", asset.animation && "animation"].filter(Boolean);
796
+ const shapes = [asset.revision && "revision", asset.state && "state", asset.animation && "animation", asset.mirror && "mirror"].filter(Boolean);
736
797
  if (shapes.length > 1) {
737
798
  context.addIssue({
738
799
  code: import_zod.z.ZodIssueCode.custom,
@@ -740,6 +801,30 @@ var AssetSchema = import_zod.z.object({
740
801
  path: [shapes[1]]
741
802
  });
742
803
  }
804
+ if (asset.mirror && (asset.source || Object.keys(asset.sourceByStyle).length)) {
805
+ context.addIssue({
806
+ code: import_zod.z.ZodIssueCode.custom,
807
+ message: "a mirror is made from another asset's files; it cannot also declare a source",
808
+ path: ["mirror"]
809
+ });
810
+ }
811
+ if (asset.prompt === void 0 && !asset.mirror) {
812
+ context.addIssue({
813
+ code: import_zod.z.ZodIssueCode.custom,
814
+ message: "Required",
815
+ path: ["prompt"]
816
+ });
817
+ }
818
+ if (asset.reference && (asset.state || asset.animation || asset.mirror)) {
819
+ context.addIssue({
820
+ code: import_zod.z.ZodIssueCode.custom,
821
+ message: "a reference sprite belongs on a base; a state, animation, or mirror takes its look from its parent",
822
+ path: ["reference"]
823
+ });
824
+ }
825
+ if (typeof asset.reference === "object" && !Object.keys(asset.reference).length) {
826
+ context.addIssue({ code: import_zod.z.ZodIssueCode.custom, message: "a reference needs at least a south image", path: ["reference"] });
827
+ }
743
828
  });
744
829
  var DEFAULT_HISTORY_LIMIT = 5;
745
830
  var HistoryLimitSchema = import_zod.z.number().int().min(0).max(100);
@@ -829,6 +914,11 @@ var LockEntrySchema = import_zod.z.object({
829
914
  maskSha256: import_zod.z.string().regex(/^[0-9a-f]{64}$/).optional(),
830
915
  strength: import_zod.z.number().min(0).max(1).optional()
831
916
  }).strict().nullable().default(null),
917
+ /** For a mirror: the asset it flips and a hash over that asset's output hashes when this was made. */
918
+ mirror: import_zod.z.object({
919
+ sourceAssetId: import_zod.z.string().min(1),
920
+ sourceSha256: import_zod.z.string().regex(/^[0-9a-f]{64}$/)
921
+ }).strict().nullable().default(null),
832
922
  /**
833
923
  * Output hashes owned by the previous generation while its replacement is
834
924
  * pending. They authorize replacing only unchanged PixelKiln-owned files.
@@ -1160,6 +1250,17 @@ var PixelLabError = class extends ProviderError {
1160
1250
  }
1161
1251
  body;
1162
1252
  };
1253
+ function proportionsPayload(proportions) {
1254
+ if (typeof proportions === "string") return { type: "preset", name: proportions };
1255
+ return {
1256
+ type: "custom",
1257
+ ...proportions.headSize !== void 0 ? { head_size: proportions.headSize } : {},
1258
+ ...proportions.armsLength !== void 0 ? { arms_length: proportions.armsLength } : {},
1259
+ ...proportions.legsLength !== void 0 ? { legs_length: proportions.legsLength } : {},
1260
+ ...proportions.shoulderWidth !== void 0 ? { shoulder_width: proportions.shoulderWidth } : {},
1261
+ ...proportions.hipWidth !== void 0 ? { hip_width: proportions.hipWidth } : {}
1262
+ };
1263
+ }
1163
1264
  var PixelLabClient = class {
1164
1265
  constructor(apiKey, timeoutMs = 12e4) {
1165
1266
  this.apiKey = apiKey;
@@ -1169,9 +1270,9 @@ var PixelLabClient = class {
1169
1270
  apiKey;
1170
1271
  http;
1171
1272
  /** Retries transport failures, 429, and 5xx; see http.ts for the policy. */
1172
- async request(path32, init) {
1273
+ async request(path33, init) {
1173
1274
  const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
1174
- const res = await this.http(`${BASE}${path32}`, {
1275
+ const res = await this.http(`${BASE}${path33}`, {
1175
1276
  ...init,
1176
1277
  headers: {
1177
1278
  Authorization: auth,
@@ -1181,13 +1282,13 @@ var PixelLabClient = class {
1181
1282
  });
1182
1283
  const text = await res.text();
1183
1284
  if (!res.ok) {
1184
- throw new PixelLabError(`${init?.method ?? "GET"} ${path32} \u2192 ${res.status}`, res.status, text);
1285
+ throw new PixelLabError(`${init?.method ?? "GET"} ${path33} \u2192 ${res.status}`, res.status, text);
1185
1286
  }
1186
1287
  if (!text) return {};
1187
1288
  try {
1188
1289
  return JSON.parse(text);
1189
1290
  } catch {
1190
- throw new Error(`${init?.method ?? "GET"} ${path32} returned invalid JSON`);
1291
+ throw new Error(`${init?.method ?? "GET"} ${path33} returned invalid JSON`);
1191
1292
  }
1192
1293
  }
1193
1294
  async balance() {
@@ -1368,14 +1469,21 @@ var PixelLabClient = class {
1368
1469
  * A base character. Standard mode picks the 4- or 8-direction template
1369
1470
  * endpoint; v3 and pro have their own and always draw 8. Every one answers
1370
1471
  * at once with the character id and a background job.
1472
+ *
1473
+ * A `reference` is the author's own sprite: standard takes one per
1474
+ * direction and draws the rest, v3 rotates the south one, and pro
1475
+ * switches to its rotate method for it. A `styleReference` is pro's style
1476
+ * anchor; the other engines have no such input.
1371
1477
  */
1372
1478
  async createCharacter(args) {
1373
1479
  const imageSize = { width: args.size, height: args.size };
1374
1480
  const palette = args.paletteSwatchBase64 ? { color_image: { type: "base64", base64: args.paletteSwatchBase64, format: "png" }, force_colors: true } : {};
1375
- let path32;
1481
+ const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
1482
+ const south = args.reference?.south;
1483
+ let path33;
1376
1484
  let body;
1377
1485
  if (args.mode === "v3") {
1378
- path32 = "/create-character-v3";
1486
+ path33 = "/create-character-v3";
1379
1487
  body = {
1380
1488
  description: args.description,
1381
1489
  image_size: imageSize,
@@ -1384,20 +1492,23 @@ var PixelLabClient = class {
1384
1492
  ...args.view ? { view: args.view } : {},
1385
1493
  ...args.outline ? { outline: args.outline } : {},
1386
1494
  ...args.detail ? { detail: args.detail } : {},
1387
- ...args.seed != null ? { seed: args.seed } : {}
1495
+ ...args.seed != null ? { seed: args.seed } : {},
1496
+ ...south ? { reference_image: encode(south) } : {}
1388
1497
  };
1389
1498
  } else if (args.mode === "pro") {
1390
- path32 = "/create-character-pro";
1499
+ path33 = "/create-character-pro";
1391
1500
  body = {
1392
1501
  description: args.description,
1393
1502
  image_size: imageSize,
1394
1503
  template_id: args.template,
1395
1504
  no_background: args.noBackground ?? true,
1505
+ method: south ? "rotate_character" : "create_with_style",
1396
1506
  ...args.view ? { view: args.view } : {},
1397
- ...args.seed != null ? { seed: args.seed } : {}
1507
+ ...args.seed != null ? { seed: args.seed } : {},
1508
+ ...south ? { reference_image: encode(south) } : args.styleReference ? { reference_image: encode(args.styleReference) } : {}
1398
1509
  };
1399
1510
  } else {
1400
- path32 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
1511
+ path33 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
1401
1512
  body = {
1402
1513
  description: args.description,
1403
1514
  image_size: imageSize,
@@ -1407,11 +1518,15 @@ var PixelLabClient = class {
1407
1518
  ...args.shading ? { shading: args.shading } : {},
1408
1519
  ...args.detail ? { detail: args.detail } : {},
1409
1520
  ...args.seed != null ? { seed: args.seed } : {},
1521
+ ...args.proportions !== void 0 ? { proportions: proportionsPayload(args.proportions) } : {},
1522
+ ...args.textGuidanceScale !== void 0 ? { text_guidance_scale: args.textGuidanceScale } : {},
1523
+ ...args.isometric !== void 0 ? { isometric: args.isometric } : {},
1524
+ ...args.reference && Object.keys(args.reference).length ? { directions: Object.fromEntries(Object.entries(args.reference).flatMap(([direction, image]) => image ? [[direction, encode(image)]] : [])) } : {},
1410
1525
  ...palette
1411
1526
  };
1412
1527
  }
1413
- const raw = await this.request(path32, { method: "POST", body: JSON.stringify(body) });
1414
- return validateResponse(CharacterSubmitSchema, raw, path32.slice(1));
1528
+ const raw = await this.request(path33, { method: "POST", body: JSON.stringify(body) });
1529
+ return validateResponse(CharacterSubmitSchema, raw, path33.slice(1));
1415
1530
  }
1416
1531
  /** A text edit of an existing character, applied to every direction and stored as a sibling. */
1417
1532
  async createCharacterState(args) {
@@ -1446,6 +1561,8 @@ var PixelLabClient = class {
1446
1561
  ...args.actionDescription ? { action_description: args.actionDescription } : {},
1447
1562
  ...args.mode === "v3" && args.frameCount ? { frame_count: args.frameCount } : {},
1448
1563
  ...args.mode === "v3" && args.keepFirstFrame === false ? { keep_first_frame: false } : {},
1564
+ ...args.mode === "template" && args.textGuidanceScale !== void 0 ? { text_guidance_scale: args.textGuidanceScale } : {},
1565
+ ...args.isometric !== void 0 ? { isometric: args.isometric } : {},
1449
1566
  ...args.seed != null ? { seed: args.seed } : {}
1450
1567
  })
1451
1568
  });
@@ -2044,10 +2161,86 @@ function cacheFileName(hash, mediaType = MediaType.PNG) {
2044
2161
  }
2045
2162
 
2046
2163
  // src/providers/pixellab.ts
2047
- var import_node_fs = require("fs");
2048
- var import_node_crypto = require("crypto");
2164
+ var import_node_fs2 = require("fs");
2165
+ var import_node_crypto2 = require("crypto");
2049
2166
  var import_node_os = __toESM(require("os"), 1);
2050
2167
  var import_node_path = __toESM(require("path"), 1);
2168
+
2169
+ // src/hash.ts
2170
+ var import_node_crypto = require("crypto");
2171
+ var import_node_fs = require("fs");
2172
+ function sha256(data) {
2173
+ return (0, import_node_crypto.createHash)("sha256").update(data).digest("hex");
2174
+ }
2175
+ async function sha256File(path33) {
2176
+ return new Promise((resolve, reject) => {
2177
+ const hash = (0, import_node_crypto.createHash)("sha256");
2178
+ const stream = (0, import_node_fs.createReadStream)(path33);
2179
+ stream.on("data", (chunk2) => hash.update(chunk2));
2180
+ stream.on("error", reject);
2181
+ stream.on("end", () => resolve(hash.digest("hex")));
2182
+ });
2183
+ }
2184
+ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.providerOptions, providerInputIdentity = spec.providerInputs ?? {}) {
2185
+ return sha256(
2186
+ JSON.stringify({
2187
+ // Preserve every existing PixelLab hash while making a provider switch
2188
+ // invalidate the spec. Older manifests implicitly mean pixellab.
2189
+ provider: spec.provider === "pixellab" ? void 0 : spec.provider,
2190
+ providerOptions: providerOptionIdentity && (typeof providerOptionIdentity !== "object" || Object.keys(providerOptionIdentity).length > 0) ? providerOptionIdentity : void 0,
2191
+ providerInputs: providerInputIdentity && (typeof providerInputIdentity !== "object" || Object.keys(providerInputIdentity).length > 0) ? providerInputIdentity : void 0,
2192
+ generator: spec.generator,
2193
+ prompt: spec.prompt,
2194
+ width: spec.width,
2195
+ height: spec.height,
2196
+ view: spec.view,
2197
+ outline: spec.outline ?? null,
2198
+ shading: spec.shading ?? null,
2199
+ detail: spec.detail ?? null,
2200
+ seed: spec.seed ?? null,
2201
+ palette: spec.palette,
2202
+ // `noBackground` only reaches the wire for pixflux; the tile fields are
2203
+ // undefined for every other generator. `tileSize` is intentionally
2204
+ // absent; width/height are derived from it, so it is already covered.
2205
+ noBackground: spec.generator === "pixflux" || spec.provider !== "pixellab" ? spec.noBackground : void 0,
2206
+ tileType: spec.tileType,
2207
+ tileView: spec.tileView,
2208
+ tileFeature: spec.tileFeature,
2209
+ outlineMode: spec.outlineMode,
2210
+ styleImages: styleImageHashes,
2211
+ // A character family: the engine and rotations of a base, the parent's
2212
+ // generated bytes for a state or animation (so a regenerated parent
2213
+ // makes its children stale), and each shape's own settings.
2214
+ character: spec.character ? {
2215
+ kind: spec.character.kind,
2216
+ mode: spec.character.mode,
2217
+ directions: spec.character.directions,
2218
+ template: spec.character.template,
2219
+ parent: spec.character.parentAssetId,
2220
+ parentSha256: spec.character.parentSha256 ?? null,
2221
+ state: spec.character.state,
2222
+ animation: spec.character.animation,
2223
+ // Absent keys keep the hashes of manifests that never set these.
2224
+ proportions: spec.character.proportions,
2225
+ textGuidanceScale: spec.character.textGuidanceScale,
2226
+ isometric: spec.character.isometric,
2227
+ reference: spec.character.reference ? Object.fromEntries(Object.entries(spec.character.reference).map(([direction, image]) => [direction, image.sha256])) : void 0
2228
+ } : void 0,
2229
+ // A mirror's bytes come from its source's recorded outputs; the plan
2230
+ // compares those directly, so only the choice of source is identity.
2231
+ mirror: spec.mirror ? { of: spec.mirror.sourceAssetId } : void 0,
2232
+ revision: spec.revision ? {
2233
+ mode: spec.revision.mode,
2234
+ from: spec.revision.sourceAssetId,
2235
+ sourceSha256: spec.revision.sourceSha256,
2236
+ maskSha256: spec.revision.maskSha256,
2237
+ strength: spec.revision.strength
2238
+ } : void 0
2239
+ })
2240
+ );
2241
+ }
2242
+
2243
+ // src/providers/pixellab.ts
2051
2244
  function pixelLabObjectUrl(generator, objectId) {
2052
2245
  if (!objectId) return null;
2053
2246
  if (generator === "map" || generator === "1dir") {
@@ -2128,7 +2321,7 @@ var PixelLabProvider = class _PixelLabProvider {
2128
2321
  */
2129
2322
  static cacheDir() {
2130
2323
  const dir = import_node_path.default.join(import_node_os.default.tmpdir(), "pixelkiln-pixflux");
2131
- (0, import_node_fs.mkdirSync)(dir, { recursive: true });
2324
+ (0, import_node_fs2.mkdirSync)(dir, { recursive: true });
2132
2325
  return dir;
2133
2326
  }
2134
2327
  estimate(spec) {
@@ -2170,8 +2363,8 @@ var PixelLabProvider = class _PixelLabProvider {
2170
2363
  "high detail"
2171
2364
  ]);
2172
2365
  }
2173
- if (spec.generator === "character") this.validateCharacter(spec);
2174
- if ((spec.generator === "map" || spec.generator === "pixflux" || spec.generator === "character") && styleImages.length) {
2366
+ if (spec.generator === "character") this.validateCharacter(spec, styleImages);
2367
+ if ((spec.generator === "map" || spec.generator === "pixflux") && styleImages.length) {
2175
2368
  throw new Error(`PixelLab ${spec.generator} does not support style images`);
2176
2369
  }
2177
2370
  for (const image of styleImages) {
@@ -2187,9 +2380,61 @@ var PixelLabProvider = class _PixelLabProvider {
2187
2380
  );
2188
2381
  }
2189
2382
  }
2190
- validateCharacter(spec) {
2383
+ validateCharacter(spec, styleImages = []) {
2191
2384
  const character = spec.character;
2192
2385
  if (!character) throw new Error(`${spec.styleId}/${spec.assetId} has no character shape`);
2386
+ const label = `${spec.styleId}/${spec.assetId}`;
2387
+ if (character.kind === "base" && character.proportions !== void 0) {
2388
+ if (character.mode !== "standard") {
2389
+ throw new Error(`${label}: proportions apply to standard bases; the ${character.mode} engine has none to set`);
2390
+ }
2391
+ if (character.template !== "mannequin") {
2392
+ throw new Error(`${label}: proportions apply to the mannequin template; ${character.template} is a quadruped`);
2393
+ }
2394
+ }
2395
+ if (character.reference) {
2396
+ const given = Object.keys(character.reference);
2397
+ if (!character.reference.south) throw new Error(`${label}: a reference needs a south-facing sprite`);
2398
+ if (character.mode !== "standard") {
2399
+ if (given.length > 1) throw new Error(`${label}: PixelLab ${character.mode} rotates one south-facing reference; drop the other directions`);
2400
+ const limit = character.mode === "v3" ? 256 : 168;
2401
+ const south = character.reference.south;
2402
+ if (south.width > limit || south.height > limit) {
2403
+ throw new Error(`${label}: reference is ${south.width}x${south.height}; PixelLab ${character.mode} takes up to ${limit}px`);
2404
+ }
2405
+ } else {
2406
+ const allowed = character.directions === 4 ? CHARACTER_DIRECTIONS_4 : CHARACTER_DIRECTIONS_8;
2407
+ for (const direction of given) {
2408
+ if (!allowed.includes(direction)) {
2409
+ throw new Error(`${label}: reference "${direction}" is not one of this character's ${character.directions} directions`);
2410
+ }
2411
+ const image = character.reference[direction];
2412
+ if (image.width !== spec.width || image.height !== spec.height) {
2413
+ throw new Error(
2414
+ `${label}: reference (${direction}) is ${image.width}x${image.height}; standard mode wants each image at the style's size, ${spec.width}x${spec.height}`
2415
+ );
2416
+ }
2417
+ }
2418
+ if (character.template !== "mannequin" && !character.reference.east) {
2419
+ throw new Error(`${label}: a ${character.template} reference needs south and east images`);
2420
+ }
2421
+ }
2422
+ }
2423
+ if (styleImages.length) {
2424
+ if (character.mode !== "pro") {
2425
+ throw new Error(
2426
+ `PixelLab ${character.mode} characters take no style images; give a base its own sprite with \`reference\`, or use mode pro for a style anchor`
2427
+ );
2428
+ }
2429
+ if (styleImages.length > 1) throw new Error("PixelLab pro characters take one style image");
2430
+ if (character.kind === "base" && character.reference) {
2431
+ throw new Error(`${label}: a pro base rotating its own reference takes no style image`);
2432
+ }
2433
+ const image = styleImages[0];
2434
+ if (image.width > 168 || image.height > 168) {
2435
+ throw new Error(`PixelLab pro character style image is ${image.width}x${image.height}; the limit is 168px`);
2436
+ }
2437
+ }
2193
2438
  const maxSize = character.mode === "v3" ? 256 : 128;
2194
2439
  if (spec.width < 16 || spec.height < 16 || spec.width > maxSize || spec.height > maxSize) {
2195
2440
  throw new Error(`PixelLab ${character.mode} characters must be between 16 and ${maxSize} pixels`);
@@ -2225,10 +2470,11 @@ var PixelLabProvider = class _PixelLabProvider {
2225
2470
  * asset so a later run can find it, and clears its own earlier take for
2226
2471
  * that direction first, since PixelLab skips a direction that exists.
2227
2472
  */
2228
- async submitCharacter(spec, context) {
2473
+ async submitCharacter(spec, styleImages, context) {
2229
2474
  const character = spec.character;
2230
2475
  if (character.kind === "base") {
2231
2476
  const swatch = spec.palette.length && character.mode === "standard" ? paletteSwatch(spec.palette).toString("base64") : void 0;
2477
+ const styleImage = styleImages[0];
2232
2478
  const res2 = await this.client.createCharacter({
2233
2479
  mode: character.mode,
2234
2480
  description: spec.prompt,
@@ -2241,7 +2487,12 @@ var PixelLabProvider = class _PixelLabProvider {
2241
2487
  detail: spec.detail,
2242
2488
  seed: spec.seed,
2243
2489
  noBackground: spec.noBackground,
2244
- paletteSwatchBase64: swatch
2490
+ paletteSwatchBase64: swatch,
2491
+ proportions: character.proportions,
2492
+ textGuidanceScale: character.textGuidanceScale,
2493
+ isometric: character.isometric,
2494
+ reference: character.reference ? readReference(spec, character.reference) : void 0,
2495
+ styleReference: styleImage ? { base64: styleImage.base64, format: styleImage.format } : void 0
2245
2496
  });
2246
2497
  return { jobId: res2.character_id, metadata: { character: { kind: "base", characterId: res2.character_id, mode: character.mode, directions: character.directions, backgroundJobId: res2.background_job_id } } };
2247
2498
  }
@@ -2283,7 +2534,9 @@ var PixelLabProvider = class _PixelLabProvider {
2283
2534
  frameCount: animation.frames,
2284
2535
  keepFirstFrame: animation.keepFirstFrame,
2285
2536
  directions: [animation.direction],
2286
- seed: spec.seed
2537
+ seed: spec.seed,
2538
+ textGuidanceScale: character.textGuidanceScale,
2539
+ isometric: character.isometric
2287
2540
  });
2288
2541
  const job = { characterId: parentId, name, direction: animation.direction, jobIds: res.background_job_ids };
2289
2542
  return {
@@ -2373,7 +2626,7 @@ var PixelLabProvider = class _PixelLabProvider {
2373
2626
  }
2374
2627
  async submit(spec, styleImages, context) {
2375
2628
  this.validate(spec, styleImages);
2376
- if (spec.generator === "character") return this.submitCharacter(spec, context);
2629
+ if (spec.generator === "character") return this.submitCharacter(spec, styleImages, context);
2377
2630
  if (spec.generator === "pixflux") {
2378
2631
  const swatch = spec.palette.length ? paletteSwatch(spec.palette).toString("base64") : void 0;
2379
2632
  const { png } = await this.client.createImagePixflux({
@@ -2384,8 +2637,8 @@ var PixelLabProvider = class _PixelLabProvider {
2384
2637
  paletteSwatchBase64: swatch,
2385
2638
  seed: spec.seed
2386
2639
  });
2387
- const jobId = (0, import_node_crypto.randomUUID)();
2388
- (0, import_node_fs.writeFileSync)(import_node_path.default.join(_PixelLabProvider.cacheDir(), `${jobId}.png`), png);
2640
+ const jobId = (0, import_node_crypto2.randomUUID)();
2641
+ (0, import_node_fs2.writeFileSync)(import_node_path.default.join(_PixelLabProvider.cacheDir(), `${jobId}.png`), png);
2389
2642
  return { jobId };
2390
2643
  }
2391
2644
  if (spec.generator === "tiles") {
@@ -2427,7 +2680,7 @@ var PixelLabProvider = class _PixelLabProvider {
2427
2680
  async poll(jobId, generator, context) {
2428
2681
  if (generator === "pixflux") {
2429
2682
  const file = import_node_path.default.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
2430
- if ((0, import_node_fs.existsSync)(file)) {
2683
+ if ((0, import_node_fs2.existsSync)(file)) {
2431
2684
  const sourceUrl = `file://${file}`;
2432
2685
  return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }] };
2433
2686
  }
@@ -2533,7 +2786,7 @@ var PixelLabProvider = class _PixelLabProvider {
2533
2786
  return { objectId, sourceUrl: firstUrl(obj?.rotation_urls) ?? obj?.preview_url ?? null };
2534
2787
  }
2535
2788
  async download(url) {
2536
- if (url.startsWith("file://")) return (0, import_node_fs.readFileSync)(url.slice("file://".length));
2789
+ if (url.startsWith("file://")) return (0, import_node_fs2.readFileSync)(url.slice("file://".length));
2537
2790
  return this.client.download(url);
2538
2791
  }
2539
2792
  async balance() {
@@ -2640,6 +2893,17 @@ function firstUrl(urls) {
2640
2893
  if (!urls) return null;
2641
2894
  return Object.values(urls).find((u) => typeof u === "string") ?? null;
2642
2895
  }
2896
+ function readReference(spec, reference) {
2897
+ const images = {};
2898
+ for (const [direction, image] of Object.entries(reference)) {
2899
+ const bytes = (0, import_node_fs2.readFileSync)(image.path);
2900
+ if (sha256(bytes) !== image.sha256) {
2901
+ throw new Error(`${spec.styleId}/${spec.assetId}: reference (${direction}) changed after the manifest was resolved: ${image.path}`);
2902
+ }
2903
+ images[direction] = { base64: bytes.toString("base64"), format: image.format };
2904
+ }
2905
+ return images;
2906
+ }
2643
2907
  function rotationSources(character) {
2644
2908
  const order = character.directions === 4 ? CHARACTER_DIRECTIONS_4 : CHARACTER_DIRECTIONS_8;
2645
2909
  const sources = [];
@@ -2710,9 +2974,9 @@ var RetroDiffusionClient = class {
2710
2974
  if (!response.ok) throw new ProviderError("retrodiffusion", `Retro Diffusion download failed (${response.status})`, { status: response.status });
2711
2975
  return Buffer.from(await response.arrayBuffer());
2712
2976
  }
2713
- async call(path32, init = {}) {
2977
+ async call(path33, init = {}) {
2714
2978
  if (!this.token) throw new Error("RD_API_KEY is not set");
2715
- const response = await this.request(`${this.baseUrl}${path32}`, {
2979
+ const response = await this.request(`${this.baseUrl}${path33}`, {
2716
2980
  ...init,
2717
2981
  headers: {
2718
2982
  "Content-Type": "application/json",
@@ -3511,74 +3775,6 @@ function redactScenarioError(value, secrets) {
3511
3775
  var import_node_crypto3 = require("crypto");
3512
3776
  var import_promises = require("fs/promises");
3513
3777
  var import_node_path2 = __toESM(require("path"), 1);
3514
-
3515
- // src/hash.ts
3516
- var import_node_crypto2 = require("crypto");
3517
- var import_node_fs2 = require("fs");
3518
- function sha256(data) {
3519
- return (0, import_node_crypto2.createHash)("sha256").update(data).digest("hex");
3520
- }
3521
- async function sha256File(path32) {
3522
- return new Promise((resolve, reject) => {
3523
- const hash = (0, import_node_crypto2.createHash)("sha256");
3524
- const stream = (0, import_node_fs2.createReadStream)(path32);
3525
- stream.on("data", (chunk2) => hash.update(chunk2));
3526
- stream.on("error", reject);
3527
- stream.on("end", () => resolve(hash.digest("hex")));
3528
- });
3529
- }
3530
- function specHash(spec, styleImageHashes, providerOptionIdentity = spec.providerOptions, providerInputIdentity = spec.providerInputs ?? {}) {
3531
- return sha256(
3532
- JSON.stringify({
3533
- // Preserve every existing PixelLab hash while making a provider switch
3534
- // invalidate the spec. Older manifests implicitly mean pixellab.
3535
- provider: spec.provider === "pixellab" ? void 0 : spec.provider,
3536
- providerOptions: providerOptionIdentity && (typeof providerOptionIdentity !== "object" || Object.keys(providerOptionIdentity).length > 0) ? providerOptionIdentity : void 0,
3537
- providerInputs: providerInputIdentity && (typeof providerInputIdentity !== "object" || Object.keys(providerInputIdentity).length > 0) ? providerInputIdentity : void 0,
3538
- generator: spec.generator,
3539
- prompt: spec.prompt,
3540
- width: spec.width,
3541
- height: spec.height,
3542
- view: spec.view,
3543
- outline: spec.outline ?? null,
3544
- shading: spec.shading ?? null,
3545
- detail: spec.detail ?? null,
3546
- seed: spec.seed ?? null,
3547
- palette: spec.palette,
3548
- // `noBackground` only reaches the wire for pixflux; the tile fields are
3549
- // undefined for every other generator. `tileSize` is intentionally
3550
- // absent; width/height are derived from it, so it is already covered.
3551
- noBackground: spec.generator === "pixflux" || spec.provider !== "pixellab" ? spec.noBackground : void 0,
3552
- tileType: spec.tileType,
3553
- tileView: spec.tileView,
3554
- tileFeature: spec.tileFeature,
3555
- outlineMode: spec.outlineMode,
3556
- styleImages: styleImageHashes,
3557
- // A character family: the engine and rotations of a base, the parent's
3558
- // generated bytes for a state or animation (so a regenerated parent
3559
- // makes its children stale), and each shape's own settings.
3560
- character: spec.character ? {
3561
- kind: spec.character.kind,
3562
- mode: spec.character.mode,
3563
- directions: spec.character.directions,
3564
- template: spec.character.template,
3565
- parent: spec.character.parentAssetId,
3566
- parentSha256: spec.character.parentSha256 ?? null,
3567
- state: spec.character.state,
3568
- animation: spec.character.animation
3569
- } : void 0,
3570
- revision: spec.revision ? {
3571
- mode: spec.revision.mode,
3572
- from: spec.revision.sourceAssetId,
3573
- sourceSha256: spec.revision.sourceSha256,
3574
- maskSha256: spec.revision.maskSha256,
3575
- strength: spec.revision.strength
3576
- } : void 0
3577
- })
3578
- );
3579
- }
3580
-
3581
- // src/providers/comfyui.ts
3582
3778
  var DEFAULT_BASE_URL3 = "http://127.0.0.1:8188";
3583
3779
  var BUILTIN_BINDINGS = /* @__PURE__ */ new Set([
3584
3780
  "prompt",
@@ -4529,6 +4725,18 @@ var FakeProvider = class {
4529
4725
  const objectId = job.objectId ?? `obj-${jobId}`;
4530
4726
  job.objectId = objectId;
4531
4727
  this.register(objectId, job.spec);
4728
+ const character = job.spec.character;
4729
+ if (character) {
4730
+ 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;
4731
+ const sources = roles.map((role) => ({ url: `fake://${objectId}/${role}.png`, role }));
4732
+ return {
4733
+ status: "ready",
4734
+ objectId,
4735
+ sourceUrl: sources[0].url,
4736
+ sources,
4737
+ ...character.kind === "animation" ? { metadata: { frameSet: { fps: character.animation.fps, count: roles.length } } } : {}
4738
+ };
4739
+ }
4532
4740
  const sourceUrl = `fake://${objectId}.png`;
4533
4741
  return { status: "ready", objectId, sourceUrl, sources: [{ url: sourceUrl }] };
4534
4742
  }
@@ -5193,16 +5401,16 @@ async function resolveSpecs(loaded, filter) {
5193
5401
  }
5194
5402
  }
5195
5403
  const styleImageCache = /* @__PURE__ */ new Map();
5196
- async function loadStyleImage(rel) {
5404
+ async function loadStyleImage(rel, what = "Style image") {
5197
5405
  const abs = import_node_path5.default.resolve(root, rel);
5198
5406
  let hit = styleImageCache.get(abs);
5199
5407
  if (!hit) {
5200
- if (!(0, import_node_fs5.existsSync)(abs)) throw new Error(`Style image not found: ${abs}`);
5408
+ if (!(0, import_node_fs5.existsSync)(abs)) throw new Error(`${what} not found: ${abs}`);
5201
5409
  const buf = await (0, import_promises3.readFile)(abs);
5202
5410
  const metadata = imageMetadata(buf);
5203
- if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${abs}`);
5411
+ if (!metadata) throw new Error(`${what} is not a readable PNG or JPEG: ${abs}`);
5204
5412
  if (metadata.width < 1 || metadata.height < 1) {
5205
- throw new Error(`Style image has invalid dimensions: ${abs}`);
5413
+ throw new Error(`${what} has invalid dimensions: ${abs}`);
5206
5414
  }
5207
5415
  hit = { base64: buf.toString("base64"), hash: sha256(buf), ...metadata };
5208
5416
  styleImageCache.set(abs, hit);
@@ -5277,7 +5485,7 @@ async function resolveSpecs(loaded, filter) {
5277
5485
  );
5278
5486
  }
5279
5487
  const characterKind = asset.animation ? "animation" : asset.state ? "state" : "base";
5280
- const subject = asset.promptByStyle[styleId] ?? asset.prompt;
5488
+ const subject = asset.promptByStyle[styleId] ?? asset.prompt ?? "";
5281
5489
  const prompt = generator === "character" && characterKind !== "base" ? subject.trim() : [style.promptPrefix, subject, style.promptSuffix].map((p) => p.trim()).filter(Boolean).join(", ");
5282
5490
  const relFile = asset.file ?? import_node_path5.default.join(asset.category ?? "", `${assetId}.png`);
5283
5491
  const outFile = import_node_path5.default.resolve(root, style.outDir, relFile);
@@ -5309,7 +5517,7 @@ async function resolveSpecs(loaded, filter) {
5309
5517
  tileView: generator === "tiles" ? style.tileView : void 0,
5310
5518
  tileFeature: generator === "tiles" ? style.tileFeature : void 0,
5311
5519
  outlineMode: generator === "tiles" ? style.outlineMode : void 0,
5312
- ...generator === "character" ? { character: resolveCharacterShape(asset, style, characterKind) } : {},
5520
+ ...generator === "character" ? { character: await resolveCharacterShape(asset, style, characterKind, { root, load: loadStyleImage }) } : {},
5313
5521
  cost: generator === "tiles" ? tilesCost(tileSize, tileVariations) : generationCost(width, height, generator),
5314
5522
  costUnit: "generations",
5315
5523
  candidates: generator === "tiles" ? tileVariations : generator === "1dir" ? candidateCount(size) : 1
@@ -5352,11 +5560,42 @@ async function resolveSpecs(loaded, filter) {
5352
5560
  const resolved = styleSpecs.get(assetId);
5353
5561
  if (!resolved) {
5354
5562
  throw new Error(
5355
- `Asset "${assetId}" is not available in style "${styleId}"; revision parents must participate in the same style.`
5563
+ `Asset "${assetId}" is not available in style "${styleId}"; revision parents, character parents, and mirror sources must be in the same style.`
5356
5564
  );
5357
5565
  }
5358
5566
  if (finalized.has(assetId)) return resolved;
5359
5567
  const asset = manifest.assets[assetId];
5568
+ if (asset.mirror) {
5569
+ if (asset.mirror === assetId) throw new Error(`assets.${assetId}: an asset cannot mirror itself`);
5570
+ const sourceSpec = await finalize(asset.mirror);
5571
+ if (sourceSpec.generator === "tiles") {
5572
+ throw new Error(`assets.${assetId}: a tile set cannot be mirrored; its edges carry meaning`);
5573
+ }
5574
+ if (sourceSpec.character?.kind === "animation") {
5575
+ const facing = sourceSpec.character.animation.direction;
5576
+ const mirrored = MIRRORED_DIRECTION[facing];
5577
+ if (mirrored === facing) {
5578
+ throw new Error(
5579
+ `assets.${assetId}: mirroring ${asset.mirror} gives another ${facing}-facing loop; mirror a loop facing east, west, or a diagonal`
5580
+ );
5581
+ }
5582
+ resolved.character = { ...sourceSpec.character, animation: { ...sourceSpec.character.animation, direction: mirrored } };
5583
+ } else if (sourceSpec.character) {
5584
+ resolved.character = { ...sourceSpec.character };
5585
+ }
5586
+ resolved.mirror = { sourceAssetId: asset.mirror, sourceSpec };
5587
+ resolved.generator = sourceSpec.generator;
5588
+ resolved.width = sourceSpec.width;
5589
+ resolved.height = sourceSpec.height;
5590
+ resolved.size = sourceSpec.size;
5591
+ resolved.prompt = "";
5592
+ resolved.specHash = specHash(resolved, styleImageHashes, providerOptionIdentity, providerInputIdentities.get(assetId));
5593
+ resolved.cost = 0;
5594
+ resolved.costUnit = sourceSpec.costUnit;
5595
+ resolved.candidates = 1;
5596
+ finalized.add(assetId);
5597
+ return resolved;
5598
+ }
5360
5599
  const characterParent = asset.state?.of ?? asset.animation?.of;
5361
5600
  if (resolved.character && characterParent) {
5362
5601
  const parentSpec = await finalize(characterParent);
@@ -5523,15 +5762,33 @@ function imageMetadata(buf) {
5523
5762
  }
5524
5763
  return null;
5525
5764
  }
5526
- function resolveCharacterShape(asset, style, kind) {
5765
+ async function resolveCharacterShape(asset, style, kind, files) {
5527
5766
  const mode2 = style.mode ?? "standard";
5528
5767
  const directions = mode2 === "standard" ? style.directions ?? 8 : 8;
5768
+ const proportions = asset.proportions ?? style.proportions;
5529
5769
  const shape = {
5530
5770
  kind,
5531
5771
  mode: mode2,
5532
5772
  directions,
5533
- template: style.template ?? "mannequin"
5773
+ template: style.template ?? "mannequin",
5774
+ ...proportions !== void 0 ? { proportions } : {},
5775
+ ...style.textGuidanceScale !== void 0 ? { textGuidanceScale: style.textGuidanceScale } : {},
5776
+ ...style.isometric !== void 0 ? { isometric: style.isometric } : {}
5534
5777
  };
5778
+ if (asset.reference) {
5779
+ const byDirection = typeof asset.reference === "string" ? { south: asset.reference } : asset.reference;
5780
+ shape.reference = {};
5781
+ for (const [direction, rel] of Object.entries(byDirection)) {
5782
+ const image = await files.load(rel, `Reference sprite (${direction})`);
5783
+ shape.reference[direction] = {
5784
+ path: import_node_path5.default.resolve(files.root, rel),
5785
+ sha256: image.hash,
5786
+ width: image.width,
5787
+ height: image.height,
5788
+ format: image.format
5789
+ };
5790
+ }
5791
+ }
5535
5792
  if (asset.state) {
5536
5793
  shape.state = {
5537
5794
  paletteFromReference: asset.state.paletteFromReference,
@@ -5556,7 +5813,7 @@ function characterDirections(directions) {
5556
5813
  }
5557
5814
 
5558
5815
  // src/project.ts
5559
- var import_node_path12 = __toESM(require("path"), 1);
5816
+ var import_node_path13 = __toESM(require("path"), 1);
5560
5817
 
5561
5818
  // src/env.ts
5562
5819
  var import_node_fs6 = require("fs");
@@ -5597,30 +5854,232 @@ function parseEnv(contents) {
5597
5854
  if ((quote === '"' || quote === "'") && value.endsWith(quote) && value.length >= 2) {
5598
5855
  value = value.slice(1, -1);
5599
5856
  }
5600
- vars[key] = value;
5857
+ vars[key] = value;
5858
+ }
5859
+ return vars;
5860
+ }
5861
+
5862
+ // src/pipeline/plan.ts
5863
+ var import_node_fs9 = require("fs");
5864
+ var import_node_path12 = __toESM(require("path"), 1);
5865
+
5866
+ // src/pipeline/mirror.ts
5867
+ var import_promises4 = require("fs/promises");
5868
+ var import_node_path7 = __toESM(require("path"), 1);
5869
+
5870
+ // src/palette.ts
5871
+ function normalizePalette(values) {
5872
+ const colors = /* @__PURE__ */ new Map();
5873
+ for (const value of values) {
5874
+ const rgb = parseHex(value);
5875
+ const hex2 = `#${rgb.r.toString(16).padStart(2, "0")}${rgb.g.toString(16).padStart(2, "0")}${rgb.b.toString(16).padStart(2, "0")}`;
5876
+ if (!colors.has(hex2)) colors.set(hex2, { hex: hex2, ...rgb });
5877
+ }
5878
+ if (colors.size < 2 || colors.size > 256) {
5879
+ throw new Error(`A palette must contain 2\u2013256 unique colors; got ${colors.size}.`);
5880
+ }
5881
+ return [...colors.values()];
5882
+ }
5883
+ function colorDistance(a, b) {
5884
+ const rmean = (a.r + b.r) / 2;
5885
+ const dr = a.r - b.r;
5886
+ const dg = a.g - b.g;
5887
+ const db = a.b - b.b;
5888
+ return Math.sqrt(
5889
+ (512 + rmean) * dr * dr / 256 + 4 * dg * dg + (767 - rmean) * db * db / 256
5890
+ );
5891
+ }
5892
+ function quantize(png, palette) {
5893
+ const colors = normalizePalette(palette);
5894
+ const pixels = Buffer.from(png.pixels);
5895
+ const nearestOf = /* @__PURE__ */ new Map();
5896
+ let changed = 0;
5897
+ for (let i = 0; i < pixels.length; i += 4) {
5898
+ const alpha = pixels[i + 3];
5899
+ if (alpha === 0) {
5900
+ if (pixels[i] || pixels[i + 1] || pixels[i + 2]) changed++;
5901
+ pixels[i] = 0;
5902
+ pixels[i + 1] = 0;
5903
+ pixels[i + 2] = 0;
5904
+ continue;
5905
+ }
5906
+ const key = pixels[i] << 16 | pixels[i + 1] << 8 | pixels[i + 2];
5907
+ let nearest = nearestOf.get(key);
5908
+ if (!nearest) {
5909
+ const pixel = { r: pixels[i], g: pixels[i + 1], b: pixels[i + 2] };
5910
+ nearest = colors[0];
5911
+ let distance = colorDistance(pixel, nearest);
5912
+ for (let j = 1; j < colors.length; j++) {
5913
+ const candidate = colors[j];
5914
+ const candidateDistance = colorDistance(pixel, candidate);
5915
+ if (candidateDistance < distance) {
5916
+ nearest = candidate;
5917
+ distance = candidateDistance;
5918
+ }
5919
+ }
5920
+ nearestOf.set(key, nearest);
5921
+ }
5922
+ if (pixels[i] !== nearest.r || pixels[i + 1] !== nearest.g || pixels[i + 2] !== nearest.b) changed++;
5923
+ pixels[i] = nearest.r;
5924
+ pixels[i + 1] = nearest.g;
5925
+ pixels[i + 2] = nearest.b;
5926
+ }
5927
+ return { png: { width: png.width, height: png.height, pixels }, changed };
5928
+ }
5929
+ function quantizeToPalette(png, palette) {
5930
+ return quantize(png, palette).png;
5931
+ }
5932
+
5933
+ // src/pipeline/postprocess.ts
5934
+ function postprocessFor(spec) {
5935
+ if (!spec.enforcePalette || spec.palette.length < 2) return void 0;
5936
+ return {
5937
+ palette: {
5938
+ colors: normalizePalette(spec.palette).map((color) => color.hex),
5939
+ dither: "none",
5940
+ distance: "redmean"
5941
+ }
5942
+ };
5943
+ }
5944
+ function samePostprocess(a, b) {
5945
+ const pa = a?.palette;
5946
+ const pb = b?.palette;
5947
+ if (!pa && !pb) return true;
5948
+ if (!pa || !pb) return false;
5949
+ return pa.dither === pb.dither && pa.distance === pb.distance && pa.colors.length === pb.colors.length && pa.colors.every((color, i) => color === pb.colors[i]);
5950
+ }
5951
+ function postprocessCurrent(entry, spec) {
5952
+ return samePostprocess(entry.postprocess, postprocessFor(spec));
5953
+ }
5954
+ function applyPostprocess(bytes, mediaType, record) {
5955
+ if (!record?.palette || mediaType !== MediaType.PNG) return { bytes, changed: false };
5956
+ const png = decodePng(bytes);
5957
+ const { png: snapped, changed } = quantize(png, record.palette.colors);
5958
+ if (!changed) return { bytes, changed: false };
5959
+ return { bytes: encodeRgbaPng(snapped.width, snapped.height, snapped.pixels), changed: true };
5960
+ }
5961
+
5962
+ // src/pipeline/mirror.ts
5963
+ function mirrorSourceHash(source) {
5964
+ return sha256(JSON.stringify(source.outputs.map((output) => output.sha256)));
5965
+ }
5966
+ function mirrorCurrent(entry, source) {
5967
+ return Boolean(entry.mirror && source && entry.mirror.sourceSha256 === mirrorSourceHash(source));
5968
+ }
5969
+ function mirroredRole(role) {
5970
+ if (role && role in MIRRORED_DIRECTION) return MIRRORED_DIRECTION[role];
5971
+ return role;
5972
+ }
5973
+ function flipHorizontal(png) {
5974
+ const { width, height, pixels } = decodePng(png);
5975
+ const flipped = Buffer.alloc(pixels.length);
5976
+ const stride = width * 4;
5977
+ for (let y = 0; y < height; y++) {
5978
+ const row = y * stride;
5979
+ for (let x = 0; x < width; x++) {
5980
+ pixels.copy(flipped, row + (width - 1 - x) * 4, row + x * 4, row + x * 4 + 4);
5981
+ }
5601
5982
  }
5602
- return vars;
5983
+ return encodeRgbaPng(width, height, flipped);
5984
+ }
5985
+ async function deriveMirror(spec, lock, lockPath, opts = {}) {
5986
+ const log = opts.onProgress ?? (() => {
5987
+ });
5988
+ const mirror = spec.mirror;
5989
+ const key = lockKey(spec.styleId, spec.assetId);
5990
+ const sourceKey = lockKey(spec.styleId, mirror.sourceAssetId);
5991
+ const source = lock.entries[sourceKey];
5992
+ if (!source || source.status !== "downloaded" || !source.outputs.length) {
5993
+ throw new Error(`mirror source ${sourceKey} has no downloaded outputs`);
5994
+ }
5995
+ const members = source.outputs.map((output, index) => ({
5996
+ file: currentEntryOutputPath(source, mirror.sourceSpec, index),
5997
+ role: mirroredRole(output.role)
5998
+ }));
5999
+ if (members.every((member) => member.role && CHARACTER_DIRECTIONS_8.includes(member.role))) {
6000
+ members.sort((a, b) => CHARACTER_DIRECTIONS_8.indexOf(a.role) - CHARACTER_DIRECTIONS_8.indexOf(b.role));
6001
+ }
6002
+ const outputs = [];
6003
+ const written = [];
6004
+ for (const [index, member] of members.entries()) {
6005
+ const bytes = flipHorizontal(await (0, import_promises4.readFile)(member.file));
6006
+ const target = expectedOutputPath(spec, member.role, index, members.length, MediaType.PNG);
6007
+ await (0, import_promises4.mkdir)(import_node_path7.default.dirname(target), { recursive: true });
6008
+ const tmp = `${target}.pixelkiln.tmp`;
6009
+ await (0, import_promises4.writeFile)(tmp, bytes);
6010
+ await (0, import_promises4.rename)(tmp, target);
6011
+ written.push(target);
6012
+ outputs.push({
6013
+ path: portableOutputPath(target, spec.root),
6014
+ sha256: sha256(bytes),
6015
+ ...member.role ? { role: member.role } : {},
6016
+ mediaType: MediaType.PNG
6017
+ });
6018
+ }
6019
+ const sourceMetadata = source.providerMetadata?.[source.provider] ?? {};
6020
+ const character = spec.character;
6021
+ const entry = upsert(lock, key, {
6022
+ styleId: spec.styleId,
6023
+ assetId: spec.assetId,
6024
+ specHash: spec.specHash,
6025
+ generator: spec.generator,
6026
+ tileFeature: null,
6027
+ prompt: "",
6028
+ width: spec.width,
6029
+ height: spec.height,
6030
+ revision: null,
6031
+ mirror: { sourceAssetId: mirror.sourceAssetId, sourceSha256: mirrorSourceHash(source) },
6032
+ supersededOutputs: [],
6033
+ jobId: null,
6034
+ submissionComplete: void 0,
6035
+ reviewObjectId: null,
6036
+ objectId: null,
6037
+ candidateIndex: null,
6038
+ status: "downloaded",
6039
+ error: null,
6040
+ outputs,
6041
+ providerMetadata: {
6042
+ [source.provider]: {
6043
+ ...sourceMetadata.frameSet ? { frameSet: sourceMetadata.frameSet } : {},
6044
+ ...character ? {
6045
+ character: {
6046
+ kind: character.kind,
6047
+ mirrorOf: mirror.sourceAssetId,
6048
+ ...character.animation ? { direction: character.animation.direction } : {}
6049
+ }
6050
+ } : {}
6051
+ }
6052
+ },
6053
+ sourceUrl: null,
6054
+ sourceUrls: [],
6055
+ submittedAt: (/* @__PURE__ */ new Date()).toISOString(),
6056
+ downloadedAt: (/* @__PURE__ */ new Date()).toISOString(),
6057
+ history: [],
6058
+ cost: 0,
6059
+ costUnit: spec.costUnit,
6060
+ provider: source.provider,
6061
+ postprocess: postprocessFor(spec)
6062
+ });
6063
+ await saveLock(lockPath, lock);
6064
+ log(` mirror ${key} \u2190 ${sourceKey} (${written.length} file${written.length === 1 ? "" : "s"} flipped, no generation cost)`);
6065
+ return entry;
5603
6066
  }
5604
-
5605
- // src/pipeline/plan.ts
5606
- var import_node_fs9 = require("fs");
5607
- var import_node_path11 = __toESM(require("path"), 1);
5608
6067
 
5609
6068
  // src/pipeline/quality-profile.ts
5610
6069
  var import_node_fs7 = require("fs");
5611
- var import_node_path9 = __toESM(require("path"), 1);
6070
+ var import_node_path10 = __toESM(require("path"), 1);
5612
6071
 
5613
6072
  // src/pipeline/refine.ts
5614
- var import_node_path8 = __toESM(require("path"), 1);
6073
+ var import_node_path9 = __toESM(require("path"), 1);
5615
6074
  var import_node_child_process2 = require("child_process");
5616
- var import_promises5 = require("fs/promises");
6075
+ var import_promises6 = require("fs/promises");
5617
6076
  var import_node_os2 = require("os");
5618
6077
  var import_node_util = require("util");
5619
6078
 
5620
6079
  // src/artifacts.ts
5621
- var import_node_path7 = __toESM(require("path"), 1);
6080
+ var import_node_path8 = __toESM(require("path"), 1);
5622
6081
  var import_node_crypto5 = require("crypto");
5623
- var import_promises4 = require("fs/promises");
6082
+ var import_promises5 = require("fs/promises");
5624
6083
  var activeTransactions = /* @__PURE__ */ new Set();
5625
6084
  function message(error) {
5626
6085
  return error instanceof Error ? error.message : String(error);
@@ -5629,7 +6088,7 @@ function digest(data) {
5629
6088
  return (0, import_node_crypto5.createHash)("sha256").update(data).digest("hex");
5630
6089
  }
5631
6090
  function portableRelative(from, to) {
5632
- return import_node_path7.default.relative(from, import_node_path7.default.resolve(to)).split(import_node_path7.default.sep).join("/") || ".";
6091
+ return import_node_path8.default.relative(from, import_node_path8.default.resolve(to)).split(import_node_path8.default.sep).join("/") || ".";
5633
6092
  }
5634
6093
  function canonical2(value) {
5635
6094
  if (value === null || typeof value === "string" || typeof value === "boolean") return value;
@@ -5679,12 +6138,12 @@ function parseArtifactManifest(absolute, data) {
5679
6138
  return raw;
5680
6139
  }
5681
6140
  async function readArtifactBundleManifest(manifestPath) {
5682
- const absolute = import_node_path7.default.resolve(manifestPath);
5683
- return parseArtifactManifest(absolute, await (0, import_promises4.readFile)(absolute, "utf8"));
6141
+ const absolute = import_node_path8.default.resolve(manifestPath);
6142
+ return parseArtifactManifest(absolute, await (0, import_promises5.readFile)(absolute, "utf8"));
5684
6143
  }
5685
6144
  async function readOptional(file) {
5686
6145
  try {
5687
- return await (0, import_promises4.readFile)(file);
6146
+ return await (0, import_promises5.readFile)(file);
5688
6147
  } catch (error) {
5689
6148
  if (isCode(error, "ENOENT")) return null;
5690
6149
  throw error;
@@ -5703,8 +6162,8 @@ function processIsAlive(pid) {
5703
6162
  }
5704
6163
  }
5705
6164
  function validTemporaryPath(candidate, destination, type) {
5706
- return import_node_path7.default.dirname(candidate) === import_node_path7.default.dirname(destination) && import_node_path7.default.basename(candidate).startsWith(
5707
- `.${import_node_path7.default.basename(destination)}.pixelkiln-${type}-`
6165
+ return import_node_path8.default.dirname(candidate) === import_node_path8.default.dirname(destination) && import_node_path8.default.basename(candidate).startsWith(
6166
+ `.${import_node_path8.default.basename(destination)}.pixelkiln-${type}-`
5708
6167
  );
5709
6168
  }
5710
6169
  function parseTransaction(journal, data) {
@@ -5720,14 +6179,14 @@ function parseTransaction(journal, data) {
5720
6179
  return raw;
5721
6180
  }
5722
6181
  async function removeTransactionFiles(journal) {
5723
- await (0, import_promises4.rm)(journal, { force: true });
5724
- await (0, import_promises4.rm)(transactionMarker(journal), { force: true });
6182
+ await (0, import_promises5.rm)(journal, { force: true });
6183
+ await (0, import_promises5.rm)(transactionMarker(journal), { force: true });
5725
6184
  }
5726
6185
  async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
5727
- const journal = import_node_path7.default.resolve(recoveryFile);
6186
+ const journal = import_node_path8.default.resolve(recoveryFile);
5728
6187
  const bytes = await readOptional(journal);
5729
6188
  if (!bytes) {
5730
- await (0, import_promises4.rm)(transactionMarker(journal), { force: true });
6189
+ await (0, import_promises5.rm)(transactionMarker(journal), { force: true });
5731
6190
  return;
5732
6191
  }
5733
6192
  if (activeTransactions.has(journal)) {
@@ -5740,7 +6199,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
5740
6199
  );
5741
6200
  }
5742
6201
  for (const entry of transaction.entries) {
5743
- if (typeof entry.destination !== "string" || typeof entry.stage !== "string" || typeof entry.sha256 !== "string" || entry.backup !== void 0 && typeof entry.backup !== "string" || !allowedDestinations.has(import_node_path7.default.resolve(entry.destination)) || !validTemporaryPath(entry.stage, entry.destination, "stage") || entry.backup !== void 0 && !validTemporaryPath(entry.backup, entry.destination, "backup")) {
6202
+ if (typeof entry.destination !== "string" || typeof entry.stage !== "string" || typeof entry.sha256 !== "string" || entry.backup !== void 0 && typeof entry.backup !== "string" || !allowedDestinations.has(import_node_path8.default.resolve(entry.destination)) || !validTemporaryPath(entry.stage, entry.destination, "stage") || entry.backup !== void 0 && !validTemporaryPath(entry.backup, entry.destination, "backup")) {
5744
6203
  throw new Error(
5745
6204
  `Refusing unsafe artifact recovery from ${journal}; its destinations or temporary paths do not match the current bundle.`
5746
6205
  );
@@ -5766,7 +6225,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
5766
6225
  }
5767
6226
  await remove2(entry.destination, errors);
5768
6227
  try {
5769
- await (0, import_promises4.rename)(entry.backup, entry.destination);
6228
+ await (0, import_promises5.rename)(entry.backup, entry.destination);
5770
6229
  } catch (error) {
5771
6230
  errors.push(
5772
6231
  `could not restore ${entry.destination}; previous file remains at ${entry.backup}: ${message(error)}`
@@ -5786,7 +6245,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
5786
6245
  await removeTransactionFiles(journal);
5787
6246
  }
5788
6247
  function createArtifactBundleManifest(manifestPath, outputs, provenance) {
5789
- const root = import_node_path7.default.dirname(import_node_path7.default.resolve(manifestPath));
6248
+ const root = import_node_path8.default.dirname(import_node_path8.default.resolve(manifestPath));
5790
6249
  const sources = provenance.sources.map((source) => ({
5791
6250
  ...source,
5792
6251
  path: portableRelative(root, source.path)
@@ -5816,11 +6275,11 @@ function withArtifactManifest(manifestPath, outputs, provenance) {
5816
6275
  ];
5817
6276
  }
5818
6277
  async function writeManagedArtifactBundle(manifestPath, outputs, provenance, options = {}) {
5819
- const absoluteManifest = import_node_path7.default.resolve(manifestPath);
6278
+ const absoluteManifest = import_node_path8.default.resolve(manifestPath);
5820
6279
  const recoveryFile = `${absoluteManifest}.transaction`;
5821
6280
  const allowedDestinations = /* @__PURE__ */ new Set([
5822
6281
  absoluteManifest,
5823
- ...outputs.map((output) => import_node_path7.default.resolve(output.path))
6282
+ ...outputs.map((output) => import_node_path8.default.resolve(output.path))
5824
6283
  ]);
5825
6284
  await recoverArtifactTransaction(recoveryFile, allowedDestinations);
5826
6285
  const existingManifestBytes = await readOptional(absoluteManifest);
@@ -5838,13 +6297,13 @@ async function writeManagedArtifactBundle(manifestPath, outputs, provenance, opt
5838
6297
  }
5839
6298
  }
5840
6299
  if (!options.force) {
5841
- const root = import_node_path7.default.dirname(absoluteManifest);
6300
+ const root = import_node_path8.default.dirname(absoluteManifest);
5842
6301
  const recorded = new Map(
5843
- previous?.outputs.map((output) => [import_node_path7.default.resolve(root, output.path), output.sha256]) ?? []
6302
+ previous?.outputs.map((output) => [import_node_path8.default.resolve(root, output.path), output.sha256]) ?? []
5844
6303
  );
5845
6304
  const conflicts = [];
5846
6305
  for (const output of outputs) {
5847
- const destination = import_node_path7.default.resolve(output.path);
6306
+ const destination = import_node_path8.default.resolve(output.path);
5848
6307
  const current = await readOptional(destination);
5849
6308
  if (!current || digest(current) === digest(output.data)) continue;
5850
6309
  const expected = recorded.get(destination);
@@ -5866,15 +6325,15 @@ Pass { force: true } (CLI: --force) to take ownership and replace the complete b
5866
6325
  );
5867
6326
  }
5868
6327
  async function verifyArtifactBundle(manifestPath) {
5869
- const absolute = import_node_path7.default.resolve(manifestPath);
6328
+ const absolute = import_node_path8.default.resolve(manifestPath);
5870
6329
  const raw = await readArtifactBundleManifest(absolute);
5871
- const root = import_node_path7.default.dirname(absolute);
6330
+ const root = import_node_path8.default.dirname(absolute);
5872
6331
  const changedSources = [];
5873
6332
  const changedOutputs = [];
5874
6333
  for (const source of raw.sources) {
5875
6334
  let actual = null;
5876
6335
  try {
5877
- actual = digest(await (0, import_promises4.readFile)(import_node_path7.default.resolve(root, source.path)));
6336
+ actual = digest(await (0, import_promises5.readFile)(import_node_path8.default.resolve(root, source.path)));
5878
6337
  } catch (error) {
5879
6338
  if (!isCode(error, "ENOENT")) throw error;
5880
6339
  }
@@ -5882,7 +6341,7 @@ async function verifyArtifactBundle(manifestPath) {
5882
6341
  }
5883
6342
  for (const output of raw.outputs) {
5884
6343
  try {
5885
- if (digest(await (0, import_promises4.readFile)(import_node_path7.default.resolve(root, output.path))) !== output.sha256) {
6344
+ if (digest(await (0, import_promises5.readFile)(import_node_path8.default.resolve(root, output.path))) !== output.sha256) {
5886
6345
  changedOutputs.push(output.path);
5887
6346
  }
5888
6347
  } catch (error) {
@@ -5908,7 +6367,7 @@ function isCode(error, code) {
5908
6367
  }
5909
6368
  async function remove2(file, errors) {
5910
6369
  try {
5911
- await (0, import_promises4.rm)(file, { force: true });
6370
+ await (0, import_promises5.rm)(file, { force: true });
5912
6371
  } catch (error) {
5913
6372
  errors?.push(`could not remove ${file}: ${message(error)}`);
5914
6373
  }
@@ -5921,7 +6380,7 @@ async function rollback(prepared) {
5921
6380
  for (const artifact of [...prepared].reverse()) {
5922
6381
  if (!artifact.backup || !artifact.backedUp) continue;
5923
6382
  try {
5924
- await (0, import_promises4.rename)(artifact.backup, artifact.destination);
6383
+ await (0, import_promises5.rename)(artifact.backup, artifact.destination);
5925
6384
  artifact.backedUp = false;
5926
6385
  } catch (error) {
5927
6386
  errors.push(
@@ -5938,7 +6397,7 @@ async function writeArtifactBundle(files, options = {}) {
5938
6397
  if (!files.length) throw new Error("An artifact bundle must contain at least one file.");
5939
6398
  const normalized = files.map((file) => {
5940
6399
  if (!file.path.trim()) throw new Error("Artifact paths cannot be empty.");
5941
- return { destination: import_node_path7.default.resolve(file.path), data: Buffer.from(file.data) };
6400
+ return { destination: import_node_path8.default.resolve(file.path), data: Buffer.from(file.data) };
5942
6401
  });
5943
6402
  const destinations = /* @__PURE__ */ new Set();
5944
6403
  for (const artifact of normalized) {
@@ -5954,7 +6413,7 @@ async function writeArtifactBundle(files, options = {}) {
5954
6413
  const unchanged = [];
5955
6414
  for (const artifact of normalized) {
5956
6415
  try {
5957
- const current = await (0, import_promises4.readFile)(artifact.destination);
6416
+ const current = await (0, import_promises5.readFile)(artifact.destination);
5958
6417
  if (current.equals(artifact.data)) {
5959
6418
  unchanged.push(artifact.destination);
5960
6419
  } else {
@@ -5968,21 +6427,21 @@ async function writeArtifactBundle(files, options = {}) {
5968
6427
  if (!changed.length) return { changed: [], unchanged };
5969
6428
  const token = (0, import_node_crypto5.randomUUID)();
5970
6429
  for (const [index, artifact] of changed.entries()) {
5971
- const basename = import_node_path7.default.basename(artifact.destination);
5972
- artifact.stage = import_node_path7.default.join(
5973
- import_node_path7.default.dirname(artifact.destination),
6430
+ const basename = import_node_path8.default.basename(artifact.destination);
6431
+ artifact.stage = import_node_path8.default.join(
6432
+ import_node_path8.default.dirname(artifact.destination),
5974
6433
  `.${basename}.pixelkiln-stage-${token}-${index}`
5975
6434
  );
5976
6435
  if (artifact.existed) {
5977
- artifact.backup = import_node_path7.default.join(
5978
- import_node_path7.default.dirname(artifact.destination),
6436
+ artifact.backup = import_node_path8.default.join(
6437
+ import_node_path8.default.dirname(artifact.destination),
5979
6438
  `.${basename}.pixelkiln-backup-${token}-${index}`
5980
6439
  );
5981
6440
  }
5982
6441
  }
5983
- const journal = options.recoveryFile ? import_node_path7.default.resolve(options.recoveryFile) : null;
6442
+ const journal = options.recoveryFile ? import_node_path8.default.resolve(options.recoveryFile) : null;
5984
6443
  if (journal) {
5985
- await (0, import_promises4.mkdir)(import_node_path7.default.dirname(journal), { recursive: true });
6444
+ await (0, import_promises5.mkdir)(import_node_path8.default.dirname(journal), { recursive: true });
5986
6445
  const transaction = {
5987
6446
  format: "pixelkiln-artifact-transaction",
5988
6447
  version: 1,
@@ -5996,7 +6455,7 @@ async function writeArtifactBundle(files, options = {}) {
5996
6455
  }))
5997
6456
  };
5998
6457
  try {
5999
- await (0, import_promises4.writeFile)(journal, JSON.stringify(transaction, null, 2) + "\n", { flag: "wx" });
6458
+ await (0, import_promises5.writeFile)(journal, JSON.stringify(transaction, null, 2) + "\n", { flag: "wx" });
6000
6459
  activeTransactions.add(journal);
6001
6460
  } catch (error) {
6002
6461
  throw new Error(
@@ -6007,9 +6466,9 @@ async function writeArtifactBundle(files, options = {}) {
6007
6466
  }
6008
6467
  try {
6009
6468
  for (const [index, artifact] of changed.entries()) {
6010
- await (0, import_promises4.mkdir)(import_node_path7.default.dirname(artifact.destination), { recursive: true });
6469
+ await (0, import_promises5.mkdir)(import_node_path8.default.dirname(artifact.destination), { recursive: true });
6011
6470
  await options.beforeStage?.(artifact.destination, index);
6012
- await (0, import_promises4.writeFile)(artifact.stage, artifact.data, { flag: "wx" });
6471
+ await (0, import_promises5.writeFile)(artifact.stage, artifact.data, { flag: "wx" });
6013
6472
  }
6014
6473
  } catch (error) {
6015
6474
  const cleanupErrors2 = await rollback(changed);
@@ -6022,17 +6481,17 @@ async function writeArtifactBundle(files, options = {}) {
6022
6481
  try {
6023
6482
  for (const artifact of changed) {
6024
6483
  if (!artifact.existed) continue;
6025
- await (0, import_promises4.rename)(artifact.destination, artifact.backup);
6484
+ await (0, import_promises5.rename)(artifact.destination, artifact.backup);
6026
6485
  artifact.backedUp = true;
6027
6486
  }
6028
6487
  for (const [index, artifact] of changed.entries()) {
6029
6488
  await options.beforePromote?.(artifact.destination, index);
6030
- await (0, import_promises4.rename)(artifact.stage, artifact.destination);
6489
+ await (0, import_promises5.rename)(artifact.stage, artifact.destination);
6031
6490
  artifact.stage = void 0;
6032
6491
  artifact.promoted = true;
6033
6492
  }
6034
6493
  if (journal) {
6035
- await (0, import_promises4.writeFile)(transactionMarker(journal), `${token}
6494
+ await (0, import_promises5.writeFile)(transactionMarker(journal), `${token}
6036
6495
  `, { flag: "wx" });
6037
6496
  durableCommit = true;
6038
6497
  await options.afterCommit?.();
@@ -6074,69 +6533,6 @@ async function writeArtifactBundle(files, options = {}) {
6074
6533
  };
6075
6534
  }
6076
6535
 
6077
- // src/palette.ts
6078
- function normalizePalette(values) {
6079
- const colors = /* @__PURE__ */ new Map();
6080
- for (const value of values) {
6081
- const rgb = parseHex(value);
6082
- const hex2 = `#${rgb.r.toString(16).padStart(2, "0")}${rgb.g.toString(16).padStart(2, "0")}${rgb.b.toString(16).padStart(2, "0")}`;
6083
- if (!colors.has(hex2)) colors.set(hex2, { hex: hex2, ...rgb });
6084
- }
6085
- if (colors.size < 2 || colors.size > 256) {
6086
- throw new Error(`A palette must contain 2\u2013256 unique colors; got ${colors.size}.`);
6087
- }
6088
- return [...colors.values()];
6089
- }
6090
- function colorDistance(a, b) {
6091
- const rmean = (a.r + b.r) / 2;
6092
- const dr = a.r - b.r;
6093
- const dg = a.g - b.g;
6094
- const db = a.b - b.b;
6095
- return Math.sqrt(
6096
- (512 + rmean) * dr * dr / 256 + 4 * dg * dg + (767 - rmean) * db * db / 256
6097
- );
6098
- }
6099
- function quantize(png, palette) {
6100
- const colors = normalizePalette(palette);
6101
- const pixels = Buffer.from(png.pixels);
6102
- const nearestOf = /* @__PURE__ */ new Map();
6103
- let changed = 0;
6104
- for (let i = 0; i < pixels.length; i += 4) {
6105
- const alpha = pixels[i + 3];
6106
- if (alpha === 0) {
6107
- if (pixels[i] || pixels[i + 1] || pixels[i + 2]) changed++;
6108
- pixels[i] = 0;
6109
- pixels[i + 1] = 0;
6110
- pixels[i + 2] = 0;
6111
- continue;
6112
- }
6113
- const key = pixels[i] << 16 | pixels[i + 1] << 8 | pixels[i + 2];
6114
- let nearest = nearestOf.get(key);
6115
- if (!nearest) {
6116
- const pixel = { r: pixels[i], g: pixels[i + 1], b: pixels[i + 2] };
6117
- nearest = colors[0];
6118
- let distance = colorDistance(pixel, nearest);
6119
- for (let j = 1; j < colors.length; j++) {
6120
- const candidate = colors[j];
6121
- const candidateDistance = colorDistance(pixel, candidate);
6122
- if (candidateDistance < distance) {
6123
- nearest = candidate;
6124
- distance = candidateDistance;
6125
- }
6126
- }
6127
- nearestOf.set(key, nearest);
6128
- }
6129
- if (pixels[i] !== nearest.r || pixels[i + 1] !== nearest.g || pixels[i + 2] !== nearest.b) changed++;
6130
- pixels[i] = nearest.r;
6131
- pixels[i + 1] = nearest.g;
6132
- pixels[i + 2] = nearest.b;
6133
- }
6134
- return { png: { width: png.width, height: png.height, pixels }, changed };
6135
- }
6136
- function quantizeToPalette(png, palette) {
6137
- return quantize(png, palette).png;
6138
- }
6139
-
6140
6536
  // src/pipeline/refine.ts
6141
6537
  var execFileAsync = (0, import_node_util.promisify)(import_node_child_process2.execFile);
6142
6538
  var PIXEL_ART_FIXER_MIN_SIDE = 16;
@@ -6336,16 +6732,16 @@ function sameFrameGrid(first, next) {
6336
6732
  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;
6337
6733
  }
6338
6734
  async function refineAsset(options) {
6339
- const source = import_node_path8.default.resolve(options.source);
6340
- const output = import_node_path8.default.resolve(options.output);
6735
+ const source = import_node_path9.default.resolve(options.source);
6736
+ const output = import_node_path9.default.resolve(options.output);
6341
6737
  if (source === output) throw new Error("Refinement source and output must be different files.");
6342
- const record = import_node_path8.default.resolve(qualityRecordPath(output));
6738
+ const record = import_node_path9.default.resolve(qualityRecordPath(output));
6343
6739
  const palette = normalizePalette(options.palette).map((color) => color.hex);
6344
6740
  const minGridConfidence = requireConfidence(options.minGridConfidence ?? "high");
6345
6741
  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.");
6346
6742
  let sourcePng;
6347
6743
  try {
6348
- sourcePng = decodePng(await (0, import_promises5.readFile)(source));
6744
+ sourcePng = decodePng(await (0, import_promises6.readFile)(source));
6349
6745
  } catch (error) {
6350
6746
  throw new Error(`Cannot read refinement source ${source}: ${message2(error)}`, { cause: error });
6351
6747
  }
@@ -6362,8 +6758,8 @@ async function refineAsset(options) {
6362
6758
  );
6363
6759
  }
6364
6760
  }
6365
- const temp = await (0, import_promises5.mkdtemp)(import_node_path8.default.join((0, import_node_os2.tmpdir)(), "pixelkiln-refine-"));
6366
- const recovered = import_node_path8.default.join(temp, "native.png");
6761
+ const temp = await (0, import_promises6.mkdtemp)(import_node_path9.default.join((0, import_node_os2.tmpdir)(), "pixelkiln-refine-"));
6762
+ const recovered = import_node_path9.default.join(temp, "native.png");
6367
6763
  const fixerCommand = compatibilityCommand ?? options.fixerPython ?? process.env.PIXELKILN_PIXEL_FIXER_PYTHON ?? "python3";
6368
6764
  const args = compatibilityCommand ? [...options.fixerArgsPrefix ?? [], source, "--extract", recovered] : ["-c", PYTHON_FIXER_BRIDGE, source, recovered];
6369
6765
  try {
@@ -6384,7 +6780,7 @@ async function refineAsset(options) {
6384
6780
  );
6385
6781
  }
6386
6782
  const detection = parseDetection(firstJsonObject(stdout));
6387
- const native = decodePng(await (0, import_promises5.readFile)(recovered));
6783
+ const native = decodePng(await (0, import_promises6.readFile)(recovered));
6388
6784
  const quantized = quantizeToPalette(native, palette);
6389
6785
  const png = encodeRgbaPng(quantized.width, quantized.height, quantized.pixels);
6390
6786
  const audit = auditRefinement(
@@ -6434,7 +6830,7 @@ async function refineAsset(options) {
6434
6830
  );
6435
6831
  return { source, output, record, detection, palette, audit };
6436
6832
  } finally {
6437
- await (0, import_promises5.rm)(temp, { recursive: true, force: true });
6833
+ await (0, import_promises6.rm)(temp, { recursive: true, force: true });
6438
6834
  }
6439
6835
  }
6440
6836
  async function refineFrameSet(options) {
@@ -6448,18 +6844,18 @@ async function refineFrameSet(options) {
6448
6844
  if (roles.some((role) => !role.trim()) || new Set(roles).size !== roles.length) {
6449
6845
  throw new Error("Frame-set roles must be non-empty and unique.");
6450
6846
  }
6451
- const output = import_node_path8.default.resolve(options.output);
6452
- const record = import_node_path8.default.resolve(qualityRecordPath(output));
6847
+ const output = import_node_path9.default.resolve(options.output);
6848
+ const record = import_node_path9.default.resolve(qualityRecordPath(output));
6453
6849
  const palette = normalizeRefinementPalette(options.palette);
6454
6850
  const finalOutputs = options.sources.map((source, index) => qualityFrameOutputPath(output, source.role, index));
6455
6851
  if (new Set(finalOutputs).size !== finalOutputs.length) {
6456
6852
  throw new Error("Frame-set roles resolve to duplicate output filenames.");
6457
6853
  }
6458
- const sourceSet = new Set(options.sources.map((source) => import_node_path8.default.resolve(source.path)));
6854
+ const sourceSet = new Set(options.sources.map((source) => import_node_path9.default.resolve(source.path)));
6459
6855
  if (finalOutputs.some((file) => sourceSet.has(file))) {
6460
6856
  throw new Error("Frame-set refinement sources and outputs must be different files.");
6461
6857
  }
6462
- const temp = await (0, import_promises5.mkdtemp)(import_node_path8.default.join((0, import_node_os2.tmpdir)(), "pixelkiln-refine-frames-"));
6858
+ const temp = await (0, import_promises6.mkdtemp)(import_node_path9.default.join((0, import_node_os2.tmpdir)(), "pixelkiln-refine-frames-"));
6463
6859
  try {
6464
6860
  const frames = [];
6465
6861
  const outputs = [];
@@ -6467,8 +6863,8 @@ async function refineFrameSet(options) {
6467
6863
  const sourcePaths = [];
6468
6864
  let firstOptions = null;
6469
6865
  for (const [index, sourceEntry] of options.sources.entries()) {
6470
- const source = import_node_path8.default.resolve(sourceEntry.path);
6471
- const temporaryOutput = import_node_path8.default.join(temp, `${String(index).padStart(2, "0")}.png`);
6866
+ const source = import_node_path9.default.resolve(sourceEntry.path);
6867
+ const temporaryOutput = import_node_path9.default.join(temp, `${String(index).padStart(2, "0")}.png`);
6472
6868
  const refined = await refineAsset({
6473
6869
  source,
6474
6870
  output: temporaryOutput,
@@ -6500,7 +6896,7 @@ async function refineFrameSet(options) {
6500
6896
  });
6501
6897
  outputs.push({
6502
6898
  path: finalOutputs[index],
6503
- data: await (0, import_promises5.readFile)(temporaryOutput)
6899
+ data: await (0, import_promises6.readFile)(temporaryOutput)
6504
6900
  });
6505
6901
  }
6506
6902
  const first = frames[0];
@@ -6543,23 +6939,23 @@ async function refineFrameSet(options) {
6543
6939
  );
6544
6940
  return {
6545
6941
  sources: sourcePaths,
6546
- outputs: outputs.map((item) => import_node_path8.default.resolve(item.path)),
6942
+ outputs: outputs.map((item) => import_node_path9.default.resolve(item.path)),
6547
6943
  record,
6548
6944
  frames,
6549
6945
  palette,
6550
6946
  audit
6551
6947
  };
6552
6948
  } finally {
6553
- await (0, import_promises5.rm)(temp, { recursive: true, force: true });
6949
+ await (0, import_promises6.rm)(temp, { recursive: true, force: true });
6554
6950
  }
6555
6951
  }
6556
6952
  async function checkQualityRecord(recordPath) {
6557
- const record = import_node_path8.default.resolve(recordPath);
6953
+ const record = import_node_path9.default.resolve(recordPath);
6558
6954
  const manifest = await readArtifactBundleManifest(record);
6559
6955
  const options = parseRecordOptions(manifest, record);
6560
6956
  const verification = await verifyArtifactBundle(record);
6561
- const sources = manifest.sources.map((source2) => import_node_path8.default.resolve(import_node_path8.default.dirname(record), source2.path));
6562
- const outputs = manifest.outputs.map((output2) => import_node_path8.default.resolve(import_node_path8.default.dirname(record), output2.path));
6957
+ const sources = manifest.sources.map((source2) => import_node_path9.default.resolve(import_node_path9.default.dirname(record), source2.path));
6958
+ const outputs = manifest.outputs.map((output2) => import_node_path9.default.resolve(import_node_path9.default.dirname(record), output2.path));
6563
6959
  const source = sources[0] ?? "";
6564
6960
  const output = outputs[0] ?? "";
6565
6961
  const expectedCount = options.frameSet?.count ?? 1;
@@ -6592,7 +6988,7 @@ async function checkQualityRecord(recordPath) {
6592
6988
  async function approveQualityRecord(recordPath, approval) {
6593
6989
  const reviewer = approval.reviewer.trim();
6594
6990
  if (!reviewer) throw new Error("A human reviewer name is required.");
6595
- const record = import_node_path8.default.resolve(recordPath);
6991
+ const record = import_node_path9.default.resolve(recordPath);
6596
6992
  const manifest = await readArtifactBundleManifest(record);
6597
6993
  const options = parseRecordOptions(manifest, record);
6598
6994
  const verification = await verifyArtifactBundle(record);
@@ -6613,14 +7009,14 @@ async function approveQualityRecord(recordPath, approval) {
6613
7009
  `A refinement record must contain exactly ${expectedCount} source(s) and output(s).`
6614
7010
  );
6615
7011
  }
6616
- const root = import_node_path8.default.dirname(record);
7012
+ const root = import_node_path9.default.dirname(record);
6617
7013
  const outputs = await Promise.all(manifest.outputs.map(async (entry) => {
6618
- const outputPath = import_node_path8.default.resolve(root, entry.path);
6619
- return { path: outputPath, data: await (0, import_promises5.readFile)(outputPath) };
7014
+ const outputPath = import_node_path9.default.resolve(root, entry.path);
7015
+ return { path: outputPath, data: await (0, import_promises6.readFile)(outputPath) };
6620
7016
  }));
6621
7017
  const sources = manifest.sources.map((source) => ({
6622
7018
  ...source,
6623
- path: import_node_path8.default.resolve(root, source.path)
7019
+ path: import_node_path9.default.resolve(root, source.path)
6624
7020
  }));
6625
7021
  const approved = {
6626
7022
  ...options,
@@ -6652,7 +7048,7 @@ async function qualitySource(spec, lock) {
6652
7048
  if (spec.generator === "frames") {
6653
7049
  return { items: null, reason: "frame-set quality requires ordered downloaded provider outputs" };
6654
7050
  }
6655
- const source = import_node_path9.default.resolve(spec.root, spec.source);
7051
+ const source = import_node_path10.default.resolve(spec.root, spec.source);
6656
7052
  return (0, import_node_fs7.existsSync)(source) ? { items: [{ role: "source", path: source }] } : { items: null, reason: `declared source is missing: ${spec.source}` };
6657
7053
  }
6658
7054
  const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
@@ -6719,7 +7115,7 @@ async function inspectQualityProfile(spec, lock) {
6719
7115
  const expectedRevision = spec.quality.fixerRevision ?? PIXEL_ART_FIXER_REVISION;
6720
7116
  const expectedTransparency = spec.quality.minTransparency ?? null;
6721
7117
  const expectedFrameSet = spec.generator === "frames" ? { fps: spec.quality.fps ?? 12, count: source.items.length, roles: source.items.map((item) => item.role) } : null;
6722
- const contractChanged = JSON.stringify(check.sources.map((item) => import_node_path9.default.resolve(item))) !== JSON.stringify(sourcePaths.map((item) => import_node_path9.default.resolve(item))) || JSON.stringify(check.outputs.map((item) => import_node_path9.default.resolve(item))) !== JSON.stringify(expectedOutputs.map((item) => import_node_path9.default.resolve(item))) || JSON.stringify(check.options.palette.colors) !== JSON.stringify(expectedPalette) || check.options.nativeGrid.revision !== expectedRevision || check.options.audit.thresholds.minGridConfidence !== spec.quality.minGridConfidence || check.options.audit.thresholds.minTransparency !== expectedTransparency || (expectedFrameSet ? !check.options.frameSet || check.options.frameSet.fps !== expectedFrameSet.fps || check.options.frameSet.count !== expectedFrameSet.count || JSON.stringify(check.options.frameSet.roles) !== JSON.stringify(expectedFrameSet.roles) : check.options.frameSet !== void 0);
7118
+ const contractChanged = JSON.stringify(check.sources.map((item) => import_node_path10.default.resolve(item))) !== JSON.stringify(sourcePaths.map((item) => import_node_path10.default.resolve(item))) || JSON.stringify(check.outputs.map((item) => import_node_path10.default.resolve(item))) !== JSON.stringify(expectedOutputs.map((item) => import_node_path10.default.resolve(item))) || JSON.stringify(check.options.palette.colors) !== JSON.stringify(expectedPalette) || check.options.nativeGrid.revision !== expectedRevision || check.options.audit.thresholds.minGridConfidence !== spec.quality.minGridConfidence || check.options.audit.thresholds.minTransparency !== expectedTransparency || (expectedFrameSet ? !check.options.frameSet || check.options.frameSet.fps !== expectedFrameSet.fps || check.options.frameSet.count !== expectedFrameSet.count || JSON.stringify(check.options.frameSet.roles) !== JSON.stringify(expectedFrameSet.roles) : check.options.frameSet !== void 0);
6723
7119
  if (contractChanged) {
6724
7120
  return {
6725
7121
  key,
@@ -6881,43 +7277,21 @@ async function requireApprovedQualitySources(specs, lock, assetIds, outputRoles)
6881
7277
  return sources;
6882
7278
  }
6883
7279
 
6884
- // src/pipeline/postprocess.ts
6885
- function postprocessFor(spec) {
6886
- if (!spec.enforcePalette || spec.palette.length < 2) return void 0;
6887
- return {
6888
- palette: {
6889
- colors: normalizePalette(spec.palette).map((color) => color.hex),
6890
- dither: "none",
6891
- distance: "redmean"
6892
- }
6893
- };
6894
- }
6895
- function samePostprocess(a, b) {
6896
- const pa = a?.palette;
6897
- const pb = b?.palette;
6898
- if (!pa && !pb) return true;
6899
- if (!pa || !pb) return false;
6900
- return pa.dither === pb.dither && pa.distance === pb.distance && pa.colors.length === pb.colors.length && pa.colors.every((color, i) => color === pb.colors[i]);
6901
- }
6902
- function postprocessCurrent(entry, spec) {
6903
- return samePostprocess(entry.postprocess, postprocessFor(spec));
6904
- }
6905
- function applyPostprocess(bytes, mediaType, record) {
6906
- if (!record?.palette || mediaType !== MediaType.PNG) return { bytes, changed: false };
6907
- const png = decodePng(bytes);
6908
- const { png: snapped, changed } = quantize(png, record.palette.colors);
6909
- if (!changed) return { bytes, changed: false };
6910
- return { bytes: encodeRgbaPng(snapped.width, snapped.height, snapped.pixels), changed: true };
6911
- }
6912
-
6913
7280
  // src/pipeline/revision.ts
6914
7281
  var import_node_fs8 = require("fs");
6915
- var import_node_path10 = __toESM(require("path"), 1);
7282
+ var import_node_path11 = __toESM(require("path"), 1);
6916
7283
  async function inspectRevisionReadiness(spec, lock) {
7284
+ if (spec.mirror) return inspectMirror(spec, lock, /* @__PURE__ */ new Set());
6917
7285
  if (spec.character?.parentSpec) return inspectCharacterParent(spec, lock, /* @__PURE__ */ new Set());
6918
7286
  if (!spec.revision) return null;
6919
7287
  return inspectRevision(spec, lock, /* @__PURE__ */ new Set());
6920
7288
  }
7289
+ async function inspectMirror(spec, lock, seen) {
7290
+ const source = spec.mirror.sourceSpec;
7291
+ const label = `mirror source ${spec.styleId}/${spec.mirror.sourceAssetId}`;
7292
+ const dependency = await inspectParent(source, lock, seen);
7293
+ return dependency.ready ? { ready: true, reason: `${label} is current` } : { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
7294
+ }
6921
7295
  async function inspectCharacterParent(spec, lock, seen) {
6922
7296
  const character = spec.character;
6923
7297
  const parentSpec = character.parentSpec;
@@ -6971,6 +7345,10 @@ async function inspectParent(spec, lock, seen) {
6971
7345
  if (seen.has(key)) return { ready: false, reason: `revision dependency cycle at ${key}` };
6972
7346
  const nextSeen = new Set(seen);
6973
7347
  nextSeen.add(key);
7348
+ if (spec.mirror) {
7349
+ const inputs = await inspectMirror(spec, lock, nextSeen);
7350
+ if (!inputs.ready) return inputs;
7351
+ }
6974
7352
  if (spec.revision) {
6975
7353
  const inputs = await inspectRevision(spec, lock, nextSeen);
6976
7354
  if (!inputs.ready) return inputs;
@@ -6987,7 +7365,7 @@ async function inspectParent(spec, lock, seen) {
6987
7365
  };
6988
7366
  }
6989
7367
  if (spec.source) {
6990
- const source = import_node_path10.default.resolve(spec.root, spec.source);
7368
+ const source = import_node_path11.default.resolve(spec.root, spec.source);
6991
7369
  return (0, import_node_fs8.existsSync)(source) ? { ready: true, reason: "declared source is present" } : { ready: false, reason: `declared source is missing: ${spec.source}` };
6992
7370
  }
6993
7371
  const entry = lock.entries[key];
@@ -7057,7 +7435,7 @@ async function buildPlan(specs, lock, opts = {}) {
7057
7435
  state = "missing";
7058
7436
  reason = "--force";
7059
7437
  } else if (!entry && spec.source && spec.generator !== "frames") {
7060
- if ((0, import_node_fs9.existsSync)(import_node_path11.default.resolve(spec.root, spec.source))) {
7438
+ if ((0, import_node_fs9.existsSync)(import_node_path12.default.resolve(spec.root, spec.source))) {
7061
7439
  state = "ok";
7062
7440
  reason = `placed from ${spec.source}; not generated`;
7063
7441
  } else {
@@ -7070,11 +7448,17 @@ async function buildPlan(specs, lock, opts = {}) {
7070
7448
  reason = "file on disk, no provenance recorded";
7071
7449
  } else {
7072
7450
  state = "missing";
7073
- reason = "not in lockfile";
7451
+ reason = spec.mirror ? `not made yet; flipped from ${spec.mirror.sourceAssetId} (no generation cost)` : "not in lockfile";
7074
7452
  }
7075
7453
  } else if (entry.specHash !== spec.specHash) {
7076
7454
  state = "stale";
7077
7455
  reason = "prompt, size, or style changed";
7456
+ } else if (spec.mirror && !mirrorCurrent(entry, lock.entries[lockKey(spec.styleId, spec.mirror.sourceAssetId)])) {
7457
+ state = "stale";
7458
+ reason = `mirror source ${spec.mirror.sourceAssetId} changed; flip it again (no generation cost)`;
7459
+ } else if (spec.mirror && entry.status === "downloaded" && entry.outputs.some((_output, index) => !(0, import_node_fs9.existsSync)(currentEntryOutputPath(entry, spec, index)))) {
7460
+ state = "stale";
7461
+ reason = "mirror output missing on disk; flip it again (no generation cost)";
7078
7462
  } else if (entry.status === "download-failed") {
7079
7463
  state = "recoverable";
7080
7464
  const force = entry.error?.includes("pass --force") ? " --force" : "";
@@ -7103,6 +7487,9 @@ async function buildPlan(specs, lock, opts = {}) {
7103
7487
  } else if (await anyOutputModified(entry, spec)) {
7104
7488
  state = "orphaned";
7105
7489
  reason = "output modified since download";
7490
+ } else if (spec.mirror && !postprocessCurrent(entry, spec)) {
7491
+ state = "stale";
7492
+ reason = "palette enforcement changed; flip the source again once it is refreshed (no generation cost)";
7106
7493
  } else if (!postprocessCurrent(entry, spec)) {
7107
7494
  state = "recoverable";
7108
7495
  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)";
@@ -7114,15 +7501,19 @@ async function buildPlan(specs, lock, opts = {}) {
7114
7501
  items.push({ spec, key, state, reason, ...quality ? { quality } : {} });
7115
7502
  }
7116
7503
  const isActionable = (i) => i.state === "missing" || i.state === "stale" || i.state === "failed";
7117
- const actionableKeys = new Set(items.filter(isActionable).map((i) => i.key));
7504
+ const stateOf = new Map(items.map((i) => [i.key, i.state]));
7118
7505
  for (const item of items) {
7119
7506
  if (!isActionable(item)) continue;
7120
- const parent = item.spec.revision?.sourceSpec ?? item.spec.character?.parentSpec;
7507
+ const parent = item.spec.mirror?.sourceSpec ?? item.spec.revision?.sourceSpec ?? item.spec.character?.parentSpec;
7121
7508
  if (!parent) continue;
7122
7509
  const parentKey = lockKey(parent.styleId, parent.assetId);
7123
- if (actionableKeys.has(parentKey)) {
7510
+ const parentState = stateOf.get(parentKey);
7511
+ if (parentState && isActionable({ state: parentState })) {
7124
7512
  item.state = "blocked";
7125
7513
  item.reason = `parent ${parentKey} is being generated in this run; this follows in the next wave`;
7514
+ } else if (item.spec.mirror && (parentState === "recoverable" || parentState === "in-flight")) {
7515
+ item.state = "blocked";
7516
+ item.reason = `mirror source ${parentKey} is ${parentState}; this follows in the next wave`;
7126
7517
  }
7127
7518
  }
7128
7519
  const actionable = items.filter(isActionable);
@@ -7180,13 +7571,13 @@ function summarize(plan) {
7180
7571
 
7181
7572
  // src/project.ts
7182
7573
  async function openProject(manifestPath = "pixelkiln.manifest.json", opts = {}) {
7183
- const absolute = import_node_path12.default.resolve(manifestPath);
7184
- const root = import_node_path12.default.dirname(absolute);
7574
+ const absolute = import_node_path13.default.resolve(manifestPath);
7575
+ const root = import_node_path13.default.dirname(absolute);
7185
7576
  if (opts.env !== false) {
7186
7577
  loadEnvFiles(root);
7187
- if (import_node_path12.default.resolve(process.cwd()) !== root) loadEnvFiles(process.cwd());
7578
+ if (import_node_path13.default.resolve(process.cwd()) !== root) loadEnvFiles(process.cwd());
7188
7579
  }
7189
- const lockPath = import_node_path12.default.resolve(opts.lockPath ?? import_node_path12.default.join(root, "pixelkiln.lock.json"));
7580
+ const lockPath = import_node_path13.default.resolve(opts.lockPath ?? import_node_path13.default.join(root, "pixelkiln.lock.json"));
7190
7581
  const loaded = await loadManifest(absolute);
7191
7582
  const specs = await resolveSpecs(loaded, { styles: opts.styles ?? [], assets: opts.assets ?? [] });
7192
7583
  const lock = await loadLock(lockPath);
@@ -7253,9 +7644,9 @@ function shouldPersistSourceUrl(value) {
7253
7644
  }
7254
7645
 
7255
7646
  // src/recipes.ts
7256
- var import_node_path13 = __toESM(require("path"), 1);
7647
+ var import_node_path14 = __toESM(require("path"), 1);
7257
7648
  var import_node_fs10 = require("fs");
7258
- var import_promises6 = require("fs/promises");
7649
+ var import_promises7 = require("fs/promises");
7259
7650
  var import_node_url = require("url");
7260
7651
  var import_zod3 = require("zod");
7261
7652
  var SHA256_RE = /^[0-9a-f]{64}$/;
@@ -7264,7 +7655,7 @@ var VERSION_RE = /^\d+\.\d+\.\d+$/;
7264
7655
  var RECIPE_FILE = "pixelkiln.recipe.json";
7265
7656
  var RECIPE_DIR_TOKEN = "{{recipeDir}}";
7266
7657
  var PortablePathSchema = import_zod3.z.string().min(1).refine((value) => {
7267
- if (value.includes("\\") || import_node_path13.default.posix.isAbsolute(value)) return false;
7658
+ if (value.includes("\\") || import_node_path14.default.posix.isAbsolute(value)) return false;
7268
7659
  const parts = value.split("/");
7269
7660
  return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
7270
7661
  }, "expected a portable relative path without '.', '..', or backslashes");
@@ -7440,10 +7831,10 @@ function message3(error) {
7440
7831
  return error instanceof Error ? error.message : String(error);
7441
7832
  }
7442
7833
  async function readRecipeFile(recipePath, bundled) {
7443
- const absolute = import_node_path13.default.resolve(recipePath);
7834
+ const absolute = import_node_path14.default.resolve(recipePath);
7444
7835
  let raw;
7445
7836
  try {
7446
- raw = JSON.parse(await (0, import_promises6.readFile)(absolute, "utf8"));
7837
+ raw = JSON.parse(await (0, import_promises7.readFile)(absolute, "utf8"));
7447
7838
  } catch (error) {
7448
7839
  throw new Error(`Could not read recipe ${absolute}: ${message3(error)}`, { cause: error });
7449
7840
  }
@@ -7454,13 +7845,13 @@ async function readRecipeFile(recipePath, bundled) {
7454
7845
  ` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
7455
7846
  );
7456
7847
  }
7457
- return { recipe: parsed.data, authored: raw, path: absolute, dir: import_node_path13.default.dirname(absolute), bundled };
7848
+ return { recipe: parsed.data, authored: raw, path: absolute, dir: import_node_path14.default.dirname(absolute), bundled };
7458
7849
  }
7459
7850
  async function walkRecipeFiles(root) {
7460
7851
  if (!(0, import_node_fs10.existsSync)(root)) return [];
7461
7852
  const out = [];
7462
- for (const entry of await (0, import_promises6.readdir)(root, { withFileTypes: true })) {
7463
- const resolved = import_node_path13.default.join(root, entry.name);
7853
+ for (const entry of await (0, import_promises7.readdir)(root, { withFileTypes: true })) {
7854
+ const resolved = import_node_path14.default.join(root, entry.name);
7464
7855
  if (entry.isDirectory()) out.push(...await walkRecipeFiles(resolved));
7465
7856
  else if (entry.isFile() && entry.name === RECIPE_FILE) out.push(resolved);
7466
7857
  }
@@ -7471,7 +7862,7 @@ function bundledRecipeRoot() {
7471
7862
  }
7472
7863
  async function listBundledRecipes(root = bundledRecipeRoot()) {
7473
7864
  const recipes = await Promise.all(
7474
- (await walkRecipeFiles(import_node_path13.default.resolve(root))).map((file) => readRecipeFile(file, true))
7865
+ (await walkRecipeFiles(import_node_path14.default.resolve(root))).map((file) => readRecipeFile(file, true))
7475
7866
  );
7476
7867
  return recipes.sort(
7477
7868
  (a, b) => a.recipe.id.localeCompare(b.recipe.id) || compareVersions(b.recipe.version, a.recipe.version)
@@ -7494,9 +7885,9 @@ function parseSelector(selector) {
7494
7885
  return { id: match[1], version: match[2] };
7495
7886
  }
7496
7887
  async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
7497
- const local = import_node_path13.default.resolve(target);
7888
+ const local = import_node_path14.default.resolve(target);
7498
7889
  if ((0, import_node_fs10.existsSync)(local)) {
7499
- const recipePath = import_node_path13.default.basename(local) === RECIPE_FILE ? local : import_node_path13.default.join(local, RECIPE_FILE);
7890
+ const recipePath = import_node_path14.default.basename(local) === RECIPE_FILE ? local : import_node_path14.default.join(local, RECIPE_FILE);
7500
7891
  return readRecipeFile(recipePath, false);
7501
7892
  }
7502
7893
  if (target.startsWith(".") || target.startsWith("/") || target.includes("\\")) {
@@ -7510,7 +7901,7 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
7510
7901
  return matches[0];
7511
7902
  }
7512
7903
  async function verifyFile(root, file) {
7513
- const absolute = import_node_path13.default.resolve(root, ...file.path.split("/"));
7904
+ const absolute = import_node_path14.default.resolve(root, ...file.path.split("/"));
7514
7905
  if (!(0, import_node_fs10.existsSync)(absolute)) {
7515
7906
  return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
7516
7907
  }
@@ -7526,7 +7917,7 @@ async function verifyRecipe(target, options = {}) {
7526
7917
  const loaded = await resolveRecipe(target, options.bundledRoot);
7527
7918
  const actualIntegrity = recipeDigest(loaded.authored);
7528
7919
  const files = await Promise.all(loaded.recipe.files.map((file) => verifyFile(loaded.dir, file)));
7529
- const modelRoot = options.modelRoot ? import_node_path13.default.resolve(options.modelRoot) : null;
7920
+ const modelRoot = options.modelRoot ? import_node_path14.default.resolve(options.modelRoot) : null;
7530
7921
  const models = modelRoot ? await Promise.all(loaded.recipe.models.map(async (model) => ({
7531
7922
  ...await verifyFile(modelRoot, model),
7532
7923
  source: model.source,
@@ -7560,7 +7951,7 @@ async function verifyRecipe(target, options = {}) {
7560
7951
  };
7561
7952
  }
7562
7953
  function renderStyle(style, destination, cwd) {
7563
- const portableDir = import_node_path13.default.relative(cwd, destination).split(import_node_path13.default.sep).join("/") || ".";
7954
+ const portableDir = import_node_path14.default.relative(cwd, destination).split(import_node_path14.default.sep).join("/") || ".";
7564
7955
  const replace = (value) => {
7565
7956
  if (typeof value === "string") return value.replaceAll(RECIPE_DIR_TOKEN, portableDir);
7566
7957
  if (Array.isArray(value)) return value.map(replace);
@@ -7572,25 +7963,25 @@ function renderStyle(style, destination, cwd) {
7572
7963
  return replace(style);
7573
7964
  }
7574
7965
  async function installRecipe(target, options = {}) {
7575
- const cwd = import_node_path13.default.resolve(options.cwd ?? process.cwd());
7966
+ const cwd = import_node_path14.default.resolve(options.cwd ?? process.cwd());
7576
7967
  const loaded = await resolveRecipe(target, options.bundledRoot);
7577
7968
  const verification = await verifyRecipe(loaded.path);
7578
7969
  if (!verification.ok) {
7579
7970
  throw new Error(`Recipe ${loaded.recipe.id}@${loaded.recipe.version} failed integrity verification.`);
7580
7971
  }
7581
- const destination = options.out ? import_node_path13.default.resolve(cwd, options.out) : import_node_path13.default.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
7972
+ const destination = options.out ? import_node_path14.default.resolve(cwd, options.out) : import_node_path14.default.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
7582
7973
  const sources = [
7583
7974
  { path: RECIPE_FILE, source: loaded.path },
7584
- ...loaded.recipe.files.map((file) => ({ path: file.path, source: import_node_path13.default.join(loaded.dir, ...file.path.split("/")) }))
7975
+ ...loaded.recipe.files.map((file) => ({ path: file.path, source: import_node_path14.default.join(loaded.dir, ...file.path.split("/")) }))
7585
7976
  ];
7586
7977
  const files = await Promise.all(sources.map(async (file) => ({
7587
- path: import_node_path13.default.join(destination, ...file.path.split("/")),
7588
- data: await (0, import_promises6.readFile)(file.source)
7978
+ path: import_node_path14.default.join(destination, ...file.path.split("/")),
7979
+ data: await (0, import_promises7.readFile)(file.source)
7589
7980
  })));
7590
7981
  if (!options.force) {
7591
7982
  for (const file of files) {
7592
7983
  if (!(0, import_node_fs10.existsSync)(file.path)) continue;
7593
- const current = await (0, import_promises6.readFile)(file.path);
7984
+ const current = await (0, import_promises7.readFile)(file.path);
7594
7985
  if (!current.equals(file.data)) {
7595
7986
  throw new OverwriteRefusedError(
7596
7987
  `Recipe destination has local changes: ${file.path}. Choose another --out or pass --force to replace declared recipe files.`,
@@ -7614,9 +8005,9 @@ async function installRecipe(target, options = {}) {
7614
8005
  }
7615
8006
 
7616
8007
  // src/pipeline/audit.ts
7617
- var import_promises7 = require("fs/promises");
8008
+ var import_promises8 = require("fs/promises");
7618
8009
  var import_node_fs11 = require("fs");
7619
- var import_node_path14 = __toESM(require("path"), 1);
8010
+ var import_node_path15 = __toESM(require("path"), 1);
7620
8011
  function paletteDistance(asset, reference) {
7621
8012
  if (!asset.length || !reference.length) return 0;
7622
8013
  let total = 0;
@@ -7657,7 +8048,7 @@ async function auditStyle(loaded, specs, styleId, lock) {
7657
8048
  continue;
7658
8049
  }
7659
8050
  try {
7660
- const png = decodePng(await (0, import_promises7.readFile)(output.absolutePath));
8051
+ const png = decodePng(await (0, import_promises8.readFile)(output.absolutePath));
7661
8052
  const palette = extractPalette(png, 12);
7662
8053
  assets.push({
7663
8054
  assetId: spec.assetId,
@@ -7681,10 +8072,10 @@ async function auditStyle(loaded, specs, styleId, lock) {
7681
8072
  let referenceFromStyleImages = false;
7682
8073
  const refPalettes = [];
7683
8074
  for (const img of style.styleImages) {
7684
- const abs = import_node_path14.default.resolve(loaded.root, img.path);
8075
+ const abs = import_node_path15.default.resolve(loaded.root, img.path);
7685
8076
  if (!(0, import_node_fs11.existsSync)(abs)) continue;
7686
8077
  try {
7687
- refPalettes.push(extractPalette(decodePng(await (0, import_promises7.readFile)(abs)), 12));
8078
+ refPalettes.push(extractPalette(decodePng(await (0, import_promises8.readFile)(abs)), 12));
7688
8079
  } catch {
7689
8080
  }
7690
8081
  }
@@ -7760,15 +8151,15 @@ function hex(c) {
7760
8151
  }
7761
8152
 
7762
8153
  // src/pipeline/quality-regression.ts
7763
- var import_node_path15 = __toESM(require("path"), 1);
8154
+ var import_node_path16 = __toESM(require("path"), 1);
7764
8155
  var import_node_fs12 = require("fs");
7765
- var import_promises8 = require("fs/promises");
8156
+ var import_promises9 = require("fs/promises");
7766
8157
  var import_zod4 = require("zod");
7767
8158
  var SHA256_RE2 = /^[0-9a-f]{64}$/;
7768
8159
  var HEX_RE = /^#[0-9a-f]{6}$/;
7769
8160
  var MAX_REDMEAN_DISTANCE = 765;
7770
8161
  var QualityPathSchema = import_zod4.z.string().min(1).refine(
7771
- (value) => !import_node_path15.default.posix.isAbsolute(value) && !value.includes("\\"),
8162
+ (value) => !import_node_path16.default.posix.isAbsolute(value) && !value.includes("\\"),
7772
8163
  "expected a portable relative path"
7773
8164
  );
7774
8165
  var DEFAULT_QUALITY_TOLERANCES = {
@@ -7915,10 +8306,10 @@ function measureDecodedImageQuality(png, digest2) {
7915
8306
  };
7916
8307
  }
7917
8308
  async function measureImageQuality(file) {
7918
- const absolute = import_node_path15.default.resolve(file);
8309
+ const absolute = import_node_path16.default.resolve(file);
7919
8310
  let bytes;
7920
8311
  try {
7921
- bytes = await (0, import_promises8.readFile)(absolute);
8312
+ bytes = await (0, import_promises9.readFile)(absolute);
7922
8313
  } catch (error) {
7923
8314
  throw new Error(`Cannot read quality image ${absolute}: ${message4(error)}`, { cause: error });
7924
8315
  }
@@ -7937,11 +8328,11 @@ function resolveQualityInputs(raw, inputsFilePath) {
7937
8328
  }
7938
8329
  const ids = parsed.data.map((entry) => entry.id);
7939
8330
  if (new Set(ids).size !== ids.length) throw new Error("--inputs contains a duplicate quality case id.");
7940
- const root = import_node_path15.default.dirname(import_node_path15.default.resolve(inputsFilePath));
8331
+ const root = import_node_path16.default.dirname(import_node_path16.default.resolve(inputsFilePath));
7941
8332
  return parsed.data.map((entry) => ({
7942
8333
  id: entry.id,
7943
- file: import_node_path15.default.resolve(root, entry.path),
7944
- ...entry.record ? { record: import_node_path15.default.resolve(root, entry.record) } : {},
8334
+ file: import_node_path16.default.resolve(root, entry.path),
8335
+ ...entry.record ? { record: import_node_path16.default.resolve(root, entry.record) } : {},
7945
8336
  tolerances: QualityToleranceSchema.parse({
7946
8337
  ...DEFAULT_QUALITY_TOLERANCES,
7947
8338
  ...entry.tolerances
@@ -7949,7 +8340,7 @@ function resolveQualityInputs(raw, inputsFilePath) {
7949
8340
  }));
7950
8341
  }
7951
8342
  function portableRelative2(from, to) {
7952
- return import_node_path15.default.relative(from, import_node_path15.default.resolve(to)).split(import_node_path15.default.sep).join("/") || ".";
8343
+ return import_node_path16.default.relative(from, import_node_path16.default.resolve(to)).split(import_node_path16.default.sep).join("/") || ".";
7953
8344
  }
7954
8345
  async function verifyRecordForImage(recordPath, imagePath) {
7955
8346
  const verification = await verifyArtifactBundle(recordPath);
@@ -7964,20 +8355,20 @@ async function verifyRecordForImage(recordPath, imagePath) {
7964
8355
  if (record.kind !== "refine" || options === null || typeof options !== "object" || options.schema !== "pixelkiln-quality") {
7965
8356
  throw new Error(`Quality record ${recordPath} is not a PixelKiln refinement record.`);
7966
8357
  }
7967
- const outputs = record.outputs.map((output) => import_node_path15.default.resolve(import_node_path15.default.dirname(recordPath), output.path));
7968
- if (!outputs.includes(import_node_path15.default.resolve(imagePath))) {
8358
+ const outputs = record.outputs.map((output) => import_node_path16.default.resolve(import_node_path16.default.dirname(recordPath), output.path));
8359
+ if (!outputs.includes(import_node_path16.default.resolve(imagePath))) {
7969
8360
  throw new Error(`Quality record ${recordPath} does not own ${imagePath}.`);
7970
8361
  }
7971
8362
  }
7972
8363
  async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
7973
8364
  if (!inputs.length) throw new Error("Cannot snapshot an empty quality baseline.");
7974
- const absolute = import_node_path15.default.resolve(baselinePath);
8365
+ const absolute = import_node_path16.default.resolve(baselinePath);
7975
8366
  for (const input of inputs) {
7976
- if (import_node_path15.default.resolve(input.file) === absolute || input.record && import_node_path15.default.resolve(input.record) === absolute) {
8367
+ if (import_node_path16.default.resolve(input.file) === absolute || input.record && import_node_path16.default.resolve(input.record) === absolute) {
7977
8368
  throw new Error(`Quality baseline output would overwrite an input: ${absolute}.`);
7978
8369
  }
7979
8370
  }
7980
- const root = import_node_path15.default.dirname(absolute);
8371
+ const root = import_node_path16.default.dirname(absolute);
7981
8372
  const cases = [];
7982
8373
  for (const input of [...inputs].sort((a, b) => a.id.localeCompare(b.id))) {
7983
8374
  const expected = await measureImageQuality(input.file);
@@ -8005,7 +8396,7 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
8005
8396
  });
8006
8397
  const data = Buffer.from(JSON.stringify(baseline, null, 2) + "\n");
8007
8398
  if ((0, import_node_fs12.existsSync)(absolute) && !options.force) {
8008
- const current = await (0, import_promises8.readFile)(absolute);
8399
+ const current = await (0, import_promises9.readFile)(absolute);
8009
8400
  if (!current.equals(data)) {
8010
8401
  throw new OverwriteRefusedError(
8011
8402
  `Quality baseline already exists with different content: ${absolute}. Pass --force to replace it.`,
@@ -8017,10 +8408,10 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
8017
8408
  return { baseline, path: absolute, changed: result.changed.length > 0 };
8018
8409
  }
8019
8410
  async function readQualityBaseline(baselinePath) {
8020
- const absolute = import_node_path15.default.resolve(baselinePath);
8411
+ const absolute = import_node_path16.default.resolve(baselinePath);
8021
8412
  let raw;
8022
8413
  try {
8023
- raw = JSON.parse(await (0, import_promises8.readFile)(absolute, "utf8"));
8414
+ raw = JSON.parse(await (0, import_promises9.readFile)(absolute, "utf8"));
8024
8415
  } catch (error) {
8025
8416
  throw new Error(`Could not read quality baseline ${absolute}: ${message4(error)}`, { cause: error });
8026
8417
  }
@@ -8090,12 +8481,12 @@ function regressionViolations(expected, actual, tolerances) {
8090
8481
  return { violations, warnings };
8091
8482
  }
8092
8483
  async function checkQualityBaseline(baselinePath) {
8093
- const absolute = import_node_path15.default.resolve(baselinePath);
8484
+ const absolute = import_node_path16.default.resolve(baselinePath);
8094
8485
  const baseline = await readQualityBaseline(absolute);
8095
- const root = import_node_path15.default.dirname(absolute);
8486
+ const root = import_node_path16.default.dirname(absolute);
8096
8487
  const cases = [];
8097
8488
  for (const entry of baseline.cases) {
8098
- const file = import_node_path15.default.resolve(root, entry.file);
8489
+ const file = import_node_path16.default.resolve(root, entry.file);
8099
8490
  let actual = null;
8100
8491
  const violations = [];
8101
8492
  const warnings = [];
@@ -8108,7 +8499,7 @@ async function checkQualityBaseline(baselinePath) {
8108
8499
  violations.push(message4(error));
8109
8500
  }
8110
8501
  if (entry.record) {
8111
- const recordPath = import_node_path15.default.resolve(root, entry.record.path);
8502
+ const recordPath = import_node_path16.default.resolve(root, entry.record.path);
8112
8503
  try {
8113
8504
  const recordHash = await sha256File(recordPath);
8114
8505
  if (recordHash !== entry.record.sha256) violations.push("quality record hash changed");
@@ -8145,13 +8536,13 @@ async function checkQualityBaseline(baselinePath) {
8145
8536
 
8146
8537
  // src/pipeline/cache-health.ts
8147
8538
  var import_node_fs16 = require("fs");
8148
- var import_promises11 = require("fs/promises");
8149
- var import_node_path19 = __toESM(require("path"), 1);
8539
+ var import_promises12 = require("fs/promises");
8540
+ var import_node_path20 = __toESM(require("path"), 1);
8150
8541
 
8151
8542
  // src/cache.ts
8152
- var import_promises9 = require("fs/promises");
8543
+ var import_promises10 = require("fs/promises");
8153
8544
  var import_node_fs13 = require("fs");
8154
- var import_node_path16 = __toESM(require("path"), 1);
8545
+ var import_node_path17 = __toESM(require("path"), 1);
8155
8546
  var import_zod5 = require("zod");
8156
8547
  var HashCacheSchema = import_zod5.z.object({
8157
8548
  version: import_zod5.z.literal(1),
@@ -8165,10 +8556,10 @@ function isSha256Hash(value) {
8165
8556
  function parseCache(value) {
8166
8557
  return HashCacheSchema.parse(value);
8167
8558
  }
8168
- async function loadCache(path32) {
8169
- if (!(0, import_node_fs13.existsSync)(path32)) return { version: 1, hashes: {} };
8559
+ async function loadCache(path33) {
8560
+ if (!(0, import_node_fs13.existsSync)(path33)) return { version: 1, hashes: {} };
8170
8561
  try {
8171
- const parsed = HashCacheSchema.safeParse(JSON.parse(await (0, import_promises9.readFile)(path32, "utf8")));
8562
+ const parsed = HashCacheSchema.safeParse(JSON.parse(await (0, import_promises10.readFile)(path33, "utf8")));
8172
8563
  if (!parsed.success) return { version: 1, hashes: {} };
8173
8564
  return {
8174
8565
  version: 1,
@@ -8180,20 +8571,20 @@ async function loadCache(path32) {
8180
8571
  return { version: 1, hashes: {} };
8181
8572
  }
8182
8573
  }
8183
- async function saveCache(path32, cache) {
8574
+ async function saveCache(path33, cache) {
8184
8575
  const sorted = {};
8185
8576
  for (const key of Object.keys(cache.hashes).sort()) {
8186
8577
  const hash = cache.hashes[key];
8187
8578
  if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
8188
8579
  sorted[key] = hash;
8189
8580
  }
8190
- await (0, import_promises9.mkdir)(import_node_path16.default.dirname(import_node_path16.default.resolve(path32)), { recursive: true });
8191
- const tmp = `${path32}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
8581
+ await (0, import_promises10.mkdir)(import_node_path17.default.dirname(import_node_path17.default.resolve(path33)), { recursive: true });
8582
+ const tmp = `${path33}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
8192
8583
  try {
8193
- await (0, import_promises9.writeFile)(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
8194
- await (0, import_promises9.rename)(tmp, path32);
8584
+ await (0, import_promises10.writeFile)(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
8585
+ await (0, import_promises10.rename)(tmp, path33);
8195
8586
  } finally {
8196
- await (0, import_promises9.rm)(tmp, { force: true });
8587
+ await (0, import_promises10.rm)(tmp, { force: true });
8197
8588
  }
8198
8589
  }
8199
8590
  function pruneCache(cache, liveIds, namespace = "") {
@@ -8216,21 +8607,22 @@ function cachePathFor(lockPath) {
8216
8607
 
8217
8608
  // src/pipeline/history.ts
8218
8609
  var import_node_fs15 = require("fs");
8219
- var import_node_path18 = __toESM(require("path"), 1);
8610
+ var import_node_path19 = __toESM(require("path"), 1);
8220
8611
 
8221
8612
  // src/pipeline/fetch.ts
8222
8613
  var import_node_fs14 = require("fs");
8223
- var import_promises10 = require("fs/promises");
8224
- var import_node_path17 = __toESM(require("path"), 1);
8614
+ var import_promises11 = require("fs/promises");
8615
+ var import_node_path18 = __toESM(require("path"), 1);
8225
8616
  async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
8226
8617
  const log = opts.onProgress ?? (() => {
8227
8618
  });
8228
8619
  const result = { downloaded: 0, skipped: 0, failed: 0, ...opts.refresh ? { unchanged: 0 } : {} };
8229
8620
  const specByKey = new Map(specs.map((s) => [lockKey(s.styleId, s.assetId), s]));
8230
8621
  normalizeLockOutputPaths(lock, specs);
8231
- const cacheDir = opts.cacheDir === false ? null : import_node_path17.default.resolve(opts.cacheDir ?? import_node_path17.default.join(import_node_path17.default.dirname(lockPath), ".pixelkiln", "cache"));
8622
+ const cacheDir = opts.cacheDir === false ? null : import_node_path18.default.resolve(opts.cacheDir ?? import_node_path18.default.join(import_node_path18.default.dirname(lockPath), ".pixelkiln", "cache"));
8232
8623
  const pending = Object.entries(lock.entries).filter(([key, e]) => {
8233
8624
  if (e.provider !== provider.id || !specByKey.has(key)) return false;
8625
+ if (specByKey.get(key)?.mirror) return false;
8234
8626
  if (e.status === "selected" || e.status === "download-failed") return true;
8235
8627
  if (e.status !== "downloaded") return false;
8236
8628
  if (opts.refresh) return e.outputs.length > 0 && Boolean(e.sourceUrls?.length || e.sourceUrl);
@@ -8329,7 +8721,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
8329
8721
  if (!source.url) throw new Error(`no durable source URL to refresh ${source.role ?? "asset"} from`);
8330
8722
  fresh = await provider.download(source.url);
8331
8723
  if (recorded && sha256(fresh) === recordedRaw && !reapply) {
8332
- log(` current ${import_node_path17.default.relative(process.cwd(), target)}`);
8724
+ log(` current ${import_node_path18.default.relative(process.cwd(), target)}`);
8333
8725
  outputs.push({ ...recorded, path: portableOutputPath(target, spec.root) });
8334
8726
  continue;
8335
8727
  }
@@ -8343,7 +8735,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
8343
8735
  if (cacheDir) {
8344
8736
  await cacheMedia(
8345
8737
  cacheDir,
8346
- await (0, import_promises10.readFile)(target),
8738
+ await (0, import_promises11.readFile)(target),
8347
8739
  recorded.mediaType ?? MediaType.PNG,
8348
8740
  recorded.sha256
8349
8741
  );
@@ -8352,10 +8744,10 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
8352
8744
  continue;
8353
8745
  }
8354
8746
  if (reapply && !fresh) {
8355
- if (!recorded.raw) raw = await (0, import_promises10.readFile)(target);
8356
- log(` reapply ${import_node_path17.default.relative(process.cwd(), target)}`);
8747
+ if (!recorded.raw) raw = await (0, import_promises11.readFile)(target);
8748
+ log(` reapply ${import_node_path18.default.relative(process.cwd(), target)}`);
8357
8749
  } else {
8358
- log(` changed ${import_node_path17.default.relative(process.cwd(), target)} (upstream)`);
8750
+ log(` changed ${import_node_path18.default.relative(process.cwd(), target)} (upstream)`);
8359
8751
  }
8360
8752
  } else {
8361
8753
  const superseded = (entry.supersededOutputs ?? []).find(
@@ -8374,7 +8766,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
8374
8766
  }
8375
8767
  }
8376
8768
  log(
8377
- ` replace ${import_node_path17.default.relative(process.cwd(), target)}` + (opts.force ? " (--force)" : " (previous tracked generation)")
8769
+ ` replace ${import_node_path18.default.relative(process.cwd(), target)}` + (opts.force ? " (--force)" : " (previous tracked generation)")
8378
8770
  );
8379
8771
  }
8380
8772
  }
@@ -8383,7 +8775,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
8383
8775
  } else if (!raw && recorded && cacheDir) {
8384
8776
  if (!reapply) final = await readCachedMedia(cacheDir, recorded.sha256, expectedMediaType);
8385
8777
  if (!final && recordedRaw) raw = await readCachedMedia(cacheDir, recordedRaw, expectedMediaType);
8386
- if (final || raw) log(` cached ${import_node_path17.default.relative(process.cwd(), target)}`);
8778
+ if (final || raw) log(` cached ${import_node_path18.default.relative(process.cwd(), target)}`);
8387
8779
  }
8388
8780
  if (!final && !raw) {
8389
8781
  if (!source.url) {
@@ -8410,13 +8802,13 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
8410
8802
  buf = processed.bytes;
8411
8803
  if (processed.changed) {
8412
8804
  rawHash = sha256(raw);
8413
- log(` snapped ${import_node_path17.default.relative(process.cwd(), target)} to ${wanted.palette.colors.length} colours`);
8805
+ log(` snapped ${import_node_path18.default.relative(process.cwd(), target)} to ${wanted.palette.colors.length} colours`);
8414
8806
  }
8415
8807
  }
8416
8808
  if (cacheDir) await cacheMedia(cacheDir, buf, mediaType);
8417
- await (0, import_promises10.mkdir)(import_node_path17.default.dirname(target), { recursive: true });
8809
+ await (0, import_promises11.mkdir)(import_node_path18.default.dirname(target), { recursive: true });
8418
8810
  const tmp = `${target}.pixelkiln.tmp`;
8419
- await (0, import_promises10.writeFile)(tmp, buf);
8811
+ await (0, import_promises11.writeFile)(tmp, buf);
8420
8812
  outputs.push({
8421
8813
  path: portableOutputPath(target, spec.root),
8422
8814
  sha256: sha256(buf),
@@ -8426,9 +8818,9 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
8426
8818
  });
8427
8819
  upsert(lock, key, { outputs: mergeOutputs(entry.outputs, outputs) });
8428
8820
  await saveLock(lockPath, lock);
8429
- await (0, import_promises10.rename)(tmp, target);
8821
+ await (0, import_promises11.rename)(tmp, target);
8430
8822
  wrote = true;
8431
- log(` wrote ${import_node_path17.default.relative(process.cwd(), target)}`);
8823
+ log(` wrote ${import_node_path18.default.relative(process.cwd(), target)}`);
8432
8824
  }
8433
8825
  if (opts.refresh && !wrote) {
8434
8826
  if (rotated) {
@@ -8472,10 +8864,10 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
8472
8864
  return result;
8473
8865
  }
8474
8866
  async function readCachedMedia(cacheDir, hash, mediaType) {
8475
- const file = import_node_path17.default.join(cacheDir, cacheFileName(hash, mediaType));
8867
+ const file = import_node_path18.default.join(cacheDir, cacheFileName(hash, mediaType));
8476
8868
  if (!(0, import_node_fs14.existsSync)(file)) return null;
8477
8869
  try {
8478
- const buf = await (0, import_promises10.readFile)(file);
8870
+ const buf = await (0, import_promises11.readFile)(file);
8479
8871
  if (sha256(buf) !== hash) return null;
8480
8872
  validateMedia(buf, mediaType);
8481
8873
  return buf;
@@ -8486,17 +8878,17 @@ async function readCachedMedia(cacheDir, hash, mediaType) {
8486
8878
  async function cacheMedia(cacheDir, buf, mediaType, knownHash) {
8487
8879
  const hash = knownHash ?? sha256(buf);
8488
8880
  validateMedia(buf, mediaType);
8489
- const file = import_node_path17.default.join(cacheDir, cacheFileName(hash, mediaType));
8881
+ const file = import_node_path18.default.join(cacheDir, cacheFileName(hash, mediaType));
8490
8882
  if (await readCachedMedia(cacheDir, hash, mediaType)) return;
8491
- await (0, import_promises10.mkdir)(cacheDir, { recursive: true });
8883
+ await (0, import_promises11.mkdir)(cacheDir, { recursive: true });
8492
8884
  const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
8493
- await (0, import_promises10.writeFile)(tmp, buf);
8885
+ await (0, import_promises11.writeFile)(tmp, buf);
8494
8886
  try {
8495
- await (0, import_promises10.rename)(tmp, file);
8887
+ await (0, import_promises11.rename)(tmp, file);
8496
8888
  } catch (err) {
8497
8889
  if (!(0, import_node_fs14.existsSync)(file)) throw err;
8498
8890
  } finally {
8499
- await (0, import_promises10.rm)(tmp, { force: true });
8891
+ await (0, import_promises11.rm)(tmp, { force: true });
8500
8892
  }
8501
8893
  }
8502
8894
  function mergeOutputs(previous, current) {
@@ -8615,7 +9007,7 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
8615
9007
  if (!(0, import_node_fs15.existsSync)(file)) continue;
8616
9008
  if (await sha256File(file) !== entry.outputs[i].sha256 && !opts.force) {
8617
9009
  throw new OverwriteRefusedError(
8618
- `${import_node_path18.default.relative(process.cwd(), file)} was changed after download; keep it with pixelkiln edit, or pass --force to replace it`,
9010
+ `${import_node_path19.default.relative(process.cwd(), file)} was changed after download; keep it with pixelkiln edit, or pass --force to replace it`,
8619
9011
  [file]
8620
9012
  );
8621
9013
  }
@@ -8667,10 +9059,10 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
8667
9059
  // src/pipeline/cache-health.ts
8668
9060
  async function inspectCaches(lock, lockPath, options = {}) {
8669
9061
  if (options.prune && !(0, import_node_fs16.existsSync)(lockPath)) {
8670
- throw new Error(`Refusing to prune without an existing lockfile at ${import_node_path19.default.resolve(lockPath)}`);
9062
+ throw new Error(`Refusing to prune without an existing lockfile at ${import_node_path20.default.resolve(lockPath)}`);
8671
9063
  }
8672
- const contentDir = import_node_path19.default.resolve(import_node_path19.default.dirname(lockPath), ".pixelkiln", "cache");
8673
- const remotePath = import_node_path19.default.resolve(cachePathFor(lockPath));
9064
+ const contentDir = import_node_path20.default.resolve(import_node_path20.default.dirname(lockPath), ".pixelkiln", "cache");
9065
+ const remotePath = import_node_path20.default.resolve(cachePathFor(lockPath));
8674
9066
  const referenced = referencedHashes(lock);
8675
9067
  let content = await inspectContentCache(contentDir, referenced);
8676
9068
  let remoteHashes = await inspectRemoteHashCache(remotePath);
@@ -8682,7 +9074,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
8682
9074
  ]);
8683
9075
  for (const name of names) {
8684
9076
  try {
8685
- await (0, import_promises11.rm)(import_node_path19.default.join(contentDir, name), { force: true });
9077
+ await (0, import_promises12.rm)(import_node_path20.default.join(contentDir, name), { force: true });
8686
9078
  removed.contentFiles++;
8687
9079
  } catch {
8688
9080
  }
@@ -8691,7 +9083,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
8691
9083
  await saveCache(remotePath, { version: 1, hashes: {} });
8692
9084
  removed.resetRemoteHashCache = true;
8693
9085
  } else if (remoteHashes.invalidIds.length) {
8694
- const cache = parseCache(JSON.parse(await (0, import_promises11.readFile)(remotePath, "utf8")));
9086
+ const cache = parseCache(JSON.parse(await (0, import_promises12.readFile)(remotePath, "utf8")));
8695
9087
  for (const id of remoteHashes.invalidIds) delete cache.hashes[id];
8696
9088
  removed.remoteHashEntries = remoteHashes.invalidIds.length;
8697
9089
  await saveCache(remotePath, cache);
@@ -8723,19 +9115,19 @@ async function inspectContentCache(contentDir, referenced) {
8723
9115
  return report;
8724
9116
  }
8725
9117
  const present = /* @__PURE__ */ new Set();
8726
- const entries = await (0, import_promises11.readdir)(contentDir, { withFileTypes: true });
9118
+ const entries = await (0, import_promises12.readdir)(contentDir, { withFileTypes: true });
8727
9119
  for (const entry of entries) {
8728
9120
  if (!entry.isFile()) {
8729
9121
  report.invalid.push({ name: entry.name, reason: "not a regular file" });
8730
9122
  continue;
8731
9123
  }
8732
9124
  report.files++;
8733
- const file = import_node_path19.default.join(contentDir, entry.name);
9125
+ const file = import_node_path20.default.join(contentDir, entry.name);
8734
9126
  const mediaType = mediaTypeFromExtension(entry.name);
8735
9127
  const expected = mediaType ? entry.name.slice(0, -4) : "";
8736
9128
  let bytes;
8737
9129
  try {
8738
- bytes = await (0, import_promises11.readFile)(file);
9130
+ bytes = await (0, import_promises12.readFile)(file);
8739
9131
  report.bytes += bytes.length;
8740
9132
  } catch (err) {
8741
9133
  report.invalid.push({
@@ -8782,7 +9174,7 @@ async function inspectRemoteHashCache(remotePath) {
8782
9174
  if (!report.exists) return report;
8783
9175
  let cache;
8784
9176
  try {
8785
- cache = parseCache(JSON.parse(await (0, import_promises11.readFile)(remotePath, "utf8")));
9177
+ cache = parseCache(JSON.parse(await (0, import_promises12.readFile)(remotePath, "utf8")));
8786
9178
  } catch (err) {
8787
9179
  report.error = err instanceof Error ? err.message : String(err);
8788
9180
  return report;
@@ -8798,19 +9190,19 @@ async function inspectRemoteHashCache(remotePath) {
8798
9190
 
8799
9191
  // src/pipeline/pack.ts
8800
9192
  var import_node_fs17 = require("fs");
8801
- var import_node_path20 = __toESM(require("path"), 1);
9193
+ var import_node_path21 = __toESM(require("path"), 1);
8802
9194
  var DEFAULT_FRAME_SET_FPS = 12;
8803
9195
  function resolvePackInputs(raw, inputsFilePath) {
8804
9196
  if (!Array.isArray(raw) || !raw.length) {
8805
9197
  throw new Error("--inputs must be a non-empty JSON array of { id, path }");
8806
9198
  }
8807
- const baseDir = import_node_path20.default.dirname(import_node_path20.default.resolve(inputsFilePath));
9199
+ const baseDir = import_node_path21.default.dirname(import_node_path21.default.resolve(inputsFilePath));
8808
9200
  return raw.map((entry, i) => {
8809
9201
  const e = entry;
8810
9202
  if (typeof e.id !== "string" || typeof e.path !== "string") {
8811
9203
  throw new Error(`--inputs[${i}] needs string "id" and "path"`);
8812
9204
  }
8813
- return { id: e.id, path: import_node_path20.default.resolve(baseDir, e.path) };
9205
+ return { id: e.id, path: import_node_path21.default.resolve(baseDir, e.path) };
8814
9206
  });
8815
9207
  }
8816
9208
  function packSprites(inputs, options = {}) {
@@ -8882,7 +9274,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
8882
9274
  }
8883
9275
  const inputs2 = Object.entries(options.sourceOverrides).map(([id, source]) => ({
8884
9276
  id,
8885
- path: import_node_path20.default.resolve(source)
9277
+ path: import_node_path21.default.resolve(source)
8886
9278
  }));
8887
9279
  if (!inputs2.length) throw new Error(`No approved quality outputs for style "${styleId}".`);
8888
9280
  const packed2 = packSprites(inputs2, options);
@@ -8906,7 +9298,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
8906
9298
  let outputs = resolveEntryOutputs(entry, id, manifestDir);
8907
9299
  const source = sources[id];
8908
9300
  if (source && outputs.length <= 1) {
8909
- inputs.push({ id, path: import_node_path20.default.resolve(manifestDir, source) });
9301
+ inputs.push({ id, path: import_node_path21.default.resolve(manifestDir, source) });
8910
9302
  continue;
8911
9303
  }
8912
9304
  if (source && sourceIsStem(source, entry, manifestDir)) {
@@ -8940,7 +9332,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
8940
9332
  }
8941
9333
  }
8942
9334
  for (const [id, source] of Object.entries(sources)) {
8943
- if (!locked.has(id)) inputs.push({ id, path: import_node_path20.default.resolve(manifestDir, source) });
9335
+ if (!locked.has(id)) inputs.push({ id, path: import_node_path21.default.resolve(manifestDir, source) });
8944
9336
  }
8945
9337
  if (!inputs.length) {
8946
9338
  throw new Error(`No readable sprites for style "${styleId}"; ${noOutput.length} skipped.`);
@@ -9048,7 +9440,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
9048
9440
  const source = sources[id];
9049
9441
  const entry = lock.entries[`${styleId}/${id}`];
9050
9442
  if (source && !(entry && sourceIsStem(source, entry, manifestDir))) {
9051
- placements.push({ id, path: import_node_path20.default.resolve(manifestDir, source), cell });
9443
+ placements.push({ id, path: import_node_path21.default.resolve(manifestDir, source), cell });
9052
9444
  continue;
9053
9445
  }
9054
9446
  if (source && entry) {
@@ -9081,7 +9473,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
9081
9473
  `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}`
9082
9474
  );
9083
9475
  }
9084
- const basePng = mount.base ? (0, import_node_fs17.readFileSync)(import_node_path20.default.resolve(manifestDir, mount.base)) : void 0;
9476
+ const basePng = mount.base ? (0, import_node_fs17.readFileSync)(import_node_path21.default.resolve(manifestDir, mount.base)) : void 0;
9085
9477
  const mounted = mountSprites(placements, {
9086
9478
  cellWidth: mount.cellWidth,
9087
9479
  cellHeight: mount.cellHeight,
@@ -9091,7 +9483,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
9091
9483
  ...mounted,
9092
9484
  sources: mount.base ? [{
9093
9485
  id: "$base",
9094
- path: import_node_path20.default.resolve(manifestDir, mount.base),
9486
+ path: import_node_path21.default.resolve(manifestDir, mount.base),
9095
9487
  sha256: sha256(basePng),
9096
9488
  included: true
9097
9489
  }, ...mounted.sources] : mounted.sources,
@@ -9464,7 +9856,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
9464
9856
  });
9465
9857
  const estimates = new Map(items.map((item) => [
9466
9858
  item.key,
9467
- validateCostEstimate(provider.id, provider.estimate(item.spec))
9859
+ item.spec.mirror ? { amount: 0, unit: item.spec.costUnit, candidates: 1 } : validateCostEstimate(provider.id, provider.estimate(item.spec))
9468
9860
  ]));
9469
9861
  const units = new Set([...estimates.values()].map((estimate) => estimate.unit));
9470
9862
  if (units.size > 1) {
@@ -9514,6 +9906,34 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
9514
9906
  }
9515
9907
  for (const { spec, key } of items) {
9516
9908
  const estimate = estimates.get(key);
9909
+ if (spec.mirror) {
9910
+ try {
9911
+ await requireRevisionReady(spec, lock);
9912
+ await deriveMirror(spec, lock, lockPath, { onProgress: log });
9913
+ submitted++;
9914
+ } catch (err) {
9915
+ const message6 = err instanceof Error ? err.message : String(err);
9916
+ failed++;
9917
+ upsert(lock, key, {
9918
+ styleId: spec.styleId,
9919
+ assetId: spec.assetId,
9920
+ specHash: spec.specHash,
9921
+ generator: spec.generator,
9922
+ prompt: "",
9923
+ width: spec.width,
9924
+ height: spec.height,
9925
+ status: "failed",
9926
+ error: message6,
9927
+ outputs: lock.entries[key]?.outputs ?? [],
9928
+ cost: 0,
9929
+ costUnit: spec.costUnit,
9930
+ provider: provider.id
9931
+ });
9932
+ await saveLock(lockPath, lock);
9933
+ log(` FAILED ${key}: ${message6}`);
9934
+ }
9935
+ continue;
9936
+ }
9517
9937
  await waitForSlot();
9518
9938
  const since = Date.now() - lastSubmitAt;
9519
9939
  if (since < spacing) await sleep(spacing - since);
@@ -9724,8 +10144,8 @@ async function poll(provider, lock, lockPath, opts = {}) {
9724
10144
 
9725
10145
  // src/pipeline/doctor.ts
9726
10146
  var import_node_fs18 = require("fs");
9727
- var import_promises12 = require("fs/promises");
9728
- var import_node_path21 = __toESM(require("path"), 1);
10147
+ var import_promises13 = require("fs/promises");
10148
+ var import_node_path22 = __toESM(require("path"), 1);
9729
10149
  async function doctor(loaded, specs, lock, lockPath, opts = {}) {
9730
10150
  const checks = [];
9731
10151
  const add = (id, level, message6) => checks.push({ id, level, message: message6 });
@@ -9736,14 +10156,14 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
9736
10156
  );
9737
10157
  add("lock", "ok", `${Object.keys(lock.entries).length} valid v2 lock entr(ies)`);
9738
10158
  const dirs = [...new Set(specs.flatMap((spec) => [
9739
- import_node_path21.default.dirname(spec.outFile),
9740
- ...spec.quality ? [import_node_path21.default.dirname(spec.quality.outFile)] : []
10159
+ import_node_path22.default.dirname(spec.outFile),
10160
+ ...spec.quality ? [import_node_path22.default.dirname(spec.quality.outFile)] : []
9741
10161
  ]))];
9742
10162
  const unwritable = [];
9743
10163
  for (const dir of dirs) {
9744
10164
  const ancestor = await nearestExistingDirectory(dir);
9745
10165
  try {
9746
- await (0, import_promises12.access)(ancestor, import_node_fs18.constants.W_OK);
10166
+ await (0, import_promises13.access)(ancestor, import_node_fs18.constants.W_OK);
9747
10167
  } catch {
9748
10168
  unwritable.push(dir);
9749
10169
  }
@@ -9760,7 +10180,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
9760
10180
  for (let index = 0; index < entry.outputs.length; index++) {
9761
10181
  const output = entry.outputs[index];
9762
10182
  const spec = specByKey.get(key);
9763
- const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, import_node_path21.default.dirname(lockPath));
10183
+ const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, import_node_path22.default.dirname(lockPath));
9764
10184
  const owner = outputOwners.get(absolute);
9765
10185
  if (owner && owner !== key) duplicateOutputs.push(`${output.path} (${owner}, ${key})`);
9766
10186
  outputOwners.set(absolute, key);
@@ -9775,7 +10195,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
9775
10195
  } else {
9776
10196
  add("lock-outputs", "ok", `${outputOwners.size} recorded output path(s) are uniquely owned`);
9777
10197
  }
9778
- const cacheDir = import_node_path21.default.join(import_node_path21.default.dirname(lockPath), ".pixelkiln", "cache");
10198
+ const cacheDir = import_node_path22.default.join(import_node_path22.default.dirname(lockPath), ".pixelkiln", "cache");
9779
10199
  const stranded = [];
9780
10200
  const invalidState = [];
9781
10201
  const staleMapUrls = [];
@@ -9791,7 +10211,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
9791
10211
  const hasSource = Boolean(entry.sourceUrl || entry.sourceUrls?.length);
9792
10212
  let hasCache = false;
9793
10213
  for (const output of entry.outputs) {
9794
- const cached = import_node_path21.default.join(
10214
+ const cached = import_node_path22.default.join(
9795
10215
  cacheDir,
9796
10216
  cacheFileName(output.sha256, output.mediaType ?? MediaType.PNG)
9797
10217
  );
@@ -9888,10 +10308,10 @@ async function nearestExistingDirectory(start) {
9888
10308
  let current = start;
9889
10309
  for (; ; ) {
9890
10310
  try {
9891
- if ((await (0, import_promises12.stat)(current)).isDirectory()) return current;
10311
+ if ((await (0, import_promises13.stat)(current)).isDirectory()) return current;
9892
10312
  } catch {
9893
10313
  }
9894
- const parent = import_node_path21.default.dirname(current);
10314
+ const parent = import_node_path22.default.dirname(current);
9895
10315
  if (parent === current) return current;
9896
10316
  current = parent;
9897
10317
  }
@@ -9899,8 +10319,8 @@ async function nearestExistingDirectory(start) {
9899
10319
 
9900
10320
  // src/pipeline/adopt.ts
9901
10321
  var import_node_fs19 = require("fs");
9902
- var import_promises13 = require("fs/promises");
9903
- var import_node_path22 = __toESM(require("path"), 1);
10322
+ var import_promises14 = require("fs/promises");
10323
+ var import_node_path23 = __toESM(require("path"), 1);
9904
10324
  async function adopt(provider, specs, lock, lockPath, opts = {}) {
9905
10325
  const log = opts.onProgress ?? (() => {
9906
10326
  });
@@ -9915,7 +10335,7 @@ async function adopt(provider, specs, lock, lockPath, opts = {}) {
9915
10335
  unmatchedLocal.push(`${spec.styleId}/${spec.assetId} (no file at ${spec.outFile})`);
9916
10336
  continue;
9917
10337
  }
9918
- const bytes = await (0, import_promises13.readFile)(spec.outFile);
10338
+ const bytes = await (0, import_promises14.readFile)(spec.outFile);
9919
10339
  try {
9920
10340
  decodePng(bytes);
9921
10341
  } catch (err) {
@@ -10034,12 +10454,12 @@ async function tagAdopted(provider, specs, lock, opts = {}) {
10034
10454
 
10035
10455
  // src/pick/server.ts
10036
10456
  var import_node_fs21 = require("fs");
10037
- var import_promises16 = require("fs/promises");
10457
+ var import_promises17 = require("fs/promises");
10038
10458
 
10039
10459
  // src/pipeline/init.ts
10040
- var import_promises14 = require("fs/promises");
10460
+ var import_promises15 = require("fs/promises");
10041
10461
  var import_node_fs20 = require("fs");
10042
- var import_node_path23 = __toESM(require("path"), 1);
10462
+ var import_node_path24 = __toESM(require("path"), 1);
10043
10463
  function pngSize(buf) {
10044
10464
  if (buf.length < 24) return null;
10045
10465
  const isPng = buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834;
@@ -10052,8 +10472,8 @@ function slugify(value) {
10052
10472
  }
10053
10473
  async function walk(dir, exclude) {
10054
10474
  const out = [];
10055
- for (const entry of await (0, import_promises14.readdir)(dir, { withFileTypes: true })) {
10056
- const full = import_node_path23.default.join(dir, entry.name);
10475
+ for (const entry of await (0, import_promises15.readdir)(dir, { withFileTypes: true })) {
10476
+ const full = import_node_path24.default.join(dir, entry.name);
10057
10477
  if (exclude.some((x) => full.includes(x))) continue;
10058
10478
  if (entry.isDirectory()) out.push(...await walk(full, exclude));
10059
10479
  else if (entry.name.toLowerCase().endsWith(".png")) out.push(full);
@@ -10067,14 +10487,14 @@ async function scanAssets(root, opts = {}) {
10067
10487
  const skipped = [];
10068
10488
  const seen = /* @__PURE__ */ new Map();
10069
10489
  for (const file of files.sort()) {
10070
- const size = pngSize(await (0, import_promises14.readFile)(file));
10490
+ const size = pngSize(await (0, import_promises15.readFile)(file));
10071
10491
  if (!size) {
10072
10492
  skipped.push(`${file} (not a readable PNG)`);
10073
10493
  continue;
10074
10494
  }
10075
- const rel = import_node_path23.default.relative(root, file);
10076
- const category = import_node_path23.default.dirname(rel) === "." ? "" : import_node_path23.default.dirname(rel);
10077
- let id = slugify(import_node_path23.default.basename(rel));
10495
+ const rel = import_node_path24.default.relative(root, file);
10496
+ const category = import_node_path24.default.dirname(rel) === "." ? "" : import_node_path24.default.dirname(rel);
10497
+ let id = slugify(import_node_path24.default.basename(rel));
10078
10498
  if (seen.has(id)) {
10079
10499
  const n = seen.get(id) + 1;
10080
10500
  seen.set(id, n);
@@ -10525,7 +10945,7 @@ document.getElementById('submit').onclick = async () => {
10525
10945
 
10526
10946
  // src/pick/review-server.ts
10527
10947
  var import_node_http = require("http");
10528
- var import_promises15 = require("fs/promises");
10948
+ var import_promises16 = require("fs/promises");
10529
10949
  function serveReviewPage(opts) {
10530
10950
  const log = opts.onProgress ?? (() => {
10531
10951
  });
@@ -10534,7 +10954,7 @@ function serveReviewPage(opts) {
10534
10954
  if (req.method === "GET" && req.url && opts.assets?.has(req.url)) {
10535
10955
  try {
10536
10956
  const asset = opts.assets.get(req.url);
10537
- const bytes = await (0, import_promises15.readFile)(asset.path);
10957
+ const bytes = await (0, import_promises16.readFile)(asset.path);
10538
10958
  res.writeHead(200, {
10539
10959
  "Content-Type": asset.contentType,
10540
10960
  "Cache-Control": "no-store",
@@ -10616,7 +11036,7 @@ async function currentArt(entry, spec) {
10616
11036
  let width = entry.width;
10617
11037
  let height = entry.height;
10618
11038
  if (contentType === "image/png") {
10619
- const size = pngSize(await (0, import_promises16.readFile)(file));
11039
+ const size = pngSize(await (0, import_promises17.readFile)(file));
10620
11040
  if (size) ({ width, height } = size);
10621
11041
  }
10622
11042
  return { path: file, contentType, width, height };
@@ -10782,13 +11202,13 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
10782
11202
  // src/gallery/snapshot.ts
10783
11203
  var import_node_crypto6 = require("crypto");
10784
11204
  var import_node_fs24 = require("fs");
10785
- var import_promises20 = require("fs/promises");
10786
- var import_node_path28 = __toESM(require("path"), 1);
11205
+ var import_promises21 = require("fs/promises");
11206
+ var import_node_path29 = __toESM(require("path"), 1);
10787
11207
 
10788
11208
  // src/workspace.ts
10789
- var import_promises17 = require("fs/promises");
11209
+ var import_promises18 = require("fs/promises");
10790
11210
  var import_node_fs22 = require("fs");
10791
- var import_node_path24 = __toESM(require("path"), 1);
11211
+ var import_node_path25 = __toESM(require("path"), 1);
10792
11212
  var import_zod6 = require("zod");
10793
11213
  var WorkspaceProjectSchema = import_zod6.z.object({
10794
11214
  id: import_zod6.z.string().min(1),
@@ -10816,7 +11236,7 @@ async function loadWorkspace(workspacePath) {
10816
11236
  if (!(0, import_node_fs22.existsSync)(workspacePath)) return { version: 1, projects: [] };
10817
11237
  let raw;
10818
11238
  try {
10819
- raw = JSON.parse(await (0, import_promises17.readFile)(workspacePath, "utf8"));
11239
+ raw = JSON.parse(await (0, import_promises18.readFile)(workspacePath, "utf8"));
10820
11240
  } catch (err) {
10821
11241
  throw new ProjectError(
10822
11242
  `Workspace catalog at ${workspacePath} is malformed:
@@ -10830,22 +11250,22 @@ async function saveWorkspace(workspacePath, ws) {
10830
11250
  version: 1,
10831
11251
  projects: [...ws.projects].sort((a, b) => a.id.localeCompare(b.id))
10832
11252
  };
10833
- await (0, import_promises17.mkdir)(import_node_path24.default.dirname(import_node_path24.default.resolve(workspacePath)), { recursive: true });
11253
+ await (0, import_promises18.mkdir)(import_node_path25.default.dirname(import_node_path25.default.resolve(workspacePath)), { recursive: true });
10834
11254
  const tmp = `${workspacePath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
10835
11255
  try {
10836
- await (0, import_promises17.writeFile)(tmp, JSON.stringify(sorted, null, 2) + "\n");
10837
- await (0, import_promises17.rename)(tmp, workspacePath);
11256
+ await (0, import_promises18.writeFile)(tmp, JSON.stringify(sorted, null, 2) + "\n");
11257
+ await (0, import_promises18.rename)(tmp, workspacePath);
10838
11258
  } finally {
10839
- await (0, import_promises17.rm)(tmp, { force: true });
11259
+ await (0, import_promises18.rm)(tmp, { force: true });
10840
11260
  }
10841
11261
  }
10842
11262
  function toPortablePath(dir, absolute) {
10843
- return import_node_path24.default.relative(dir, absolute).split(import_node_path24.default.sep).join("/");
11263
+ return import_node_path25.default.relative(dir, absolute).split(import_node_path25.default.sep).join("/");
10844
11264
  }
10845
11265
  function resolveProject(dir, project) {
10846
11266
  return {
10847
- manifestPath: import_node_path24.default.resolve(dir, project.manifest.split("/").join(import_node_path24.default.sep)),
10848
- lockPath: import_node_path24.default.resolve(dir, project.lock.split("/").join(import_node_path24.default.sep))
11267
+ manifestPath: import_node_path25.default.resolve(dir, project.manifest.split("/").join(import_node_path25.default.sep)),
11268
+ lockPath: import_node_path25.default.resolve(dir, project.lock.split("/").join(import_node_path25.default.sep))
10849
11269
  };
10850
11270
  }
10851
11271
  function validateWorkspace(ws, dir) {
@@ -10858,7 +11278,7 @@ function validateWorkspace(ws, dir) {
10858
11278
  const { manifestPath, lockPath } = resolveProject(dir, project);
10859
11279
  lockOwners.set(lockPath, [...lockOwners.get(lockPath) ?? [], project.id]);
10860
11280
  manifestOwners.set(manifestPath, [...manifestOwners.get(manifestPath) ?? [], project.id]);
10861
- if (import_node_path24.default.isAbsolute(project.manifest) || import_node_path24.default.isAbsolute(project.lock)) {
11281
+ if (import_node_path25.default.isAbsolute(project.manifest) || import_node_path25.default.isAbsolute(project.lock)) {
10862
11282
  diagnostics.push({
10863
11283
  id: "absolute-path",
10864
11284
  level: "warning",
@@ -10919,12 +11339,12 @@ function validateWorkspace(ws, dir) {
10919
11339
  }
10920
11340
 
10921
11341
  // src/gallery/edit.ts
10922
- var import_node_path27 = __toESM(require("path"), 1);
11342
+ var import_node_path28 = __toESM(require("path"), 1);
10923
11343
  var import_zod9 = require("zod");
10924
11344
 
10925
11345
  // src/manifest-edit.ts
10926
- var import_promises18 = require("fs/promises");
10927
- var import_node_path25 = __toESM(require("path"), 1);
11346
+ var import_promises19 = require("fs/promises");
11347
+ var import_node_path26 = __toESM(require("path"), 1);
10928
11348
  var import_zod7 = require("zod");
10929
11349
  var HexSha = import_zod7.z.string().regex(/^[0-9a-f]{64}$/);
10930
11350
  var AssetPatchSchema = import_zod7.z.object({
@@ -11139,8 +11559,8 @@ function applyEdit(raw, edit) {
11139
11559
  }
11140
11560
  }
11141
11561
  async function applyManifestEdit(manifestPath, edit) {
11142
- const absolute = import_node_path25.default.resolve(manifestPath);
11143
- const original = await (0, import_promises18.readFile)(absolute, "utf8");
11562
+ const absolute = import_node_path26.default.resolve(manifestPath);
11563
+ const original = await (0, import_promises19.readFile)(absolute, "utf8");
11144
11564
  const before = sha256(original);
11145
11565
  if (before !== edit.expectedSha256) throw new ManifestDriftError(absolute);
11146
11566
  let raw;
@@ -11154,12 +11574,12 @@ async function applyManifestEdit(manifestPath, edit) {
11154
11574
  applyEdit(raw, edit);
11155
11575
  const next = serializeLike(original, raw);
11156
11576
  if (next === original) return { manifestPath: absolute, sha256: before, changed: false };
11157
- const tmp = import_node_path25.default.join(
11158
- import_node_path25.default.dirname(absolute),
11159
- `.${import_node_path25.default.basename(absolute)}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`
11577
+ const tmp = import_node_path26.default.join(
11578
+ import_node_path26.default.dirname(absolute),
11579
+ `.${import_node_path26.default.basename(absolute)}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`
11160
11580
  );
11161
11581
  try {
11162
- await (0, import_promises18.writeFile)(tmp, next);
11582
+ await (0, import_promises19.writeFile)(tmp, next);
11163
11583
  try {
11164
11584
  const loaded = await loadManifest(tmp);
11165
11585
  await resolveSpecs(loaded);
@@ -11167,26 +11587,26 @@ async function applyManifestEdit(manifestPath, edit) {
11167
11587
  const message6 = error instanceof Error ? error.message : String(error);
11168
11588
  throw new ManifestEditError(message6.replace(tmp, absolute));
11169
11589
  }
11170
- await (0, import_promises18.rename)(tmp, absolute);
11590
+ await (0, import_promises19.rename)(tmp, absolute);
11171
11591
  } finally {
11172
- await (0, import_promises18.rm)(tmp, { force: true });
11592
+ await (0, import_promises19.rm)(tmp, { force: true });
11173
11593
  }
11174
11594
  return { manifestPath: absolute, sha256: sha256(next), changed: true };
11175
11595
  }
11176
11596
 
11177
11597
  // src/pipeline/hand-edit.ts
11178
11598
  var import_node_fs23 = require("fs");
11179
- var import_promises19 = require("fs/promises");
11180
- var import_node_path26 = __toESM(require("path"), 1);
11599
+ var import_promises20 = require("fs/promises");
11600
+ var import_node_path27 = __toESM(require("path"), 1);
11181
11601
  var import_zod8 = require("zod");
11182
11602
  var HAND_EDIT_DIR = "edits";
11183
11603
  function handEditPath(loaded, spec) {
11184
11604
  const style = loaded.manifest.styles[spec.styleId];
11185
- const outDir = import_node_path26.default.resolve(loaded.root, style?.outDir ?? ".");
11186
- const relative = import_node_path26.default.relative(outDir, spec.outFile);
11187
- const inside = relative && !relative.startsWith("..") && !import_node_path26.default.isAbsolute(relative);
11188
- const file = inside ? relative : import_node_path26.default.basename(spec.outFile);
11189
- return import_node_path26.default.join(outDir, HAND_EDIT_DIR, file.replace(/\.gif$/i, ".png"));
11605
+ const outDir = import_node_path27.default.resolve(loaded.root, style?.outDir ?? ".");
11606
+ const relative = import_node_path27.default.relative(outDir, spec.outFile);
11607
+ const inside = relative && !relative.startsWith("..") && !import_node_path27.default.isAbsolute(relative);
11608
+ const file = inside ? relative : import_node_path27.default.basename(spec.outFile);
11609
+ return import_node_path27.default.join(outDir, HAND_EDIT_DIR, file.replace(/\.gif$/i, ".png"));
11190
11610
  }
11191
11611
  function handEditBases(spec, lock) {
11192
11612
  const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
@@ -11232,7 +11652,7 @@ async function startHandEdit(loaded, lock, spec, opts = {}) {
11232
11652
  `${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`
11233
11653
  );
11234
11654
  }
11235
- const existing = spec.source ? import_node_path26.default.resolve(loaded.root, spec.source) : null;
11655
+ const existing = spec.source ? import_node_path27.default.resolve(loaded.root, spec.source) : null;
11236
11656
  const editPath = existing ?? handEditPath(loaded, spec);
11237
11657
  const members = handEditMembers(loaded, spec, lock, editPath);
11238
11658
  let created = false;
@@ -11243,8 +11663,8 @@ async function startHandEdit(loaded, lock, spec, opts = {}) {
11243
11663
  `${spec.styleId}/${spec.assetId} has no generated PNG on disk to start from; generate or restore it first`
11244
11664
  );
11245
11665
  }
11246
- await (0, import_promises19.mkdir)(import_node_path26.default.dirname(member.path), { recursive: true });
11247
- await (0, import_promises19.copyFile)(member.base, member.path);
11666
+ await (0, import_promises20.mkdir)(import_node_path27.default.dirname(member.path), { recursive: true });
11667
+ await (0, import_promises20.copyFile)(member.base, member.path);
11248
11668
  created = true;
11249
11669
  }
11250
11670
  const source = portableOutputPath(editPath, loaded.root);
@@ -11322,7 +11742,7 @@ function handEditProjectPath(editPath) {
11322
11742
  async function readHandEditCompanion(editPath) {
11323
11743
  let text;
11324
11744
  try {
11325
- text = await (0, import_promises19.readFile)(handEditCompanionPath(editPath), "utf8");
11745
+ text = await (0, import_promises20.readFile)(handEditCompanionPath(editPath), "utf8");
11326
11746
  } catch {
11327
11747
  return null;
11328
11748
  }
@@ -11365,7 +11785,7 @@ async function saveHandEdit(loaded, lock, spec, input) {
11365
11785
  }
11366
11786
  });
11367
11787
  const bases = handEditBases(spec, lock);
11368
- const expectedSize = bases?.[0] ? decodePng(await (0, import_promises19.readFile)(bases[0].path)) : { width: spec.width, height: spec.height };
11788
+ const expectedSize = bases?.[0] ? decodePng(await (0, import_promises20.readFile)(bases[0].path)) : { width: spec.width, height: spec.height };
11369
11789
  for (const [index, image] of decoded.entries()) {
11370
11790
  if (image.width !== expectedSize.width || image.height !== expectedSize.height) {
11371
11791
  const which = memberSet ? `member ${incoming[index].role ?? index}` : "the edit";
@@ -11381,7 +11801,7 @@ async function saveHandEdit(loaded, lock, spec, input) {
11381
11801
  editor: input.editor,
11382
11802
  protocol: input.protocol,
11383
11803
  savedAt: (input.now ?? /* @__PURE__ */ new Date()).toISOString(),
11384
- project: projectPath ? import_node_path26.default.basename(projectPath) : null,
11804
+ project: projectPath ? import_node_path27.default.basename(projectPath) : null,
11385
11805
  outputs: incoming.map((frame, index) => ({
11386
11806
  role: frame.role,
11387
11807
  basedOn: basedOnHash(entry, index),
@@ -11411,10 +11831,10 @@ function basedOnHash(entry, index) {
11411
11831
  async function replaceFile(file, bytes) {
11412
11832
  const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
11413
11833
  try {
11414
- await (0, import_promises19.writeFile)(tmp, bytes);
11415
- await (0, import_promises19.rename)(tmp, file);
11834
+ await (0, import_promises20.writeFile)(tmp, bytes);
11835
+ await (0, import_promises20.rename)(tmp, file);
11416
11836
  } finally {
11417
- await (0, import_promises19.rm)(tmp, { force: true });
11837
+ await (0, import_promises20.rm)(tmp, { force: true });
11418
11838
  }
11419
11839
  }
11420
11840
  function openInEditor(file, editor = process.env.PIXELKILN_EDITOR) {
@@ -11511,7 +11931,7 @@ function createGalleryEditHandler(opts) {
11511
11931
  }
11512
11932
  const result = await applyManifestEdit(manifestPath, parsed.data);
11513
11933
  if (result.changed) {
11514
- const relative = import_node_path27.default.relative(process.cwd(), result.manifestPath);
11934
+ const relative = import_node_path28.default.relative(process.cwd(), result.manifestPath);
11515
11935
  const shown = !relative || relative.startsWith("..") ? result.manifestPath : relative;
11516
11936
  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;
11517
11937
  log(` manifest edited: ${parsed.data.action === "add-asset" ? "added" : "changed"} ${subject} in ${shown}`);
@@ -11523,7 +11943,7 @@ function createGalleryEditHandler(opts) {
11523
11943
  // src/gallery/snapshot.ts
11524
11944
  var PROJECT_FILE_TYPE = "application/octet-stream";
11525
11945
  function galleryMediaId(absolutePath) {
11526
- return (0, import_node_crypto6.createHash)("sha256").update(import_node_path28.default.resolve(absolutePath)).digest("hex").slice(0, 24);
11946
+ return (0, import_node_crypto6.createHash)("sha256").update(import_node_path29.default.resolve(absolutePath)).digest("hex").slice(0, 24);
11527
11947
  }
11528
11948
  function galleryMediaRoute(id) {
11529
11949
  return `/media/${id}`;
@@ -11542,7 +11962,7 @@ function describeHistory(media, entry, cacheDir) {
11542
11962
  return (entry.history ?? []).map((generation, i) => {
11543
11963
  const outputs = generation.outputs.map((output) => {
11544
11964
  const mediaType = output.mediaType ?? MediaType.PNG;
11545
- const file = import_node_path28.default.join(cacheDir, cacheFileName(output.sha256, mediaType));
11965
+ const file = import_node_path29.default.join(cacheDir, cacheFileName(output.sha256, mediaType));
11546
11966
  let url = null;
11547
11967
  if ((0, import_node_fs24.existsSync)(file)) {
11548
11968
  const id = galleryMediaId(file);
@@ -11573,7 +11993,7 @@ function describeHistory(media, entry, cacheDir) {
11573
11993
  var MAX_COMPARED_BYTES = 8 * 1024 * 1024;
11574
11994
  async function samePixels(a, b) {
11575
11995
  try {
11576
- const [left, right] = await Promise.all([(0, import_promises20.readFile)(a), (0, import_promises20.readFile)(b)]);
11996
+ const [left, right] = await Promise.all([(0, import_promises21.readFile)(a), (0, import_promises21.readFile)(b)]);
11577
11997
  if (left.length > MAX_COMPARED_BYTES || right.length > MAX_COMPARED_BYTES) return false;
11578
11998
  const x = decodePng(left);
11579
11999
  const y = decodePng(right);
@@ -11611,7 +12031,7 @@ function describeCharacter(spec, entry) {
11611
12031
  }
11612
12032
  async function fileInfo(absolutePath) {
11613
12033
  try {
11614
- const info = await (0, import_promises20.stat)(absolutePath);
12034
+ const info = await (0, import_promises21.stat)(absolutePath);
11615
12035
  return info.isFile() ? { bytes: info.size, modifiedAt: info.mtime.toISOString() } : null;
11616
12036
  } catch {
11617
12037
  return null;
@@ -11643,9 +12063,9 @@ async function describeOutput(media, root, absolutePath, recorded = {}) {
11643
12063
  async function describeQuality(media, root, inspection) {
11644
12064
  const outputPaths = inspection.outputs ?? [inspection.output];
11645
12065
  const outputs = await Promise.all(
11646
- outputPaths.map((file) => describeOutput(media, root, import_node_path28.default.resolve(root, file)))
12066
+ outputPaths.map((file) => describeOutput(media, root, import_node_path29.default.resolve(root, file)))
11647
12067
  );
11648
- const recordPath = import_node_path28.default.resolve(root, inspection.record);
12068
+ const recordPath = import_node_path29.default.resolve(root, inspection.record);
11649
12069
  const recordExists = (0, import_node_fs24.existsSync)(recordPath);
11650
12070
  let options = null;
11651
12071
  let check = null;
@@ -11665,7 +12085,7 @@ async function describeQuality(media, root, inspection) {
11665
12085
  return {
11666
12086
  state: inspection.state,
11667
12087
  reason: inspection.reason,
11668
- output: portableOutputPath(import_node_path28.default.resolve(root, inspection.output), root),
12088
+ output: portableOutputPath(import_node_path29.default.resolve(root, inspection.output), root),
11669
12089
  record: portableOutputPath(recordPath, root),
11670
12090
  recordExists,
11671
12091
  outputs,
@@ -11702,9 +12122,9 @@ async function buildGallerySnapshot(opts) {
11702
12122
  const media = /* @__PURE__ */ new Map();
11703
12123
  const plan = await buildPlan(specs, lock);
11704
12124
  const items = [];
11705
- const manifestText = await (0, import_promises20.readFile)(loaded.path, "utf8");
12125
+ const manifestText = await (0, import_promises21.readFile)(loaded.path, "utf8");
11706
12126
  const manifestSha256 = sha256(manifestText);
11707
- const cacheDir = import_node_path28.default.resolve(import_node_path28.default.dirname(opts.lockPath), ".pixelkiln", "cache");
12127
+ const cacheDir = import_node_path29.default.resolve(import_node_path29.default.dirname(opts.lockPath), ".pixelkiln", "cache");
11708
12128
  let rawStyles = {};
11709
12129
  try {
11710
12130
  const raw = JSON.parse(manifestText);
@@ -11728,7 +12148,7 @@ async function buildGallerySnapshot(opts) {
11728
12148
  )
11729
12149
  );
11730
12150
  if (spec.source) {
11731
- const editPath = import_node_path28.default.resolve(root, spec.source);
12151
+ const editPath = import_node_path29.default.resolve(root, spec.source);
11732
12152
  const memberCount = sourceIsStem(spec.source, entry, root) ? entry.outputs.length : 1;
11733
12153
  const editShas = await Promise.all(
11734
12154
  Array.from({ length: memberCount }, async (_, index) => {
@@ -11777,7 +12197,7 @@ async function buildGallerySnapshot(opts) {
11777
12197
  editStatus = edits.some((member) => !member.exists) ? "missing" : editChanged.every((changed) => !changed) ? "same" : regenerated ? "regenerated-since" : "edited";
11778
12198
  }
11779
12199
  } else if (spec.source) {
11780
- outputs = [await describeOutput(media, root, import_node_path28.default.resolve(root, spec.source))];
12200
+ outputs = [await describeOutput(media, root, import_node_path29.default.resolve(root, spec.source))];
11781
12201
  } else if (!entry && (0, import_node_fs24.existsSync)(spec.outFile)) {
11782
12202
  outputs = [await describeOutput(media, root, spec.outFile)];
11783
12203
  } else {
@@ -11817,6 +12237,7 @@ async function buildGallerySnapshot(opts) {
11817
12237
  currentSpecHash: spec.specHash,
11818
12238
  revision: entry?.revision ?? null,
11819
12239
  revisionParentKey: spec.revision ? lockKey(spec.styleId, spec.revision.sourceAssetId) : entry?.revision ? lockKey(spec.styleId, entry.revision.sourceAssetId) : null,
12240
+ mirrorOfKey: spec.mirror ? lockKey(spec.styleId, spec.mirror.sourceAssetId) : entry?.mirror ? lockKey(spec.styleId, entry.mirror.sourceAssetId) : null,
11820
12241
  character: describeCharacter(spec, entry),
11821
12242
  outputs,
11822
12243
  quality,
@@ -11879,6 +12300,7 @@ async function buildGallerySnapshot(opts) {
11879
12300
  currentSpecHash: null,
11880
12301
  revision: entry.revision,
11881
12302
  revisionParentKey: entry.revision ? lockKey(entry.styleId, entry.revision.sourceAssetId) : null,
12303
+ mirrorOfKey: entry.mirror ? lockKey(entry.styleId, entry.mirror.sourceAssetId) : null,
11882
12304
  character: describeCharacter(void 0, entry),
11883
12305
  outputs,
11884
12306
  quality: null,
@@ -11967,7 +12389,7 @@ async function buildGallerySnapshot(opts) {
11967
12389
  id: loaded.manifest.name,
11968
12390
  name: loaded.manifest.name,
11969
12391
  manifest: loaded.path,
11970
- lock: import_node_path28.default.resolve(opts.lockPath),
12392
+ lock: import_node_path29.default.resolve(opts.lockPath),
11971
12393
  root,
11972
12394
  provider: loaded.manifest.provider,
11973
12395
  account: null,
@@ -11993,7 +12415,7 @@ async function buildGallerySnapshot(opts) {
11993
12415
  return { snapshot, media };
11994
12416
  }
11995
12417
  async function buildWorkspaceGallerySnapshot(opts) {
11996
- const dir = import_node_path28.default.dirname(import_node_path28.default.resolve(opts.workspacePath));
12418
+ const dir = import_node_path29.default.dirname(import_node_path29.default.resolve(opts.workspacePath));
11997
12419
  const media = /* @__PURE__ */ new Map();
11998
12420
  const projects = [];
11999
12421
  const items = [];
@@ -12036,11 +12458,11 @@ async function buildWorkspaceGallerySnapshot(opts) {
12036
12458
  id: project.id,
12037
12459
  name: loaded.manifest.name,
12038
12460
  manifest: manifestPath,
12039
- lock: import_node_path28.default.resolve(lockPath),
12461
+ lock: import_node_path29.default.resolve(lockPath),
12040
12462
  root: loaded.root,
12041
12463
  provider: loaded.manifest.provider,
12042
12464
  account: project.account ?? null,
12043
- manifestSha256: manifestSha256 ?? sha256(await (0, import_promises20.readFile)(manifestPath, "utf8")),
12465
+ manifestSha256: manifestSha256 ?? sha256(await (0, import_promises21.readFile)(manifestPath, "utf8")),
12044
12466
  historyLimit: historyLimit(loaded.manifest),
12045
12467
  entries: Object.keys(lock.entries).length,
12046
12468
  items: projectItems.length,
@@ -12052,8 +12474,8 @@ async function buildWorkspaceGallerySnapshot(opts) {
12052
12474
  id: project.id,
12053
12475
  name: project.id,
12054
12476
  manifest: manifestPath,
12055
- lock: import_node_path28.default.resolve(lockPath),
12056
- root: import_node_path28.default.dirname(manifestPath),
12477
+ lock: import_node_path29.default.resolve(lockPath),
12478
+ root: import_node_path29.default.dirname(manifestPath),
12057
12479
  provider: project.provider,
12058
12480
  account: project.account ?? null,
12059
12481
  manifestSha256: null,
@@ -12080,7 +12502,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
12080
12502
  version: 1,
12081
12503
  generatedAt: (opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
12082
12504
  project: null,
12083
- workspace: { path: import_node_path28.default.resolve(opts.workspacePath), dir, projects },
12505
+ workspace: { path: import_node_path29.default.resolve(opts.workspacePath), dir, projects },
12084
12506
  filter,
12085
12507
  totals: {
12086
12508
  entries: projects.reduce((sum, project) => sum + project.entries, 0),
@@ -12230,9 +12652,9 @@ var EDITOR_RELEASE_BASE = "https://github.com/gfargo/pixelkiln/releases/download
12230
12652
  // src/editor/install.ts
12231
12653
  var import_node_crypto7 = require("crypto");
12232
12654
  var import_node_fs26 = require("fs");
12233
- var import_promises21 = require("fs/promises");
12655
+ var import_promises22 = require("fs/promises");
12234
12656
  var import_node_os3 = __toESM(require("os"), 1);
12235
- var import_node_path29 = __toESM(require("path"), 1);
12657
+ var import_node_path30 = __toESM(require("path"), 1);
12236
12658
  var EditorInstallError = class extends Error {
12237
12659
  constructor(message6, file) {
12238
12660
  super(message6);
@@ -12243,15 +12665,15 @@ var EditorInstallError = class extends Error {
12243
12665
  };
12244
12666
  function editorToolsRoot() {
12245
12667
  const override = process.env.PIXELKILN_TOOLS_DIR?.trim();
12246
- if (override) return import_node_path29.default.resolve(override);
12247
- if (process.platform === "darwin") return import_node_path29.default.join(import_node_os3.default.homedir(), "Library", "Caches", "pixelkiln", "tools");
12668
+ if (override) return import_node_path30.default.resolve(override);
12669
+ if (process.platform === "darwin") return import_node_path30.default.join(import_node_os3.default.homedir(), "Library", "Caches", "pixelkiln", "tools");
12248
12670
  if (process.platform === "win32") {
12249
- return import_node_path29.default.join(process.env.LOCALAPPDATA ?? import_node_path29.default.join(import_node_os3.default.homedir(), "AppData", "Local"), "pixelkiln", "tools");
12671
+ return import_node_path30.default.join(process.env.LOCALAPPDATA ?? import_node_path30.default.join(import_node_os3.default.homedir(), "AppData", "Local"), "pixelkiln", "tools");
12250
12672
  }
12251
- return import_node_path29.default.join(process.env.XDG_CACHE_HOME ?? import_node_path29.default.join(import_node_os3.default.homedir(), ".cache"), "pixelkiln", "tools");
12673
+ return import_node_path30.default.join(process.env.XDG_CACHE_HOME ?? import_node_path30.default.join(import_node_os3.default.homedir(), ".cache"), "pixelkiln", "tools");
12252
12674
  }
12253
12675
  function editorDir(pin = EDITOR_PIN, root = editorToolsRoot()) {
12254
- return import_node_path29.default.join(root, "pixelorama", pin.release ?? "unpublished");
12676
+ return import_node_path30.default.join(root, "pixelorama", pin.release ?? "unpublished");
12255
12677
  }
12256
12678
  function editorBaseUrl(pin = EDITOR_PIN) {
12257
12679
  const override = process.env.PIXELKILN_EDITOR_URL?.trim();
@@ -12261,7 +12683,7 @@ function editorBaseUrl(pin = EDITOR_PIN) {
12261
12683
  }
12262
12684
  async function sha256Of(file) {
12263
12685
  try {
12264
- return (0, import_node_crypto7.createHash)("sha256").update(await (0, import_promises21.readFile)(file)).digest("hex");
12686
+ return (0, import_node_crypto7.createHash)("sha256").update(await (0, import_promises22.readFile)(file)).digest("hex");
12265
12687
  } catch {
12266
12688
  return null;
12267
12689
  }
@@ -12274,8 +12696,8 @@ async function editorStatus(opts = {}) {
12274
12696
  let totalBytes = 0;
12275
12697
  for (const [name, expected] of Object.entries(pin.files)) {
12276
12698
  totalBytes += expected.bytes;
12277
- const file = import_node_path29.default.join(dir, name);
12278
- const info = (0, import_node_fs26.existsSync)(file) ? await (0, import_promises21.stat)(file).catch(() => null) : null;
12699
+ const file = import_node_path30.default.join(dir, name);
12700
+ const info = (0, import_node_fs26.existsSync)(file) ? await (0, import_promises22.stat)(file).catch(() => null) : null;
12279
12701
  if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
12280
12702
  installedBytes += expected.bytes;
12281
12703
  } else {
@@ -12304,7 +12726,7 @@ async function installEditor(opts = {}) {
12304
12726
  );
12305
12727
  }
12306
12728
  if (!baseUrl) throw new EditorInstallError("no download location for the editor build");
12307
- await (0, import_promises21.mkdir)(dir, { recursive: true });
12729
+ await (0, import_promises22.mkdir)(dir, { recursive: true });
12308
12730
  const downloaded = [];
12309
12731
  const skipped = Object.keys(pin.files).filter((name) => !before.missing.includes(name));
12310
12732
  let fetchedBytes = before.installedBytes;
@@ -12332,13 +12754,13 @@ async function installEditor(opts = {}) {
12332
12754
  if (digest2 !== expected.sha256) {
12333
12755
  throw new EditorInstallError(`${name}: sha256 ${digest2.slice(0, 12)}\u2026 does not match the pinned ${expected.sha256.slice(0, 12)}\u2026`, name);
12334
12756
  }
12335
- const final = import_node_path29.default.join(dir, name);
12757
+ const final = import_node_path30.default.join(dir, name);
12336
12758
  const tmp = `${final}.${process.pid}.part`;
12337
12759
  try {
12338
- await (0, import_promises21.writeFile)(tmp, bytes);
12339
- await (0, import_promises21.rename)(tmp, final);
12760
+ await (0, import_promises22.writeFile)(tmp, bytes);
12761
+ await (0, import_promises22.rename)(tmp, final);
12340
12762
  } finally {
12341
- await (0, import_promises21.rm)(tmp, { force: true });
12763
+ await (0, import_promises22.rm)(tmp, { force: true });
12342
12764
  }
12343
12765
  fetchedBytes += bytes.length;
12344
12766
  downloaded.push(name);
@@ -12378,12 +12800,12 @@ var EDITOR_CONTENT_TYPES = {
12378
12800
  ".json": "application/json; charset=utf-8"
12379
12801
  };
12380
12802
  function editorContentType(name) {
12381
- return EDITOR_CONTENT_TYPES[import_node_path29.default.extname(name).toLowerCase()] ?? null;
12803
+ return EDITOR_CONTENT_TYPES[import_node_path30.default.extname(name).toLowerCase()] ?? null;
12382
12804
  }
12383
12805
 
12384
12806
  // src/gallery/editor.ts
12385
- var import_promises22 = require("fs/promises");
12386
- var import_node_path30 = __toESM(require("path"), 1);
12807
+ var import_promises23 = require("fs/promises");
12808
+ var import_node_path31 = __toESM(require("path"), 1);
12387
12809
  function editorRoute(release, name = "index.html") {
12388
12810
  return `/editor/${encodeURIComponent(release)}/${encodeURIComponent(name)}`;
12389
12811
  }
@@ -12456,7 +12878,7 @@ function createGalleryEditorHandlers(opts = {}) {
12456
12878
  if (!status.installed) return null;
12457
12879
  let bytes;
12458
12880
  try {
12459
- bytes = await (0, import_promises22.readFile)(import_node_path30.default.join(dir, name));
12881
+ bytes = await (0, import_promises23.readFile)(import_node_path31.default.join(dir, name));
12460
12882
  } catch {
12461
12883
  verified = null;
12462
12884
  return null;
@@ -12473,7 +12895,7 @@ function createGalleryEditorHandlers(opts = {}) {
12473
12895
  // src/gallery/server.ts
12474
12896
  var import_node_crypto8 = require("crypto");
12475
12897
  var import_node_http2 = require("http");
12476
- var import_promises23 = require("fs/promises");
12898
+ var import_promises24 = require("fs/promises");
12477
12899
  var MAX_EDIT_BYTES = 48 * 1024 * 1024;
12478
12900
  var EDITOR_CSP = [
12479
12901
  "default-src 'self'",
@@ -12632,7 +13054,7 @@ async function serveGallery(opts) {
12632
13054
  const asset = reviewAssets.get(jobId)?.get(url2.pathname);
12633
13055
  if (!asset) return fail(404, "no such review asset");
12634
13056
  try {
12635
- const bytes = await (0, import_promises23.readFile)(asset.path);
13057
+ const bytes = await (0, import_promises24.readFile)(asset.path);
12636
13058
  res.writeHead(200, {
12637
13059
  "Content-Type": asset.contentType,
12638
13060
  "Cache-Control": "no-store",
@@ -12708,7 +13130,7 @@ async function serveGallery(opts) {
12708
13130
  if (!asset) return fail(404, "no such gallery media");
12709
13131
  let bytes;
12710
13132
  try {
12711
- bytes = await (0, import_promises23.readFile)(asset.path);
13133
+ bytes = await (0, import_promises24.readFile)(asset.path);
12712
13134
  } catch {
12713
13135
  return fail(404, "gallery media is no longer on disk; refresh the page");
12714
13136
  }
@@ -13082,9 +13504,9 @@ function createGenerateHandlers(opts) {
13082
13504
  }
13083
13505
 
13084
13506
  // src/pipeline/salvage.ts
13085
- var import_promises24 = require("fs/promises");
13507
+ var import_promises25 = require("fs/promises");
13086
13508
  var import_node_fs27 = require("fs");
13087
- var import_node_path31 = __toESM(require("path"), 1);
13509
+ var import_node_path32 = __toESM(require("path"), 1);
13088
13510
  function providerClaimId(provider, objectId) {
13089
13511
  return `${encodeURIComponent(provider)}:${encodeURIComponent(objectId)}`;
13090
13512
  }
@@ -13094,7 +13516,7 @@ async function loadClaims(lockPaths, opts = {}) {
13094
13516
  if (!(0, import_node_fs27.existsSync)(p)) throw new Error(`Claim lockfile not found: ${p}`);
13095
13517
  let parsed;
13096
13518
  try {
13097
- parsed = parseLock(JSON.parse(await (0, import_promises24.readFile)(p, "utf8")));
13519
+ parsed = parseLock(JSON.parse(await (0, import_promises25.readFile)(p, "utf8")));
13098
13520
  } catch {
13099
13521
  throw new Error(`Claim lockfile is malformed: ${p}`);
13100
13522
  }
@@ -13147,20 +13569,20 @@ function matchOrphanStyle(prompt, manifest) {
13147
13569
  return matchStyleByPattern(prompt, manifest);
13148
13570
  }
13149
13571
  async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, claimPaths) {
13150
- const own = import_node_path31.default.resolve(ownManifestPath);
13572
+ const own = import_node_path32.default.resolve(ownManifestPath);
13151
13573
  const siblingManifestPaths = [
13152
13574
  .../* @__PURE__ */ new Set([
13153
13575
  ...workspaceManifestPaths,
13154
- ...claimPaths.map((c) => import_node_path31.default.join(import_node_path31.default.dirname(import_node_path31.default.resolve(c)), "pixelkiln.manifest.json"))
13576
+ ...claimPaths.map((c) => import_node_path32.default.join(import_node_path32.default.dirname(import_node_path32.default.resolve(c)), "pixelkiln.manifest.json"))
13155
13577
  ])
13156
13578
  ];
13157
13579
  const siblings = [];
13158
13580
  for (const siblingManifestPath of siblingManifestPaths) {
13159
- if (import_node_path31.default.resolve(siblingManifestPath) === own) continue;
13581
+ if (import_node_path32.default.resolve(siblingManifestPath) === own) continue;
13160
13582
  if (!(0, import_node_fs27.existsSync)(siblingManifestPath)) continue;
13161
13583
  try {
13162
13584
  const { manifest } = await loadManifest(siblingManifestPath);
13163
- siblings.push({ label: import_node_path31.default.basename(import_node_path31.default.dirname(siblingManifestPath)), manifest });
13585
+ siblings.push({ label: import_node_path32.default.basename(import_node_path32.default.dirname(siblingManifestPath)), manifest });
13164
13586
  } catch {
13165
13587
  }
13166
13588
  }
@@ -13276,8 +13698,8 @@ async function applyTags(provider, decisions, existing, opts = {}) {
13276
13698
  }
13277
13699
 
13278
13700
  // src/pick/salvage-server.ts
13279
- var import_promises25 = require("fs/promises");
13280
- var import_node_path32 = __toESM(require("path"), 1);
13701
+ var import_promises26 = require("fs/promises");
13702
+ var import_node_path33 = __toESM(require("path"), 1);
13281
13703
 
13282
13704
  // src/pick/salvage-sheet.ts
13283
13705
  var escapeHtml3 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
@@ -13442,7 +13864,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
13442
13864
  });
13443
13865
  const html = renderSalvageSheet(orphans, {
13444
13866
  styleId: ctx.styleId,
13445
- importDir: import_node_path32.default.relative(process.cwd(), ctx.importDir) || "."
13867
+ importDir: import_node_path33.default.relative(process.cwd(), ctx.importDir) || "."
13446
13868
  });
13447
13869
  const byId = new Map(orphans.map((o) => [o.id, o]));
13448
13870
  const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
@@ -13471,10 +13893,10 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
13471
13893
  if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
13472
13894
  decodePng(buf);
13473
13895
  const assetId = idFromPrompt(orphan.prompt, taken);
13474
- const rel = import_node_path32.default.join("_salvaged", `${assetId}.png`);
13475
- const outFile = import_node_path32.default.resolve(ctx.importDir, rel);
13476
- await (0, import_promises25.mkdir)(import_node_path32.default.dirname(outFile), { recursive: true });
13477
- await (0, import_promises25.writeFile)(outFile, buf);
13896
+ const rel = import_node_path33.default.join("_salvaged", `${assetId}.png`);
13897
+ const outFile = import_node_path33.default.resolve(ctx.importDir, rel);
13898
+ await (0, import_promises26.mkdir)(import_node_path33.default.dirname(outFile), { recursive: true });
13899
+ await (0, import_promises26.writeFile)(outFile, buf);
13478
13900
  ctx.manifest.assets[assetId] = {
13479
13901
  prompt: orphan.prompt,
13480
13902
  promptByStyle: {},
@@ -13506,7 +13928,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
13506
13928
  sourceUrl: durableSource,
13507
13929
  sourceUrls: durableSource ? [{ url: durableSource }] : [],
13508
13930
  outputs: [{
13509
- path: portableOutputPath(outFile, import_node_path32.default.dirname(ctx.manifestPath)),
13931
+ path: portableOutputPath(outFile, import_node_path33.default.dirname(ctx.manifestPath)),
13510
13932
  sha256: sha256(buf)
13511
13933
  }],
13512
13934
  submittedAt: orphan.createdAt,
@@ -13530,9 +13952,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
13530
13952
  }
13531
13953
  }
13532
13954
  await applyTags(provider, decisions, existingTags, { onProgress: log });
13533
- const raw = JSON.parse(await (0, import_promises25.readFile)(ctx.manifestPath, "utf8"));
13955
+ const raw = JSON.parse(await (0, import_promises26.readFile)(ctx.manifestPath, "utf8"));
13534
13956
  for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
13535
- await (0, import_promises25.writeFile)(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
13957
+ await (0, import_promises26.writeFile)(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
13536
13958
  await saveLock(ctx.lockPath, ctx.lock);
13537
13959
  return result;
13538
13960
  }
@@ -13644,10 +14066,12 @@ async function workspaceStatus(ws, dir) {
13644
14066
  BudgetError,
13645
14067
  CHARACTER_DIRECTIONS_4,
13646
14068
  CHARACTER_DIRECTIONS_8,
14069
+ CHARACTER_PROPORTION_PRESETS,
13647
14070
  CharacterAnimationModeSchema,
13648
14071
  CharacterAnimationSchema,
13649
14072
  CharacterDirectionSchema,
13650
14073
  CharacterModeSchema,
14074
+ CharacterProportionsSchema,
13651
14075
  CharacterStateSchema,
13652
14076
  ComfyUIProvider,
13653
14077
  DEFAULT_HISTORY_LIMIT,
@@ -13675,6 +14099,7 @@ async function workspaceStatus(ws, dir) {
13675
14099
  MAX_DOWNLOAD_BYTES,
13676
14100
  MAX_HAND_EDIT_BYTES,
13677
14101
  MAX_RETRIES,
14102
+ MIRRORED_DIRECTION,
13678
14103
  ManifestDriftError,
13679
14104
  ManifestEditError,
13680
14105
  ManifestEditSchema,
@@ -13813,6 +14238,7 @@ async function workspaceStatus(ws, dir) {
13813
14238
  portableOutputPath,
13814
14239
  prepareReview,
13815
14240
  primaryOutput,
14241
+ proportionsPayload,
13816
14242
  providerCredentialEnvs,
13817
14243
  providerFactory,
13818
14244
  pushTags,