pixelkiln 0.40.0 → 0.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -68,7 +68,7 @@ function exitCodeFor(err) {
68
68
 
69
69
  // src/cli/main.ts
70
70
  import { existsSync as existsSync32 } from "fs";
71
- import { readFile as readFile28 } from "fs/promises";
71
+ import { readFile as readFile29 } from "fs/promises";
72
72
 
73
73
  // src/cli/args.ts
74
74
  import path from "path";
@@ -610,7 +610,7 @@ function formatCost(unit, amount) {
610
610
 
611
611
  // src/pipeline/plan.ts
612
612
  import { existsSync as existsSync5 } from "fs";
613
- import path8 from "path";
613
+ import path9 from "path";
614
614
 
615
615
  // src/hash.ts
616
616
  import { createHash } from "crypto";
@@ -618,10 +618,10 @@ import { createReadStream } from "fs";
618
618
  function sha256(data) {
619
619
  return createHash("sha256").update(data).digest("hex");
620
620
  }
621
- async function sha256File(path44) {
621
+ async function sha256File(path45) {
622
622
  return new Promise((resolve, reject) => {
623
623
  const hash = createHash("sha256");
624
- const stream = createReadStream(path44);
624
+ const stream = createReadStream(path45);
625
625
  stream.on("data", (chunk2) => hash.update(chunk2));
626
626
  stream.on("error", reject);
627
627
  stream.on("end", () => resolve(hash.digest("hex")));
@@ -667,6 +667,9 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
667
667
  state: spec.character.state,
668
668
  animation: spec.character.animation
669
669
  } : void 0,
670
+ // A mirror's bytes come from its source's recorded outputs; the plan
671
+ // compares those directly, so only the choice of source is identity.
672
+ mirror: spec.mirror ? { of: spec.mirror.sourceAssetId } : void 0,
670
673
  revision: spec.revision ? {
671
674
  mode: spec.revision.mode,
672
675
  from: spec.revision.sourceAssetId,
@@ -678,9 +681,9 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
678
681
  );
679
682
  }
680
683
 
681
- // src/outputs.ts
682
- import { existsSync as existsSync2 } from "fs";
683
- import path3 from "path";
684
+ // src/pipeline/mirror.ts
685
+ import { mkdir as mkdir2, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "fs/promises";
686
+ import path4 from "path";
684
687
 
685
688
  // src/lock.ts
686
689
  import { readFile, writeFile, rename, mkdir, rmdir, rm, stat } from "fs/promises";
@@ -769,6 +772,16 @@ var CHARACTER_DIRECTIONS_8 = [
769
772
  "south-west"
770
773
  ];
771
774
  var CHARACTER_DIRECTIONS_4 = ["south", "west", "east", "north"];
775
+ var MIRRORED_DIRECTION = {
776
+ south: "south",
777
+ "south-east": "south-west",
778
+ east: "west",
779
+ "north-east": "north-west",
780
+ north: "north",
781
+ "north-west": "north-east",
782
+ west: "east",
783
+ "south-west": "south-east"
784
+ };
772
785
  var CharacterModeSchema = z.enum(["standard", "v3", "pro"]);
773
786
  var CharacterAnimationModeSchema = z.enum(["template", "v3", "pro"]);
774
787
  var CharacterStateSchema = z.object({
@@ -1028,8 +1041,8 @@ var InheritedStyleSchema = StyleObjectSchema.partial().extend({
1028
1041
  }).strict();
1029
1042
  var StyleInputSchema = z.union([StyleSchema, InheritedStyleSchema]);
1030
1043
  var AssetSchema = z.object({
1031
- /** The subject. Style wrapping comes from the style's prefix/suffix. */
1032
- prompt: z.string(),
1044
+ /** The subject. Style wrapping comes from the style's prefix/suffix. Only a `mirror` may leave it out. */
1045
+ prompt: z.string().optional(),
1033
1046
  /** Subdirectory under the style's outDir. Optional. */
1034
1047
  category: z.string().optional(),
1035
1048
  /** Overrides the style default. `map` generator only. */
@@ -1087,6 +1100,19 @@ var AssetSchema = z.object({
1087
1100
  state: CharacterStateSchema.optional(),
1088
1101
  /** `character` styles: this asset is a loop of another character asset in one direction. */
1089
1102
  animation: CharacterAnimationSchema.optional(),
1103
+ /**
1104
+ * Another asset of the same style flipped left to right, made locally
1105
+ * from that asset's downloaded files at no generation cost.
1106
+ *
1107
+ * The saving is in character loops, where each direction is its own
1108
+ * generation: a walk facing west mirrored is the walk facing east, and a
1109
+ * diagonal mirrors to the other diagonal. South and north mirror onto
1110
+ * themselves and are refused. A single image or a frame set from any
1111
+ * generator can be mirrored too; a tile set cannot, since its edges
1112
+ * carry meaning. Mirroring swaps handedness, so an asymmetric character
1113
+ * (a sword hand, a parted fringe) comes out the other way round.
1114
+ */
1115
+ mirror: z.string().min(1).optional(),
1090
1116
  /**
1091
1117
  * Generated output role to place in `cell` when this asset expands to
1092
1118
  * several files. Omit for ordinary single-output assets. A structural set
@@ -1126,7 +1152,7 @@ var AssetSchema = z.object({
1126
1152
  path: ["revision"]
1127
1153
  });
1128
1154
  }
1129
- const shapes = [asset.revision && "revision", asset.state && "state", asset.animation && "animation"].filter(Boolean);
1155
+ const shapes = [asset.revision && "revision", asset.state && "state", asset.animation && "animation", asset.mirror && "mirror"].filter(Boolean);
1130
1156
  if (shapes.length > 1) {
1131
1157
  context.addIssue({
1132
1158
  code: z.ZodIssueCode.custom,
@@ -1134,6 +1160,20 @@ var AssetSchema = z.object({
1134
1160
  path: [shapes[1]]
1135
1161
  });
1136
1162
  }
1163
+ if (asset.mirror && (asset.source || Object.keys(asset.sourceByStyle).length)) {
1164
+ context.addIssue({
1165
+ code: z.ZodIssueCode.custom,
1166
+ message: "a mirror is made from another asset's files; it cannot also declare a source",
1167
+ path: ["mirror"]
1168
+ });
1169
+ }
1170
+ if (asset.prompt === void 0 && !asset.mirror) {
1171
+ context.addIssue({
1172
+ code: z.ZodIssueCode.custom,
1173
+ message: "Required",
1174
+ path: ["prompt"]
1175
+ });
1176
+ }
1137
1177
  });
1138
1178
  var DEFAULT_HISTORY_LIMIT = 5;
1139
1179
  var HistoryLimitSchema = z.number().int().min(0).max(100);
@@ -1223,6 +1263,11 @@ var LockEntrySchema = z.object({
1223
1263
  maskSha256: z.string().regex(/^[0-9a-f]{64}$/).optional(),
1224
1264
  strength: z.number().min(0).max(1).optional()
1225
1265
  }).strict().nullable().default(null),
1266
+ /** For a mirror: the asset it flips and a hash over that asset's output hashes when this was made. */
1267
+ mirror: z.object({
1268
+ sourceAssetId: z.string().min(1),
1269
+ sourceSha256: z.string().regex(/^[0-9a-f]{64}$/)
1270
+ }).strict().nullable().default(null),
1226
1271
  /**
1227
1272
  * Output hashes owned by the previous generation while its replacement is
1228
1273
  * pending. They authorize replacing only unchanged PixelKiln-owned files.
@@ -1922,6 +1967,8 @@ function cacheFileName(hash, mediaType = MediaType.PNG) {
1922
1967
  }
1923
1968
 
1924
1969
  // src/outputs.ts
1970
+ import { existsSync as existsSync2 } from "fs";
1971
+ import path3 from "path";
1925
1972
  function portableOutputPath(file, manifestDir) {
1926
1973
  const root = path3.resolve(manifestDir);
1927
1974
  const absolute = path3.isAbsolute(file) ? path3.normalize(file) : path3.resolve(root, file);
@@ -2047,21 +2094,219 @@ function frameSetFps(entry) {
2047
2094
  return null;
2048
2095
  }
2049
2096
 
2097
+ // src/palette.ts
2098
+ function normalizePalette(values) {
2099
+ const colors = /* @__PURE__ */ new Map();
2100
+ for (const value of values) {
2101
+ const rgb = parseHex(value);
2102
+ const hex2 = `#${rgb.r.toString(16).padStart(2, "0")}${rgb.g.toString(16).padStart(2, "0")}${rgb.b.toString(16).padStart(2, "0")}`;
2103
+ if (!colors.has(hex2)) colors.set(hex2, { hex: hex2, ...rgb });
2104
+ }
2105
+ if (colors.size < 2 || colors.size > 256) {
2106
+ throw new Error(`A palette must contain 2\u2013256 unique colors; got ${colors.size}.`);
2107
+ }
2108
+ return [...colors.values()];
2109
+ }
2110
+ function colorDistance(a, b) {
2111
+ const rmean = (a.r + b.r) / 2;
2112
+ const dr = a.r - b.r;
2113
+ const dg = a.g - b.g;
2114
+ const db = a.b - b.b;
2115
+ return Math.sqrt(
2116
+ (512 + rmean) * dr * dr / 256 + 4 * dg * dg + (767 - rmean) * db * db / 256
2117
+ );
2118
+ }
2119
+ function quantize(png, palette) {
2120
+ const colors = normalizePalette(palette);
2121
+ const pixels = Buffer.from(png.pixels);
2122
+ const nearestOf = /* @__PURE__ */ new Map();
2123
+ let changed = 0;
2124
+ for (let i = 0; i < pixels.length; i += 4) {
2125
+ const alpha = pixels[i + 3];
2126
+ if (alpha === 0) {
2127
+ if (pixels[i] || pixels[i + 1] || pixels[i + 2]) changed++;
2128
+ pixels[i] = 0;
2129
+ pixels[i + 1] = 0;
2130
+ pixels[i + 2] = 0;
2131
+ continue;
2132
+ }
2133
+ const key = pixels[i] << 16 | pixels[i + 1] << 8 | pixels[i + 2];
2134
+ let nearest = nearestOf.get(key);
2135
+ if (!nearest) {
2136
+ const pixel = { r: pixels[i], g: pixels[i + 1], b: pixels[i + 2] };
2137
+ nearest = colors[0];
2138
+ let distance = colorDistance(pixel, nearest);
2139
+ for (let j = 1; j < colors.length; j++) {
2140
+ const candidate = colors[j];
2141
+ const candidateDistance = colorDistance(pixel, candidate);
2142
+ if (candidateDistance < distance) {
2143
+ nearest = candidate;
2144
+ distance = candidateDistance;
2145
+ }
2146
+ }
2147
+ nearestOf.set(key, nearest);
2148
+ }
2149
+ if (pixels[i] !== nearest.r || pixels[i + 1] !== nearest.g || pixels[i + 2] !== nearest.b) changed++;
2150
+ pixels[i] = nearest.r;
2151
+ pixels[i + 1] = nearest.g;
2152
+ pixels[i + 2] = nearest.b;
2153
+ }
2154
+ return { png: { width: png.width, height: png.height, pixels }, changed };
2155
+ }
2156
+ function quantizeToPalette(png, palette) {
2157
+ return quantize(png, palette).png;
2158
+ }
2159
+
2160
+ // src/pipeline/postprocess.ts
2161
+ function postprocessFor(spec) {
2162
+ if (!spec.enforcePalette || spec.palette.length < 2) return void 0;
2163
+ return {
2164
+ palette: {
2165
+ colors: normalizePalette(spec.palette).map((color) => color.hex),
2166
+ dither: "none",
2167
+ distance: "redmean"
2168
+ }
2169
+ };
2170
+ }
2171
+ function samePostprocess(a, b) {
2172
+ const pa = a?.palette;
2173
+ const pb = b?.palette;
2174
+ if (!pa && !pb) return true;
2175
+ if (!pa || !pb) return false;
2176
+ return pa.dither === pb.dither && pa.distance === pb.distance && pa.colors.length === pb.colors.length && pa.colors.every((color, i) => color === pb.colors[i]);
2177
+ }
2178
+ function postprocessCurrent(entry, spec) {
2179
+ return samePostprocess(entry.postprocess, postprocessFor(spec));
2180
+ }
2181
+ function applyPostprocess(bytes, mediaType, record) {
2182
+ if (!record?.palette || mediaType !== MediaType.PNG) return { bytes, changed: false };
2183
+ const png = decodePng(bytes);
2184
+ const { png: snapped, changed } = quantize(png, record.palette.colors);
2185
+ if (!changed) return { bytes, changed: false };
2186
+ return { bytes: encodeRgbaPng(snapped.width, snapped.height, snapped.pixels), changed: true };
2187
+ }
2188
+
2189
+ // src/pipeline/mirror.ts
2190
+ function mirrorSourceHash(source) {
2191
+ return sha256(JSON.stringify(source.outputs.map((output) => output.sha256)));
2192
+ }
2193
+ function mirrorCurrent(entry, source) {
2194
+ return Boolean(entry.mirror && source && entry.mirror.sourceSha256 === mirrorSourceHash(source));
2195
+ }
2196
+ function mirroredRole(role) {
2197
+ if (role && role in MIRRORED_DIRECTION) return MIRRORED_DIRECTION[role];
2198
+ return role;
2199
+ }
2200
+ function flipHorizontal(png) {
2201
+ const { width, height, pixels } = decodePng(png);
2202
+ const flipped = Buffer.alloc(pixels.length);
2203
+ const stride = width * 4;
2204
+ for (let y = 0; y < height; y++) {
2205
+ const row = y * stride;
2206
+ for (let x = 0; x < width; x++) {
2207
+ pixels.copy(flipped, row + (width - 1 - x) * 4, row + x * 4, row + x * 4 + 4);
2208
+ }
2209
+ }
2210
+ return encodeRgbaPng(width, height, flipped);
2211
+ }
2212
+ async function deriveMirror(spec, lock, lockPath, opts = {}) {
2213
+ const log2 = opts.onProgress ?? (() => {
2214
+ });
2215
+ const mirror = spec.mirror;
2216
+ const key = lockKey(spec.styleId, spec.assetId);
2217
+ const sourceKey = lockKey(spec.styleId, mirror.sourceAssetId);
2218
+ const source = lock.entries[sourceKey];
2219
+ if (!source || source.status !== "downloaded" || !source.outputs.length) {
2220
+ throw new Error(`mirror source ${sourceKey} has no downloaded outputs`);
2221
+ }
2222
+ const members = source.outputs.map((output, index) => ({
2223
+ file: currentEntryOutputPath(source, mirror.sourceSpec, index),
2224
+ role: mirroredRole(output.role)
2225
+ }));
2226
+ if (members.every((member) => member.role && CHARACTER_DIRECTIONS_8.includes(member.role))) {
2227
+ members.sort((a, b) => CHARACTER_DIRECTIONS_8.indexOf(a.role) - CHARACTER_DIRECTIONS_8.indexOf(b.role));
2228
+ }
2229
+ const outputs = [];
2230
+ const written = [];
2231
+ for (const [index, member] of members.entries()) {
2232
+ const bytes = flipHorizontal(await readFile2(member.file));
2233
+ const target = expectedOutputPath(spec, member.role, index, members.length, MediaType.PNG);
2234
+ await mkdir2(path4.dirname(target), { recursive: true });
2235
+ const tmp = `${target}.pixelkiln.tmp`;
2236
+ await writeFile2(tmp, bytes);
2237
+ await rename2(tmp, target);
2238
+ written.push(target);
2239
+ outputs.push({
2240
+ path: portableOutputPath(target, spec.root),
2241
+ sha256: sha256(bytes),
2242
+ ...member.role ? { role: member.role } : {},
2243
+ mediaType: MediaType.PNG
2244
+ });
2245
+ }
2246
+ const sourceMetadata = source.providerMetadata?.[source.provider] ?? {};
2247
+ const character = spec.character;
2248
+ const entry = upsert(lock, key, {
2249
+ styleId: spec.styleId,
2250
+ assetId: spec.assetId,
2251
+ specHash: spec.specHash,
2252
+ generator: spec.generator,
2253
+ tileFeature: null,
2254
+ prompt: "",
2255
+ width: spec.width,
2256
+ height: spec.height,
2257
+ revision: null,
2258
+ mirror: { sourceAssetId: mirror.sourceAssetId, sourceSha256: mirrorSourceHash(source) },
2259
+ supersededOutputs: [],
2260
+ jobId: null,
2261
+ submissionComplete: void 0,
2262
+ reviewObjectId: null,
2263
+ objectId: null,
2264
+ candidateIndex: null,
2265
+ status: "downloaded",
2266
+ error: null,
2267
+ outputs,
2268
+ providerMetadata: {
2269
+ [source.provider]: {
2270
+ ...sourceMetadata.frameSet ? { frameSet: sourceMetadata.frameSet } : {},
2271
+ ...character ? {
2272
+ character: {
2273
+ kind: character.kind,
2274
+ mirrorOf: mirror.sourceAssetId,
2275
+ ...character.animation ? { direction: character.animation.direction } : {}
2276
+ }
2277
+ } : {}
2278
+ }
2279
+ },
2280
+ sourceUrl: null,
2281
+ sourceUrls: [],
2282
+ submittedAt: (/* @__PURE__ */ new Date()).toISOString(),
2283
+ downloadedAt: (/* @__PURE__ */ new Date()).toISOString(),
2284
+ history: [],
2285
+ cost: 0,
2286
+ costUnit: spec.costUnit,
2287
+ provider: source.provider,
2288
+ postprocess: postprocessFor(spec)
2289
+ });
2290
+ await saveLock(lockPath, lock);
2291
+ log2(` mirror ${key} \u2190 ${sourceKey} (${written.length} file${written.length === 1 ? "" : "s"} flipped, no generation cost)`);
2292
+ return entry;
2293
+ }
2294
+
2050
2295
  // src/pipeline/quality-profile.ts
2051
2296
  import { existsSync as existsSync3 } from "fs";
2052
- import path6 from "path";
2297
+ import path7 from "path";
2053
2298
 
2054
2299
  // src/pipeline/refine.ts
2055
- import path5 from "path";
2300
+ import path6 from "path";
2056
2301
  import { execFile } from "child_process";
2057
- import { mkdtemp, readFile as readFile3, rm as rm3 } from "fs/promises";
2302
+ import { mkdtemp, readFile as readFile4, rm as rm3 } from "fs/promises";
2058
2303
  import { tmpdir } from "os";
2059
2304
  import { promisify } from "util";
2060
2305
 
2061
2306
  // src/artifacts.ts
2062
- import path4 from "path";
2307
+ import path5 from "path";
2063
2308
  import { createHash as createHash2, randomUUID } from "crypto";
2064
- import { mkdir as mkdir2, readFile as readFile2, rename as rename2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
2309
+ import { mkdir as mkdir3, readFile as readFile3, rename as rename3, rm as rm2, writeFile as writeFile3 } from "fs/promises";
2065
2310
  var activeTransactions = /* @__PURE__ */ new Set();
2066
2311
  function message(error) {
2067
2312
  return error instanceof Error ? error.message : String(error);
@@ -2070,7 +2315,7 @@ function digest(data) {
2070
2315
  return createHash2("sha256").update(data).digest("hex");
2071
2316
  }
2072
2317
  function portableRelative(from, to) {
2073
- return path4.relative(from, path4.resolve(to)).split(path4.sep).join("/") || ".";
2318
+ return path5.relative(from, path5.resolve(to)).split(path5.sep).join("/") || ".";
2074
2319
  }
2075
2320
  function canonical(value) {
2076
2321
  if (value === null || typeof value === "string" || typeof value === "boolean") return value;
@@ -2120,12 +2365,12 @@ function parseArtifactManifest(absolute, data) {
2120
2365
  return raw;
2121
2366
  }
2122
2367
  async function readArtifactBundleManifest(manifestPath) {
2123
- const absolute = path4.resolve(manifestPath);
2124
- return parseArtifactManifest(absolute, await readFile2(absolute, "utf8"));
2368
+ const absolute = path5.resolve(manifestPath);
2369
+ return parseArtifactManifest(absolute, await readFile3(absolute, "utf8"));
2125
2370
  }
2126
2371
  async function readOptional(file) {
2127
2372
  try {
2128
- return await readFile2(file);
2373
+ return await readFile3(file);
2129
2374
  } catch (error) {
2130
2375
  if (isCode(error, "ENOENT")) return null;
2131
2376
  throw error;
@@ -2144,8 +2389,8 @@ function processIsAlive(pid) {
2144
2389
  }
2145
2390
  }
2146
2391
  function validTemporaryPath(candidate, destination, type) {
2147
- return path4.dirname(candidate) === path4.dirname(destination) && path4.basename(candidate).startsWith(
2148
- `.${path4.basename(destination)}.pixelkiln-${type}-`
2392
+ return path5.dirname(candidate) === path5.dirname(destination) && path5.basename(candidate).startsWith(
2393
+ `.${path5.basename(destination)}.pixelkiln-${type}-`
2149
2394
  );
2150
2395
  }
2151
2396
  function parseTransaction(journal, data) {
@@ -2165,7 +2410,7 @@ async function removeTransactionFiles(journal) {
2165
2410
  await rm2(transactionMarker(journal), { force: true });
2166
2411
  }
2167
2412
  async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
2168
- const journal = path4.resolve(recoveryFile);
2413
+ const journal = path5.resolve(recoveryFile);
2169
2414
  const bytes = await readOptional(journal);
2170
2415
  if (!bytes) {
2171
2416
  await rm2(transactionMarker(journal), { force: true });
@@ -2181,7 +2426,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
2181
2426
  );
2182
2427
  }
2183
2428
  for (const entry of transaction.entries) {
2184
- if (typeof entry.destination !== "string" || typeof entry.stage !== "string" || typeof entry.sha256 !== "string" || entry.backup !== void 0 && typeof entry.backup !== "string" || !allowedDestinations.has(path4.resolve(entry.destination)) || !validTemporaryPath(entry.stage, entry.destination, "stage") || entry.backup !== void 0 && !validTemporaryPath(entry.backup, entry.destination, "backup")) {
2429
+ if (typeof entry.destination !== "string" || typeof entry.stage !== "string" || typeof entry.sha256 !== "string" || entry.backup !== void 0 && typeof entry.backup !== "string" || !allowedDestinations.has(path5.resolve(entry.destination)) || !validTemporaryPath(entry.stage, entry.destination, "stage") || entry.backup !== void 0 && !validTemporaryPath(entry.backup, entry.destination, "backup")) {
2185
2430
  throw new Error(
2186
2431
  `Refusing unsafe artifact recovery from ${journal}; its destinations or temporary paths do not match the current bundle.`
2187
2432
  );
@@ -2207,7 +2452,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
2207
2452
  }
2208
2453
  await remove2(entry.destination, errors);
2209
2454
  try {
2210
- await rename2(entry.backup, entry.destination);
2455
+ await rename3(entry.backup, entry.destination);
2211
2456
  } catch (error) {
2212
2457
  errors.push(
2213
2458
  `could not restore ${entry.destination}; previous file remains at ${entry.backup}: ${message(error)}`
@@ -2227,7 +2472,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
2227
2472
  await removeTransactionFiles(journal);
2228
2473
  }
2229
2474
  function createArtifactBundleManifest(manifestPath, outputs, provenance) {
2230
- const root = path4.dirname(path4.resolve(manifestPath));
2475
+ const root = path5.dirname(path5.resolve(manifestPath));
2231
2476
  const sources = provenance.sources.map((source) => ({
2232
2477
  ...source,
2233
2478
  path: portableRelative(root, source.path)
@@ -2257,11 +2502,11 @@ function withArtifactManifest(manifestPath, outputs, provenance) {
2257
2502
  ];
2258
2503
  }
2259
2504
  async function writeManagedArtifactBundle(manifestPath, outputs, provenance, options = {}) {
2260
- const absoluteManifest = path4.resolve(manifestPath);
2505
+ const absoluteManifest = path5.resolve(manifestPath);
2261
2506
  const recoveryFile = `${absoluteManifest}.transaction`;
2262
2507
  const allowedDestinations = /* @__PURE__ */ new Set([
2263
2508
  absoluteManifest,
2264
- ...outputs.map((output) => path4.resolve(output.path))
2509
+ ...outputs.map((output) => path5.resolve(output.path))
2265
2510
  ]);
2266
2511
  await recoverArtifactTransaction(recoveryFile, allowedDestinations);
2267
2512
  const existingManifestBytes = await readOptional(absoluteManifest);
@@ -2279,13 +2524,13 @@ async function writeManagedArtifactBundle(manifestPath, outputs, provenance, opt
2279
2524
  }
2280
2525
  }
2281
2526
  if (!options.force) {
2282
- const root = path4.dirname(absoluteManifest);
2527
+ const root = path5.dirname(absoluteManifest);
2283
2528
  const recorded = new Map(
2284
- previous?.outputs.map((output) => [path4.resolve(root, output.path), output.sha256]) ?? []
2529
+ previous?.outputs.map((output) => [path5.resolve(root, output.path), output.sha256]) ?? []
2285
2530
  );
2286
2531
  const conflicts = [];
2287
2532
  for (const output of outputs) {
2288
- const destination = path4.resolve(output.path);
2533
+ const destination = path5.resolve(output.path);
2289
2534
  const current = await readOptional(destination);
2290
2535
  if (!current || digest(current) === digest(output.data)) continue;
2291
2536
  const expected = recorded.get(destination);
@@ -2307,15 +2552,15 @@ Pass { force: true } (CLI: --force) to take ownership and replace the complete b
2307
2552
  );
2308
2553
  }
2309
2554
  async function verifyArtifactBundle(manifestPath) {
2310
- const absolute = path4.resolve(manifestPath);
2555
+ const absolute = path5.resolve(manifestPath);
2311
2556
  const raw = await readArtifactBundleManifest(absolute);
2312
- const root = path4.dirname(absolute);
2557
+ const root = path5.dirname(absolute);
2313
2558
  const changedSources = [];
2314
2559
  const changedOutputs = [];
2315
2560
  for (const source of raw.sources) {
2316
2561
  let actual = null;
2317
2562
  try {
2318
- actual = digest(await readFile2(path4.resolve(root, source.path)));
2563
+ actual = digest(await readFile3(path5.resolve(root, source.path)));
2319
2564
  } catch (error) {
2320
2565
  if (!isCode(error, "ENOENT")) throw error;
2321
2566
  }
@@ -2323,7 +2568,7 @@ async function verifyArtifactBundle(manifestPath) {
2323
2568
  }
2324
2569
  for (const output of raw.outputs) {
2325
2570
  try {
2326
- if (digest(await readFile2(path4.resolve(root, output.path))) !== output.sha256) {
2571
+ if (digest(await readFile3(path5.resolve(root, output.path))) !== output.sha256) {
2327
2572
  changedOutputs.push(output.path);
2328
2573
  }
2329
2574
  } catch (error) {
@@ -2362,7 +2607,7 @@ async function rollback(prepared) {
2362
2607
  for (const artifact of [...prepared].reverse()) {
2363
2608
  if (!artifact.backup || !artifact.backedUp) continue;
2364
2609
  try {
2365
- await rename2(artifact.backup, artifact.destination);
2610
+ await rename3(artifact.backup, artifact.destination);
2366
2611
  artifact.backedUp = false;
2367
2612
  } catch (error) {
2368
2613
  errors.push(
@@ -2379,7 +2624,7 @@ async function writeArtifactBundle(files, options = {}) {
2379
2624
  if (!files.length) throw new Error("An artifact bundle must contain at least one file.");
2380
2625
  const normalized = files.map((file) => {
2381
2626
  if (!file.path.trim()) throw new Error("Artifact paths cannot be empty.");
2382
- return { destination: path4.resolve(file.path), data: Buffer.from(file.data) };
2627
+ return { destination: path5.resolve(file.path), data: Buffer.from(file.data) };
2383
2628
  });
2384
2629
  const destinations = /* @__PURE__ */ new Set();
2385
2630
  for (const artifact of normalized) {
@@ -2395,7 +2640,7 @@ async function writeArtifactBundle(files, options = {}) {
2395
2640
  const unchanged = [];
2396
2641
  for (const artifact of normalized) {
2397
2642
  try {
2398
- const current = await readFile2(artifact.destination);
2643
+ const current = await readFile3(artifact.destination);
2399
2644
  if (current.equals(artifact.data)) {
2400
2645
  unchanged.push(artifact.destination);
2401
2646
  } else {
@@ -2409,21 +2654,21 @@ async function writeArtifactBundle(files, options = {}) {
2409
2654
  if (!changed.length) return { changed: [], unchanged };
2410
2655
  const token = randomUUID();
2411
2656
  for (const [index, artifact] of changed.entries()) {
2412
- const basename = path4.basename(artifact.destination);
2413
- artifact.stage = path4.join(
2414
- path4.dirname(artifact.destination),
2657
+ const basename = path5.basename(artifact.destination);
2658
+ artifact.stage = path5.join(
2659
+ path5.dirname(artifact.destination),
2415
2660
  `.${basename}.pixelkiln-stage-${token}-${index}`
2416
2661
  );
2417
2662
  if (artifact.existed) {
2418
- artifact.backup = path4.join(
2419
- path4.dirname(artifact.destination),
2663
+ artifact.backup = path5.join(
2664
+ path5.dirname(artifact.destination),
2420
2665
  `.${basename}.pixelkiln-backup-${token}-${index}`
2421
2666
  );
2422
2667
  }
2423
2668
  }
2424
- const journal = options.recoveryFile ? path4.resolve(options.recoveryFile) : null;
2669
+ const journal = options.recoveryFile ? path5.resolve(options.recoveryFile) : null;
2425
2670
  if (journal) {
2426
- await mkdir2(path4.dirname(journal), { recursive: true });
2671
+ await mkdir3(path5.dirname(journal), { recursive: true });
2427
2672
  const transaction = {
2428
2673
  format: "pixelkiln-artifact-transaction",
2429
2674
  version: 1,
@@ -2437,7 +2682,7 @@ async function writeArtifactBundle(files, options = {}) {
2437
2682
  }))
2438
2683
  };
2439
2684
  try {
2440
- await writeFile2(journal, JSON.stringify(transaction, null, 2) + "\n", { flag: "wx" });
2685
+ await writeFile3(journal, JSON.stringify(transaction, null, 2) + "\n", { flag: "wx" });
2441
2686
  activeTransactions.add(journal);
2442
2687
  } catch (error) {
2443
2688
  throw new Error(
@@ -2448,9 +2693,9 @@ async function writeArtifactBundle(files, options = {}) {
2448
2693
  }
2449
2694
  try {
2450
2695
  for (const [index, artifact] of changed.entries()) {
2451
- await mkdir2(path4.dirname(artifact.destination), { recursive: true });
2696
+ await mkdir3(path5.dirname(artifact.destination), { recursive: true });
2452
2697
  await options.beforeStage?.(artifact.destination, index);
2453
- await writeFile2(artifact.stage, artifact.data, { flag: "wx" });
2698
+ await writeFile3(artifact.stage, artifact.data, { flag: "wx" });
2454
2699
  }
2455
2700
  } catch (error) {
2456
2701
  const cleanupErrors2 = await rollback(changed);
@@ -2463,17 +2708,17 @@ async function writeArtifactBundle(files, options = {}) {
2463
2708
  try {
2464
2709
  for (const artifact of changed) {
2465
2710
  if (!artifact.existed) continue;
2466
- await rename2(artifact.destination, artifact.backup);
2711
+ await rename3(artifact.destination, artifact.backup);
2467
2712
  artifact.backedUp = true;
2468
2713
  }
2469
2714
  for (const [index, artifact] of changed.entries()) {
2470
2715
  await options.beforePromote?.(artifact.destination, index);
2471
- await rename2(artifact.stage, artifact.destination);
2716
+ await rename3(artifact.stage, artifact.destination);
2472
2717
  artifact.stage = void 0;
2473
2718
  artifact.promoted = true;
2474
2719
  }
2475
2720
  if (journal) {
2476
- await writeFile2(transactionMarker(journal), `${token}
2721
+ await writeFile3(transactionMarker(journal), `${token}
2477
2722
  `, { flag: "wx" });
2478
2723
  durableCommit = true;
2479
2724
  await options.afterCommit?.();
@@ -2515,69 +2760,6 @@ async function writeArtifactBundle(files, options = {}) {
2515
2760
  };
2516
2761
  }
2517
2762
 
2518
- // src/palette.ts
2519
- function normalizePalette(values) {
2520
- const colors = /* @__PURE__ */ new Map();
2521
- for (const value of values) {
2522
- const rgb = parseHex(value);
2523
- const hex2 = `#${rgb.r.toString(16).padStart(2, "0")}${rgb.g.toString(16).padStart(2, "0")}${rgb.b.toString(16).padStart(2, "0")}`;
2524
- if (!colors.has(hex2)) colors.set(hex2, { hex: hex2, ...rgb });
2525
- }
2526
- if (colors.size < 2 || colors.size > 256) {
2527
- throw new Error(`A palette must contain 2\u2013256 unique colors; got ${colors.size}.`);
2528
- }
2529
- return [...colors.values()];
2530
- }
2531
- function colorDistance(a, b) {
2532
- const rmean = (a.r + b.r) / 2;
2533
- const dr = a.r - b.r;
2534
- const dg = a.g - b.g;
2535
- const db = a.b - b.b;
2536
- return Math.sqrt(
2537
- (512 + rmean) * dr * dr / 256 + 4 * dg * dg + (767 - rmean) * db * db / 256
2538
- );
2539
- }
2540
- function quantize(png, palette) {
2541
- const colors = normalizePalette(palette);
2542
- const pixels = Buffer.from(png.pixels);
2543
- const nearestOf = /* @__PURE__ */ new Map();
2544
- let changed = 0;
2545
- for (let i = 0; i < pixels.length; i += 4) {
2546
- const alpha = pixels[i + 3];
2547
- if (alpha === 0) {
2548
- if (pixels[i] || pixels[i + 1] || pixels[i + 2]) changed++;
2549
- pixels[i] = 0;
2550
- pixels[i + 1] = 0;
2551
- pixels[i + 2] = 0;
2552
- continue;
2553
- }
2554
- const key = pixels[i] << 16 | pixels[i + 1] << 8 | pixels[i + 2];
2555
- let nearest = nearestOf.get(key);
2556
- if (!nearest) {
2557
- const pixel = { r: pixels[i], g: pixels[i + 1], b: pixels[i + 2] };
2558
- nearest = colors[0];
2559
- let distance = colorDistance(pixel, nearest);
2560
- for (let j = 1; j < colors.length; j++) {
2561
- const candidate = colors[j];
2562
- const candidateDistance = colorDistance(pixel, candidate);
2563
- if (candidateDistance < distance) {
2564
- nearest = candidate;
2565
- distance = candidateDistance;
2566
- }
2567
- }
2568
- nearestOf.set(key, nearest);
2569
- }
2570
- if (pixels[i] !== nearest.r || pixels[i + 1] !== nearest.g || pixels[i + 2] !== nearest.b) changed++;
2571
- pixels[i] = nearest.r;
2572
- pixels[i + 1] = nearest.g;
2573
- pixels[i + 2] = nearest.b;
2574
- }
2575
- return { png: { width: png.width, height: png.height, pixels }, changed };
2576
- }
2577
- function quantizeToPalette(png, palette) {
2578
- return quantize(png, palette).png;
2579
- }
2580
-
2581
2763
  // src/pipeline/refine.ts
2582
2764
  var execFileAsync = promisify(execFile);
2583
2765
  var PIXEL_ART_FIXER_MIN_SIDE = 16;
@@ -2777,16 +2959,16 @@ function sameFrameGrid(first, next) {
2777
2959
  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;
2778
2960
  }
2779
2961
  async function refineAsset(options) {
2780
- const source = path5.resolve(options.source);
2781
- const output = path5.resolve(options.output);
2962
+ const source = path6.resolve(options.source);
2963
+ const output = path6.resolve(options.output);
2782
2964
  if (source === output) throw new Error("Refinement source and output must be different files.");
2783
- const record = path5.resolve(qualityRecordPath(output));
2965
+ const record = path6.resolve(qualityRecordPath(output));
2784
2966
  const palette = normalizePalette(options.palette).map((color) => color.hex);
2785
2967
  const minGridConfidence = requireConfidence(options.minGridConfidence ?? "high");
2786
2968
  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.");
2787
2969
  let sourcePng;
2788
2970
  try {
2789
- sourcePng = decodePng(await readFile3(source));
2971
+ sourcePng = decodePng(await readFile4(source));
2790
2972
  } catch (error) {
2791
2973
  throw new Error(`Cannot read refinement source ${source}: ${message2(error)}`, { cause: error });
2792
2974
  }
@@ -2803,8 +2985,8 @@ async function refineAsset(options) {
2803
2985
  );
2804
2986
  }
2805
2987
  }
2806
- const temp = await mkdtemp(path5.join(tmpdir(), "pixelkiln-refine-"));
2807
- const recovered = path5.join(temp, "native.png");
2988
+ const temp = await mkdtemp(path6.join(tmpdir(), "pixelkiln-refine-"));
2989
+ const recovered = path6.join(temp, "native.png");
2808
2990
  const fixerCommand = compatibilityCommand ?? options.fixerPython ?? process.env.PIXELKILN_PIXEL_FIXER_PYTHON ?? "python3";
2809
2991
  const args = compatibilityCommand ? [...options.fixerArgsPrefix ?? [], source, "--extract", recovered] : ["-c", PYTHON_FIXER_BRIDGE, source, recovered];
2810
2992
  try {
@@ -2825,7 +3007,7 @@ async function refineAsset(options) {
2825
3007
  );
2826
3008
  }
2827
3009
  const detection = parseDetection(firstJsonObject(stdout));
2828
- const native = decodePng(await readFile3(recovered));
3010
+ const native = decodePng(await readFile4(recovered));
2829
3011
  const quantized = quantizeToPalette(native, palette);
2830
3012
  const png = encodeRgbaPng(quantized.width, quantized.height, quantized.pixels);
2831
3013
  const audit = auditRefinement(
@@ -2889,18 +3071,18 @@ async function refineFrameSet(options) {
2889
3071
  if (roles.some((role) => !role.trim()) || new Set(roles).size !== roles.length) {
2890
3072
  throw new Error("Frame-set roles must be non-empty and unique.");
2891
3073
  }
2892
- const output = path5.resolve(options.output);
2893
- const record = path5.resolve(qualityRecordPath(output));
3074
+ const output = path6.resolve(options.output);
3075
+ const record = path6.resolve(qualityRecordPath(output));
2894
3076
  const palette = normalizeRefinementPalette(options.palette);
2895
3077
  const finalOutputs = options.sources.map((source, index) => qualityFrameOutputPath(output, source.role, index));
2896
3078
  if (new Set(finalOutputs).size !== finalOutputs.length) {
2897
3079
  throw new Error("Frame-set roles resolve to duplicate output filenames.");
2898
3080
  }
2899
- const sourceSet = new Set(options.sources.map((source) => path5.resolve(source.path)));
3081
+ const sourceSet = new Set(options.sources.map((source) => path6.resolve(source.path)));
2900
3082
  if (finalOutputs.some((file) => sourceSet.has(file))) {
2901
3083
  throw new Error("Frame-set refinement sources and outputs must be different files.");
2902
3084
  }
2903
- const temp = await mkdtemp(path5.join(tmpdir(), "pixelkiln-refine-frames-"));
3085
+ const temp = await mkdtemp(path6.join(tmpdir(), "pixelkiln-refine-frames-"));
2904
3086
  try {
2905
3087
  const frames = [];
2906
3088
  const outputs = [];
@@ -2908,8 +3090,8 @@ async function refineFrameSet(options) {
2908
3090
  const sourcePaths = [];
2909
3091
  let firstOptions = null;
2910
3092
  for (const [index, sourceEntry] of options.sources.entries()) {
2911
- const source = path5.resolve(sourceEntry.path);
2912
- const temporaryOutput = path5.join(temp, `${String(index).padStart(2, "0")}.png`);
3093
+ const source = path6.resolve(sourceEntry.path);
3094
+ const temporaryOutput = path6.join(temp, `${String(index).padStart(2, "0")}.png`);
2913
3095
  const refined = await refineAsset({
2914
3096
  source,
2915
3097
  output: temporaryOutput,
@@ -2941,7 +3123,7 @@ async function refineFrameSet(options) {
2941
3123
  });
2942
3124
  outputs.push({
2943
3125
  path: finalOutputs[index],
2944
- data: await readFile3(temporaryOutput)
3126
+ data: await readFile4(temporaryOutput)
2945
3127
  });
2946
3128
  }
2947
3129
  const first = frames[0];
@@ -2984,7 +3166,7 @@ async function refineFrameSet(options) {
2984
3166
  );
2985
3167
  return {
2986
3168
  sources: sourcePaths,
2987
- outputs: outputs.map((item) => path5.resolve(item.path)),
3169
+ outputs: outputs.map((item) => path6.resolve(item.path)),
2988
3170
  record,
2989
3171
  frames,
2990
3172
  palette,
@@ -2995,12 +3177,12 @@ async function refineFrameSet(options) {
2995
3177
  }
2996
3178
  }
2997
3179
  async function checkQualityRecord(recordPath) {
2998
- const record = path5.resolve(recordPath);
3180
+ const record = path6.resolve(recordPath);
2999
3181
  const manifest = await readArtifactBundleManifest(record);
3000
3182
  const options = parseRecordOptions(manifest, record);
3001
3183
  const verification = await verifyArtifactBundle(record);
3002
- const sources = manifest.sources.map((source2) => path5.resolve(path5.dirname(record), source2.path));
3003
- const outputs = manifest.outputs.map((output2) => path5.resolve(path5.dirname(record), output2.path));
3184
+ const sources = manifest.sources.map((source2) => path6.resolve(path6.dirname(record), source2.path));
3185
+ const outputs = manifest.outputs.map((output2) => path6.resolve(path6.dirname(record), output2.path));
3004
3186
  const source = sources[0] ?? "";
3005
3187
  const output = outputs[0] ?? "";
3006
3188
  const expectedCount = options.frameSet?.count ?? 1;
@@ -3033,7 +3215,7 @@ async function checkQualityRecord(recordPath) {
3033
3215
  async function approveQualityRecord(recordPath, approval) {
3034
3216
  const reviewer = approval.reviewer.trim();
3035
3217
  if (!reviewer) throw new Error("A human reviewer name is required.");
3036
- const record = path5.resolve(recordPath);
3218
+ const record = path6.resolve(recordPath);
3037
3219
  const manifest = await readArtifactBundleManifest(record);
3038
3220
  const options = parseRecordOptions(manifest, record);
3039
3221
  const verification = await verifyArtifactBundle(record);
@@ -3054,14 +3236,14 @@ async function approveQualityRecord(recordPath, approval) {
3054
3236
  `A refinement record must contain exactly ${expectedCount} source(s) and output(s).`
3055
3237
  );
3056
3238
  }
3057
- const root = path5.dirname(record);
3239
+ const root = path6.dirname(record);
3058
3240
  const outputs = await Promise.all(manifest.outputs.map(async (entry) => {
3059
- const outputPath = path5.resolve(root, entry.path);
3060
- return { path: outputPath, data: await readFile3(outputPath) };
3241
+ const outputPath = path6.resolve(root, entry.path);
3242
+ return { path: outputPath, data: await readFile4(outputPath) };
3061
3243
  }));
3062
3244
  const sources = manifest.sources.map((source) => ({
3063
3245
  ...source,
3064
- path: path5.resolve(root, source.path)
3246
+ path: path6.resolve(root, source.path)
3065
3247
  }));
3066
3248
  const approved = {
3067
3249
  ...options,
@@ -3093,7 +3275,7 @@ async function qualitySource(spec, lock) {
3093
3275
  if (spec.generator === "frames") {
3094
3276
  return { items: null, reason: "frame-set quality requires ordered downloaded provider outputs" };
3095
3277
  }
3096
- const source = path6.resolve(spec.root, spec.source);
3278
+ const source = path7.resolve(spec.root, spec.source);
3097
3279
  return existsSync3(source) ? { items: [{ role: "source", path: source }] } : { items: null, reason: `declared source is missing: ${spec.source}` };
3098
3280
  }
3099
3281
  const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
@@ -3160,7 +3342,7 @@ async function inspectQualityProfile(spec, lock) {
3160
3342
  const expectedRevision = spec.quality.fixerRevision ?? PIXEL_ART_FIXER_REVISION;
3161
3343
  const expectedTransparency = spec.quality.minTransparency ?? null;
3162
3344
  const expectedFrameSet = spec.generator === "frames" ? { fps: spec.quality.fps ?? 12, count: source.items.length, roles: source.items.map((item) => item.role) } : null;
3163
- const contractChanged = JSON.stringify(check.sources.map((item) => path6.resolve(item))) !== JSON.stringify(sourcePaths.map((item) => path6.resolve(item))) || JSON.stringify(check.outputs.map((item) => path6.resolve(item))) !== JSON.stringify(expectedOutputs.map((item) => path6.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);
3345
+ const contractChanged = JSON.stringify(check.sources.map((item) => path7.resolve(item))) !== JSON.stringify(sourcePaths.map((item) => path7.resolve(item))) || JSON.stringify(check.outputs.map((item) => path7.resolve(item))) !== JSON.stringify(expectedOutputs.map((item) => path7.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);
3164
3346
  if (contractChanged) {
3165
3347
  return {
3166
3348
  key,
@@ -3322,43 +3504,21 @@ async function requireApprovedQualitySources(specs, lock, assetIds, outputRoles)
3322
3504
  return sources;
3323
3505
  }
3324
3506
 
3325
- // src/pipeline/postprocess.ts
3326
- function postprocessFor(spec) {
3327
- if (!spec.enforcePalette || spec.palette.length < 2) return void 0;
3328
- return {
3329
- palette: {
3330
- colors: normalizePalette(spec.palette).map((color) => color.hex),
3331
- dither: "none",
3332
- distance: "redmean"
3333
- }
3334
- };
3335
- }
3336
- function samePostprocess(a, b) {
3337
- const pa = a?.palette;
3338
- const pb = b?.palette;
3339
- if (!pa && !pb) return true;
3340
- if (!pa || !pb) return false;
3341
- return pa.dither === pb.dither && pa.distance === pb.distance && pa.colors.length === pb.colors.length && pa.colors.every((color, i) => color === pb.colors[i]);
3342
- }
3343
- function postprocessCurrent(entry, spec) {
3344
- return samePostprocess(entry.postprocess, postprocessFor(spec));
3345
- }
3346
- function applyPostprocess(bytes, mediaType, record) {
3347
- if (!record?.palette || mediaType !== MediaType.PNG) return { bytes, changed: false };
3348
- const png = decodePng(bytes);
3349
- const { png: snapped, changed } = quantize(png, record.palette.colors);
3350
- if (!changed) return { bytes, changed: false };
3351
- return { bytes: encodeRgbaPng(snapped.width, snapped.height, snapped.pixels), changed: true };
3352
- }
3353
-
3354
3507
  // src/pipeline/revision.ts
3355
3508
  import { existsSync as existsSync4 } from "fs";
3356
- import path7 from "path";
3509
+ import path8 from "path";
3357
3510
  async function inspectRevisionReadiness(spec, lock) {
3511
+ if (spec.mirror) return inspectMirror(spec, lock, /* @__PURE__ */ new Set());
3358
3512
  if (spec.character?.parentSpec) return inspectCharacterParent(spec, lock, /* @__PURE__ */ new Set());
3359
3513
  if (!spec.revision) return null;
3360
3514
  return inspectRevision(spec, lock, /* @__PURE__ */ new Set());
3361
3515
  }
3516
+ async function inspectMirror(spec, lock, seen) {
3517
+ const source = spec.mirror.sourceSpec;
3518
+ const label = `mirror source ${spec.styleId}/${spec.mirror.sourceAssetId}`;
3519
+ const dependency = await inspectParent(source, lock, seen);
3520
+ return dependency.ready ? { ready: true, reason: `${label} is current` } : { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
3521
+ }
3362
3522
  async function inspectCharacterParent(spec, lock, seen) {
3363
3523
  const character = spec.character;
3364
3524
  const parentSpec = character.parentSpec;
@@ -3412,6 +3572,10 @@ async function inspectParent(spec, lock, seen) {
3412
3572
  if (seen.has(key)) return { ready: false, reason: `revision dependency cycle at ${key}` };
3413
3573
  const nextSeen = new Set(seen);
3414
3574
  nextSeen.add(key);
3575
+ if (spec.mirror) {
3576
+ const inputs = await inspectMirror(spec, lock, nextSeen);
3577
+ if (!inputs.ready) return inputs;
3578
+ }
3415
3579
  if (spec.revision) {
3416
3580
  const inputs = await inspectRevision(spec, lock, nextSeen);
3417
3581
  if (!inputs.ready) return inputs;
@@ -3428,7 +3592,7 @@ async function inspectParent(spec, lock, seen) {
3428
3592
  };
3429
3593
  }
3430
3594
  if (spec.source) {
3431
- const source = path7.resolve(spec.root, spec.source);
3595
+ const source = path8.resolve(spec.root, spec.source);
3432
3596
  return existsSync4(source) ? { ready: true, reason: "declared source is present" } : { ready: false, reason: `declared source is missing: ${spec.source}` };
3433
3597
  }
3434
3598
  const entry = lock.entries[key];
@@ -3498,7 +3662,7 @@ async function buildPlan(specs, lock, opts = {}) {
3498
3662
  state = "missing";
3499
3663
  reason = "--force";
3500
3664
  } else if (!entry && spec.source && spec.generator !== "frames") {
3501
- if (existsSync5(path8.resolve(spec.root, spec.source))) {
3665
+ if (existsSync5(path9.resolve(spec.root, spec.source))) {
3502
3666
  state = "ok";
3503
3667
  reason = `placed from ${spec.source}; not generated`;
3504
3668
  } else {
@@ -3511,11 +3675,17 @@ async function buildPlan(specs, lock, opts = {}) {
3511
3675
  reason = "file on disk, no provenance recorded";
3512
3676
  } else {
3513
3677
  state = "missing";
3514
- reason = "not in lockfile";
3678
+ reason = spec.mirror ? `not made yet; flipped from ${spec.mirror.sourceAssetId} (no generation cost)` : "not in lockfile";
3515
3679
  }
3516
3680
  } else if (entry.specHash !== spec.specHash) {
3517
3681
  state = "stale";
3518
3682
  reason = "prompt, size, or style changed";
3683
+ } else if (spec.mirror && !mirrorCurrent(entry, lock.entries[lockKey(spec.styleId, spec.mirror.sourceAssetId)])) {
3684
+ state = "stale";
3685
+ reason = `mirror source ${spec.mirror.sourceAssetId} changed; flip it again (no generation cost)`;
3686
+ } else if (spec.mirror && entry.status === "downloaded" && entry.outputs.some((_output, index) => !existsSync5(currentEntryOutputPath(entry, spec, index)))) {
3687
+ state = "stale";
3688
+ reason = "mirror output missing on disk; flip it again (no generation cost)";
3519
3689
  } else if (entry.status === "download-failed") {
3520
3690
  state = "recoverable";
3521
3691
  const force = entry.error?.includes("pass --force") ? " --force" : "";
@@ -3544,6 +3714,9 @@ async function buildPlan(specs, lock, opts = {}) {
3544
3714
  } else if (await anyOutputModified(entry, spec)) {
3545
3715
  state = "orphaned";
3546
3716
  reason = "output modified since download";
3717
+ } else if (spec.mirror && !postprocessCurrent(entry, spec)) {
3718
+ state = "stale";
3719
+ reason = "palette enforcement changed; flip the source again once it is refreshed (no generation cost)";
3547
3720
  } else if (!postprocessCurrent(entry, spec)) {
3548
3721
  state = "recoverable";
3549
3722
  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)";
@@ -3555,15 +3728,19 @@ async function buildPlan(specs, lock, opts = {}) {
3555
3728
  items.push({ spec, key, state, reason, ...quality ? { quality } : {} });
3556
3729
  }
3557
3730
  const isActionable = (i) => i.state === "missing" || i.state === "stale" || i.state === "failed";
3558
- const actionableKeys = new Set(items.filter(isActionable).map((i) => i.key));
3731
+ const stateOf = new Map(items.map((i) => [i.key, i.state]));
3559
3732
  for (const item of items) {
3560
3733
  if (!isActionable(item)) continue;
3561
- const parent = item.spec.revision?.sourceSpec ?? item.spec.character?.parentSpec;
3734
+ const parent = item.spec.mirror?.sourceSpec ?? item.spec.revision?.sourceSpec ?? item.spec.character?.parentSpec;
3562
3735
  if (!parent) continue;
3563
3736
  const parentKey = lockKey(parent.styleId, parent.assetId);
3564
- if (actionableKeys.has(parentKey)) {
3737
+ const parentState = stateOf.get(parentKey);
3738
+ if (parentState && isActionable({ state: parentState })) {
3565
3739
  item.state = "blocked";
3566
3740
  item.reason = `parent ${parentKey} is being generated in this run; this follows in the next wave`;
3741
+ } else if (item.spec.mirror && (parentState === "recoverable" || parentState === "in-flight")) {
3742
+ item.state = "blocked";
3743
+ item.reason = `mirror source ${parentKey} is ${parentState}; this follows in the next wave`;
3567
3744
  }
3568
3745
  }
3569
3746
  const actionable = items.filter(isActionable);
@@ -3718,12 +3895,12 @@ async function confirm(question, auto) {
3718
3895
  }
3719
3896
 
3720
3897
  // src/cli/commands/account.ts
3721
- import path21 from "path";
3898
+ import path22 from "path";
3722
3899
  import { existsSync as existsSync16 } from "fs";
3723
3900
 
3724
3901
  // src/env.ts
3725
3902
  import { readFileSync, existsSync as existsSync6 } from "fs";
3726
- import path9 from "path";
3903
+ import path10 from "path";
3727
3904
  function loadEnvFiles(dir) {
3728
3905
  const loaded = [];
3729
3906
  for (const [file, vars] of envFiles(dir)) {
@@ -3746,7 +3923,7 @@ function readEnvFiles(dir) {
3746
3923
  function envFiles(dir) {
3747
3924
  const out = [];
3748
3925
  for (const name of [".env.local", ".env"]) {
3749
- const file = path9.join(dir, name);
3926
+ const file = path10.join(dir, name);
3750
3927
  if (!existsSync6(file)) continue;
3751
3928
  try {
3752
3929
  out.push([file, parseEnv(readFileSync(file, "utf8"))]);
@@ -3776,8 +3953,8 @@ function parseEnv(contents) {
3776
3953
 
3777
3954
  // src/providers/comfyui.ts
3778
3955
  import { randomUUID as randomUUID2 } from "crypto";
3779
- import { readFile as readFile4 } from "fs/promises";
3780
- import path10 from "path";
3956
+ import { readFile as readFile5 } from "fs/promises";
3957
+ import path11 from "path";
3781
3958
 
3782
3959
  // src/http.ts
3783
3960
  var MAX_RETRIES = 4;
@@ -3964,10 +4141,10 @@ var ComfyUIProvider = class _ComfyUIProvider {
3964
4141
  }
3965
4142
  async resolveOptions(value, context) {
3966
4143
  const options = parseOptions(value);
3967
- const workflowPath = path10.resolve(context.root, options.workflowFile);
4144
+ const workflowPath = path11.resolve(context.root, options.workflowFile);
3968
4145
  let raw;
3969
4146
  try {
3970
- raw = await readFile4(workflowPath, "utf8");
4147
+ raw = await readFile5(workflowPath, "utf8");
3971
4148
  } catch (err) {
3972
4149
  throw new Error(
3973
4150
  `ComfyUI workflow for style "${context.styleId}" could not be read at ${workflowPath}: ${err instanceof Error ? err.message : String(err)}`
@@ -4019,10 +4196,10 @@ var ComfyUIProvider = class _ComfyUIProvider {
4019
4196
  if (typeof item !== "string") {
4020
4197
  throw new Error(`ComfyUI ${label} must be a manifest-relative PNG or JPEG path`);
4021
4198
  }
4022
- const file = path10.resolve(context.root, item);
4199
+ const file = path11.resolve(context.root, item);
4023
4200
  let bytes;
4024
4201
  try {
4025
- bytes = await readFile4(file);
4202
+ bytes = await readFile5(file);
4026
4203
  } catch (error) {
4027
4204
  throw new Error(
4028
4205
  `ComfyUI ${label} could not be read at ${file}: ${error instanceof Error ? error.message : String(error)}`
@@ -4154,7 +4331,7 @@ var ComfyUIProvider = class _ComfyUIProvider {
4154
4331
  setBinding(workflow2, binding, input);
4155
4332
  return;
4156
4333
  }
4157
- const bytes = await readFile4(input.path);
4334
+ const bytes = await readFile5(input.path);
4158
4335
  if (sha256(bytes) !== input.sha256) {
4159
4336
  throw new Error(`ComfyUI provider input "${name}" changed before upload`);
4160
4337
  }
@@ -4221,7 +4398,7 @@ var ComfyUIProvider = class _ComfyUIProvider {
4221
4398
  if (!spec.revision.sourceSha256 || !spec.revision.sourceFormat) {
4222
4399
  throw new Error("ComfyUI revision source is not ready");
4223
4400
  }
4224
- const sourceBytes = await readFile4(spec.revision.sourceFile);
4401
+ const sourceBytes = await readFile5(spec.revision.sourceFile);
4225
4402
  if (sha256(sourceBytes) !== spec.revision.sourceSha256) {
4226
4403
  throw new Error("ComfyUI revision source changed before upload");
4227
4404
  }
@@ -4235,7 +4412,7 @@ var ComfyUIProvider = class _ComfyUIProvider {
4235
4412
  if (!spec.revision.maskSha256 || !spec.revision.maskFormat) {
4236
4413
  throw new Error("ComfyUI revision mask is not ready");
4237
4414
  }
4238
- const maskBytes = await readFile4(spec.revision.maskFile);
4415
+ const maskBytes = await readFile5(spec.revision.maskFile);
4239
4416
  if (sha256(maskBytes) !== spec.revision.maskSha256) {
4240
4417
  throw new Error("ComfyUI revision mask changed before upload");
4241
4418
  }
@@ -4709,7 +4886,7 @@ function canonical2(value) {
4709
4886
  import { mkdirSync, existsSync as existsSync7, readFileSync as readFileSync2, writeFileSync } from "fs";
4710
4887
  import { randomUUID as randomUUID3 } from "crypto";
4711
4888
  import os from "os";
4712
- import path11 from "path";
4889
+ import path12 from "path";
4713
4890
 
4714
4891
  // src/client.ts
4715
4892
  import { z as z2 } from "zod";
@@ -4848,9 +5025,9 @@ var PixelLabClient = class {
4848
5025
  apiKey;
4849
5026
  http;
4850
5027
  /** Retries transport failures, 429, and 5xx; see http.ts for the policy. */
4851
- async request(path44, init) {
5028
+ async request(path45, init) {
4852
5029
  const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
4853
- const res = await this.http(`${BASE}${path44}`, {
5030
+ const res = await this.http(`${BASE}${path45}`, {
4854
5031
  ...init,
4855
5032
  headers: {
4856
5033
  Authorization: auth,
@@ -4860,13 +5037,13 @@ var PixelLabClient = class {
4860
5037
  });
4861
5038
  const text = await res.text();
4862
5039
  if (!res.ok) {
4863
- throw new PixelLabError(`${init?.method ?? "GET"} ${path44} \u2192 ${res.status}`, res.status, text);
5040
+ throw new PixelLabError(`${init?.method ?? "GET"} ${path45} \u2192 ${res.status}`, res.status, text);
4864
5041
  }
4865
5042
  if (!text) return {};
4866
5043
  try {
4867
5044
  return JSON.parse(text);
4868
5045
  } catch {
4869
- throw new Error(`${init?.method ?? "GET"} ${path44} returned invalid JSON`);
5046
+ throw new Error(`${init?.method ?? "GET"} ${path45} returned invalid JSON`);
4870
5047
  }
4871
5048
  }
4872
5049
  async balance() {
@@ -5051,10 +5228,10 @@ var PixelLabClient = class {
5051
5228
  async createCharacter(args) {
5052
5229
  const imageSize = { width: args.size, height: args.size };
5053
5230
  const palette = args.paletteSwatchBase64 ? { color_image: { type: "base64", base64: args.paletteSwatchBase64, format: "png" }, force_colors: true } : {};
5054
- let path44;
5231
+ let path45;
5055
5232
  let body;
5056
5233
  if (args.mode === "v3") {
5057
- path44 = "/create-character-v3";
5234
+ path45 = "/create-character-v3";
5058
5235
  body = {
5059
5236
  description: args.description,
5060
5237
  image_size: imageSize,
@@ -5066,7 +5243,7 @@ var PixelLabClient = class {
5066
5243
  ...args.seed != null ? { seed: args.seed } : {}
5067
5244
  };
5068
5245
  } else if (args.mode === "pro") {
5069
- path44 = "/create-character-pro";
5246
+ path45 = "/create-character-pro";
5070
5247
  body = {
5071
5248
  description: args.description,
5072
5249
  image_size: imageSize,
@@ -5076,7 +5253,7 @@ var PixelLabClient = class {
5076
5253
  ...args.seed != null ? { seed: args.seed } : {}
5077
5254
  };
5078
5255
  } else {
5079
- path44 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
5256
+ path45 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
5080
5257
  body = {
5081
5258
  description: args.description,
5082
5259
  image_size: imageSize,
@@ -5089,8 +5266,8 @@ var PixelLabClient = class {
5089
5266
  ...palette
5090
5267
  };
5091
5268
  }
5092
- const raw = await this.request(path44, { method: "POST", body: JSON.stringify(body) });
5093
- return validateResponse(CharacterSubmitSchema, raw, path44.slice(1));
5269
+ const raw = await this.request(path45, { method: "POST", body: JSON.stringify(body) });
5270
+ return validateResponse(CharacterSubmitSchema, raw, path45.slice(1));
5094
5271
  }
5095
5272
  /** A text edit of an existing character, applied to every direction and stored as a sibling. */
5096
5273
  async createCharacterState(args) {
@@ -5278,7 +5455,7 @@ var PixelLabProvider = class _PixelLabProvider {
5278
5455
  * filename, polling is an existence check, and downloading is a file read.
5279
5456
  */
5280
5457
  static cacheDir() {
5281
- const dir = path11.join(os.tmpdir(), "pixelkiln-pixflux");
5458
+ const dir = path12.join(os.tmpdir(), "pixelkiln-pixflux");
5282
5459
  mkdirSync(dir, { recursive: true });
5283
5460
  return dir;
5284
5461
  }
@@ -5448,12 +5625,7 @@ var PixelLabProvider = class _PixelLabProvider {
5448
5625
  const character = await this.client.getCharacter(jobId);
5449
5626
  if (character.status === "failed") return { status: "failed", error: "character generation failed upstream" };
5450
5627
  if (character.status !== "completed" || !character.rotation_urls) return { status: "processing" };
5451
- const order = character.directions === 4 ? CHARACTER_DIRECTIONS_4 : CHARACTER_DIRECTIONS_8;
5452
- const sources = [];
5453
- for (const direction of order) {
5454
- const url = character.rotation_urls[direction];
5455
- if (url) sources.push({ url, role: direction });
5456
- }
5628
+ const sources = rotationSources(character);
5457
5629
  if (!sources.length) return { status: "failed", error: "character completed with no rotation images" };
5458
5630
  return {
5459
5631
  status: "ready",
@@ -5541,7 +5713,7 @@ var PixelLabProvider = class _PixelLabProvider {
5541
5713
  seed: spec.seed
5542
5714
  });
5543
5715
  const jobId = randomUUID3();
5544
- writeFileSync(path11.join(_PixelLabProvider.cacheDir(), `${jobId}.png`), png);
5716
+ writeFileSync(path12.join(_PixelLabProvider.cacheDir(), `${jobId}.png`), png);
5545
5717
  return { jobId };
5546
5718
  }
5547
5719
  if (spec.generator === "tiles") {
@@ -5582,7 +5754,7 @@ var PixelLabProvider = class _PixelLabProvider {
5582
5754
  }
5583
5755
  async poll(jobId, generator, context) {
5584
5756
  if (generator === "pixflux") {
5585
- const file = path11.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
5757
+ const file = path12.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
5586
5758
  if (existsSync7(file)) {
5587
5759
  const sourceUrl = `file://${file}`;
5588
5760
  return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }] };
@@ -5745,6 +5917,27 @@ var PixelLabProvider = class _PixelLabProvider {
5745
5917
  })))
5746
5918
  };
5747
5919
  }
5920
+ /**
5921
+ * A character's rotation and frame URLs carry its last edit time as a
5922
+ * cache-buster, so the record's URLs can name bytes a cache still holds
5923
+ * after an edit in PixelLab's editor. Objects keep stable URLs.
5924
+ */
5925
+ async refreshSources(objectId, context) {
5926
+ if (context.generator !== "character") return void 0;
5927
+ const [characterId, groupId] = objectId.split("#");
5928
+ const character = await this.client.getCharacter(characterId).catch((err) => {
5929
+ if (err instanceof PixelLabError && err.status === 404) return null;
5930
+ throw err;
5931
+ });
5932
+ if (!character) return null;
5933
+ if (!groupId) return character.status === "completed" ? rotationSources(character) : void 0;
5934
+ const recorded = context.metadata?.character ?? {};
5935
+ const direction = recorded.direction ?? "south";
5936
+ const group = character.animations.find((g) => g.animation_group_id === groupId);
5937
+ const frames = group?.directions.find((d) => d.direction === direction && d.frames.length)?.frames ?? (recorded.animationName ? findAnimation(character, recorded.animationName, direction, recorded.animationId ?? null)?.frames : void 0);
5938
+ if (!frames) return null;
5939
+ return frames.map((url, index) => ({ url, role: `frame-${String(index).padStart(2, "0")}` }));
5940
+ }
5748
5941
  };
5749
5942
  function remoteCharacter(character) {
5750
5943
  return {
@@ -5775,6 +5968,15 @@ function firstUrl(urls) {
5775
5968
  if (!urls) return null;
5776
5969
  return Object.values(urls).find((u) => typeof u === "string") ?? null;
5777
5970
  }
5971
+ function rotationSources(character) {
5972
+ const order = character.directions === 4 ? CHARACTER_DIRECTIONS_4 : CHARACTER_DIRECTIONS_8;
5973
+ const sources = [];
5974
+ for (const direction of order) {
5975
+ const url = character.rotation_urls?.[direction];
5976
+ if (url) sources.push({ url, role: direction });
5977
+ }
5978
+ return sources;
5979
+ }
5778
5980
  function findAnimation(character, name, direction, animationId) {
5779
5981
  for (const group of character.animations) {
5780
5982
  const match = group.directions.find((d) => d.direction === direction && d.frames.length > 0);
@@ -5836,9 +6038,9 @@ var RetroDiffusionClient = class {
5836
6038
  if (!response.ok) throw new ProviderError("retrodiffusion", `Retro Diffusion download failed (${response.status})`, { status: response.status });
5837
6039
  return Buffer.from(await response.arrayBuffer());
5838
6040
  }
5839
- async call(path44, init = {}) {
6041
+ async call(path45, init = {}) {
5840
6042
  if (!this.token) throw new Error("RD_API_KEY is not set");
5841
- const response = await this.request(`${this.baseUrl}${path44}`, {
6043
+ const response = await this.request(`${this.baseUrl}${path45}`, {
5842
6044
  ...init,
5843
6045
  headers: {
5844
6046
  "Content-Type": "application/json",
@@ -6695,13 +6897,13 @@ registerProvider({
6695
6897
  });
6696
6898
 
6697
6899
  // src/manifest.ts
6698
- import { readFile as readFile5 } from "fs/promises";
6900
+ import { readFile as readFile6 } from "fs/promises";
6699
6901
  import { existsSync as existsSync8 } from "fs";
6700
- import path12 from "path";
6902
+ import path13 from "path";
6701
6903
  async function loadManifest(manifestPath) {
6702
- const abs = path12.resolve(manifestPath);
6904
+ const abs = path13.resolve(manifestPath);
6703
6905
  if (!existsSync8(abs)) throw new ProjectError(`No manifest at ${abs}`);
6704
- const input = ManifestInputSchema.safeParse(JSON.parse(await readFile5(abs, "utf8")));
6906
+ const input = ManifestInputSchema.safeParse(JSON.parse(await readFile6(abs, "utf8")));
6705
6907
  if (!input.success) {
6706
6908
  const issues = formatManifestIssues(input.error.issues);
6707
6909
  throw new ProjectError(`Manifest at ${abs} is invalid:
@@ -6786,7 +6988,7 @@ ${issues}`);
6786
6988
  throw new ProjectError(`Manifest at ${abs} is invalid:
6787
6989
  ${unknownReferences.map((i) => ` ${i}`).join("\n")}`);
6788
6990
  }
6789
- return { manifest: parsed.data, root: path12.dirname(abs), path: abs };
6991
+ return { manifest: parsed.data, root: path13.dirname(abs), path: abs };
6790
6992
  }
6791
6993
  function parentAssetId(asset) {
6792
6994
  return asset?.revision?.from ?? asset?.state?.of ?? asset?.animation?.of;
@@ -6886,11 +7088,11 @@ async function resolveSpecs(loaded, filter) {
6886
7088
  }
6887
7089
  const styleImageCache = /* @__PURE__ */ new Map();
6888
7090
  async function loadStyleImage(rel) {
6889
- const abs = path12.resolve(root, rel);
7091
+ const abs = path13.resolve(root, rel);
6890
7092
  let hit = styleImageCache.get(abs);
6891
7093
  if (!hit) {
6892
7094
  if (!existsSync8(abs)) throw new Error(`Style image not found: ${abs}`);
6893
- const buf = await readFile5(abs);
7095
+ const buf = await readFile6(abs);
6894
7096
  const metadata = imageMetadata(buf);
6895
7097
  if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${abs}`);
6896
7098
  if (metadata.width < 1 || metadata.height < 1) {
@@ -6916,7 +7118,7 @@ async function resolveSpecs(loaded, filter) {
6916
7118
  styleImageDimensions.push(loadedImage);
6917
7119
  }
6918
7120
  const resolvedImages = style.styleImages.map((image) => {
6919
- const hit = styleImageCache.get(path12.resolve(root, image.path));
7121
+ const hit = styleImageCache.get(path13.resolve(root, image.path));
6920
7122
  return { base64: hit.base64, width: hit.width, height: hit.height, format: hit.format };
6921
7123
  });
6922
7124
  const styleSpecs = /* @__PURE__ */ new Map();
@@ -6969,11 +7171,11 @@ async function resolveSpecs(loaded, filter) {
6969
7171
  );
6970
7172
  }
6971
7173
  const characterKind = asset.animation ? "animation" : asset.state ? "state" : "base";
6972
- const subject = asset.promptByStyle[styleId] ?? asset.prompt;
7174
+ const subject = asset.promptByStyle[styleId] ?? asset.prompt ?? "";
6973
7175
  const prompt = generator === "character" && characterKind !== "base" ? subject.trim() : [style.promptPrefix, subject, style.promptSuffix].map((p) => p.trim()).filter(Boolean).join(", ");
6974
- const relFile = asset.file ?? path12.join(asset.category ?? "", `${assetId}.png`);
6975
- const outFile = path12.resolve(root, style.outDir, relFile);
6976
- const qualityOutFile = style.quality ? path12.resolve(root, style.quality.outDir, pngPath(relFile)) : void 0;
7176
+ const relFile = asset.file ?? path13.join(asset.category ?? "", `${assetId}.png`);
7177
+ const outFile = path13.resolve(root, style.outDir, relFile);
7178
+ const qualityOutFile = style.quality ? path13.resolve(root, style.quality.outDir, pngPath(relFile)) : void 0;
6977
7179
  const tileSize = generator === "tiles" ? size : style.tileSize ?? 32;
6978
7180
  const tileVariations = tileFeatureOutputCount(generator === "tiles" ? style.tileFeature : void 0) ?? tileVariationCount(countNumberedDescriptions(prompt));
6979
7181
  const base = {
@@ -7026,7 +7228,7 @@ async function resolveSpecs(loaded, filter) {
7026
7228
  minGridConfidence: style.quality.minGridConfidence,
7027
7229
  ...style.quality.minTransparency == null ? {} : { minTransparency: style.quality.minTransparency },
7028
7230
  ...style.quality.fixerRevision ? { fixerRevision: style.quality.fixerRevision } : {},
7029
- ...style.quality.fixerPython ? { fixerPython: path12.resolve(root, style.quality.fixerPython) } : {},
7231
+ ...style.quality.fixerPython ? { fixerPython: path13.resolve(root, style.quality.fixerPython) } : {},
7030
7232
  ...style.quality.fps == null ? {} : { fps: style.quality.fps }
7031
7233
  }
7032
7234
  } : {},
@@ -7044,11 +7246,42 @@ async function resolveSpecs(loaded, filter) {
7044
7246
  const resolved = styleSpecs.get(assetId);
7045
7247
  if (!resolved) {
7046
7248
  throw new Error(
7047
- `Asset "${assetId}" is not available in style "${styleId}"; revision parents must participate in the same style.`
7249
+ `Asset "${assetId}" is not available in style "${styleId}"; revision parents, character parents, and mirror sources must be in the same style.`
7048
7250
  );
7049
7251
  }
7050
7252
  if (finalized.has(assetId)) return resolved;
7051
7253
  const asset = manifest.assets[assetId];
7254
+ if (asset.mirror) {
7255
+ if (asset.mirror === assetId) throw new Error(`assets.${assetId}: an asset cannot mirror itself`);
7256
+ const sourceSpec = await finalize(asset.mirror);
7257
+ if (sourceSpec.generator === "tiles") {
7258
+ throw new Error(`assets.${assetId}: a tile set cannot be mirrored; its edges carry meaning`);
7259
+ }
7260
+ if (sourceSpec.character?.kind === "animation") {
7261
+ const facing = sourceSpec.character.animation.direction;
7262
+ const mirrored = MIRRORED_DIRECTION[facing];
7263
+ if (mirrored === facing) {
7264
+ throw new Error(
7265
+ `assets.${assetId}: mirroring ${asset.mirror} gives another ${facing}-facing loop; mirror a loop facing east, west, or a diagonal`
7266
+ );
7267
+ }
7268
+ resolved.character = { ...sourceSpec.character, animation: { ...sourceSpec.character.animation, direction: mirrored } };
7269
+ } else if (sourceSpec.character) {
7270
+ resolved.character = { ...sourceSpec.character };
7271
+ }
7272
+ resolved.mirror = { sourceAssetId: asset.mirror, sourceSpec };
7273
+ resolved.generator = sourceSpec.generator;
7274
+ resolved.width = sourceSpec.width;
7275
+ resolved.height = sourceSpec.height;
7276
+ resolved.size = sourceSpec.size;
7277
+ resolved.prompt = "";
7278
+ resolved.specHash = specHash(resolved, styleImageHashes, providerOptionIdentity, providerInputIdentities.get(assetId));
7279
+ resolved.cost = 0;
7280
+ resolved.costUnit = sourceSpec.costUnit;
7281
+ resolved.candidates = 1;
7282
+ finalized.add(assetId);
7283
+ return resolved;
7284
+ }
7052
7285
  const characterParent = asset.state?.of ?? asset.animation?.of;
7053
7286
  if (resolved.character && characterParent) {
7054
7287
  const parentSpec = await finalize(characterParent);
@@ -7072,9 +7305,9 @@ async function resolveSpecs(loaded, filter) {
7072
7305
  );
7073
7306
  }
7074
7307
  const sourceSpec = await finalize(asset.revision.from);
7075
- const sourceFile = sourceSpec.quality?.outFile ?? (sourceSpec.source ? path12.resolve(root, sourceSpec.source) : sourceSpec.outFile);
7308
+ const sourceFile = sourceSpec.quality?.outFile ?? (sourceSpec.source ? path13.resolve(root, sourceSpec.source) : sourceSpec.outFile);
7076
7309
  const sourceImage = await optionalRevisionImage(sourceFile, "revision source");
7077
- const maskFile = asset.revision.mask ? path12.resolve(root, asset.revision.mask) : void 0;
7310
+ const maskFile = asset.revision.mask ? path13.resolve(root, asset.revision.mask) : void 0;
7078
7311
  const maskImage = maskFile ? await optionalRevisionImage(maskFile, "revision mask", "png") : null;
7079
7312
  if (sourceImage && maskImage && (sourceImage.width !== maskImage.width || sourceImage.height !== maskImage.height)) {
7080
7313
  throw new Error(
@@ -7155,12 +7388,12 @@ async function resolveSpecs(loaded, filter) {
7155
7388
  return specs;
7156
7389
  }
7157
7390
  function pngPath(file) {
7158
- const extension = path12.extname(file);
7391
+ const extension = path13.extname(file);
7159
7392
  return extension ? `${file.slice(0, -extension.length)}.png` : `${file}.png`;
7160
7393
  }
7161
7394
  async function optionalRevisionImage(file, label, requiredFormat) {
7162
7395
  if (!existsSync8(file)) return null;
7163
- const bytes = await readFile5(file);
7396
+ const bytes = await readFile6(file);
7164
7397
  const metadata = imageMetadata(bytes);
7165
7398
  if (!metadata) throw new Error(`${label} is not a readable PNG or JPEG: ${file}`);
7166
7399
  if (requiredFormat && metadata.format !== requiredFormat) {
@@ -7173,8 +7406,8 @@ async function resolveStyleImages(loaded, styleId) {
7173
7406
  if (!style) throw new Error(`Unknown style "${styleId}"`);
7174
7407
  const out = [];
7175
7408
  for (const img of style.styleImages) {
7176
- const file = path12.resolve(loaded.root, img.path);
7177
- const buf = await readFile5(file);
7409
+ const file = path13.resolve(loaded.root, img.path);
7410
+ const buf = await readFile6(file);
7178
7411
  const metadata = imageMetadata(buf);
7179
7412
  if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${file}`);
7180
7413
  if (metadata.width < 1 || metadata.height < 1) {
@@ -7243,8 +7476,8 @@ function resolveCharacterShape(asset, style, kind) {
7243
7476
 
7244
7477
  // src/pipeline/fetch.ts
7245
7478
  import { existsSync as existsSync9, readFileSync as readFileSync3 } from "fs";
7246
- import { mkdir as mkdir3, readFile as readFile6, rename as rename3, rm as rm4, writeFile as writeFile3 } from "fs/promises";
7247
- import path13 from "path";
7479
+ import { mkdir as mkdir4, readFile as readFile7, rename as rename4, rm as rm4, writeFile as writeFile4 } from "fs/promises";
7480
+ import path14 from "path";
7248
7481
 
7249
7482
  // src/source-url.ts
7250
7483
  var SENSITIVE_QUERY_KEYS = /* @__PURE__ */ new Set([
@@ -7290,9 +7523,10 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
7290
7523
  const result = { downloaded: 0, skipped: 0, failed: 0, ...opts.refresh ? { unchanged: 0 } : {} };
7291
7524
  const specByKey = new Map(specs.map((s) => [lockKey(s.styleId, s.assetId), s]));
7292
7525
  normalizeLockOutputPaths(lock, specs);
7293
- const cacheDir = opts.cacheDir === false ? null : path13.resolve(opts.cacheDir ?? path13.join(path13.dirname(lockPath), ".pixelkiln", "cache"));
7526
+ const cacheDir = opts.cacheDir === false ? null : path14.resolve(opts.cacheDir ?? path14.join(path14.dirname(lockPath), ".pixelkiln", "cache"));
7294
7527
  const pending = Object.entries(lock.entries).filter(([key, e]) => {
7295
7528
  if (e.provider !== provider.id || !specByKey.has(key)) return false;
7529
+ if (specByKey.get(key)?.mirror) return false;
7296
7530
  if (e.status === "selected" || e.status === "download-failed") return true;
7297
7531
  if (e.status !== "downloaded") return false;
7298
7532
  if (opts.refresh) return e.outputs.length > 0 && Boolean(e.sourceUrls?.length || e.sourceUrl);
@@ -7331,7 +7565,40 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
7331
7565
  result.skipped++;
7332
7566
  continue;
7333
7567
  }
7334
- const sources = entry.sourceUrls?.length ? entry.sourceUrls : entry.sourceUrl ? [{ url: entry.sourceUrl }] : opts.repair && entry.outputs.length ? entry.outputs.map((o) => ({ url: "", role: o.role, mediaType: o.mediaType })) : [];
7568
+ const recordedSources = entry.sourceUrls?.length ? entry.sourceUrls : entry.sourceUrl ? [{ url: entry.sourceUrl }] : opts.repair && entry.outputs.length ? entry.outputs.map((o) => ({ url: "", role: o.role, mediaType: o.mediaType })) : [];
7569
+ let sources = recordedSources;
7570
+ let rotated = false;
7571
+ if (opts.refresh && entry.status === "downloaded" && entry.objectId && provider.refreshSources) {
7572
+ let current;
7573
+ try {
7574
+ current = await provider.refreshSources(entry.objectId, {
7575
+ generator: entry.generator,
7576
+ metadata: entry.providerMetadata?.[provider.id],
7577
+ sources: recordedSources
7578
+ });
7579
+ } catch (err) {
7580
+ const message6 = err instanceof Error ? err.message : String(err);
7581
+ upsert(lock, key, { status: "download-failed", error: `download failed: ${message6}` });
7582
+ result.failed++;
7583
+ log2(` FAILED ${key}: ${message6}`);
7584
+ await saveLock(lockPath, lock);
7585
+ continue;
7586
+ }
7587
+ if (current === null) {
7588
+ upsert(lock, key, { status: "download-failed", error: `download failed: ${entry.objectId} no longer exists upstream` });
7589
+ result.failed++;
7590
+ log2(` FAILED ${key}: ${entry.objectId} no longer exists upstream`);
7591
+ await saveLock(lockPath, lock);
7592
+ continue;
7593
+ }
7594
+ if (current) {
7595
+ sources = current.map((source) => ({
7596
+ ...source,
7597
+ mediaType: source.mediaType ?? recordedSources.find((r) => r.role === source.role)?.mediaType
7598
+ }));
7599
+ rotated = sources.length !== recordedSources.length || sources.some((source, index) => source.url !== recordedSources[index]?.url || source.role !== recordedSources[index]?.role);
7600
+ }
7601
+ }
7335
7602
  if (!sources.length) {
7336
7603
  upsert(lock, key, {
7337
7604
  status: "download-failed",
@@ -7358,7 +7625,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
7358
7625
  if (!source.url) throw new Error(`no durable source URL to refresh ${source.role ?? "asset"} from`);
7359
7626
  fresh = await provider.download(source.url);
7360
7627
  if (recorded && sha256(fresh) === recordedRaw && !reapply) {
7361
- log2(` current ${path13.relative(process.cwd(), target)}`);
7628
+ log2(` current ${path14.relative(process.cwd(), target)}`);
7362
7629
  outputs.push({ ...recorded, path: portableOutputPath(target, spec.root) });
7363
7630
  continue;
7364
7631
  }
@@ -7372,7 +7639,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
7372
7639
  if (cacheDir) {
7373
7640
  await cacheMedia(
7374
7641
  cacheDir,
7375
- await readFile6(target),
7642
+ await readFile7(target),
7376
7643
  recorded.mediaType ?? MediaType.PNG,
7377
7644
  recorded.sha256
7378
7645
  );
@@ -7381,10 +7648,10 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
7381
7648
  continue;
7382
7649
  }
7383
7650
  if (reapply && !fresh) {
7384
- if (!recorded.raw) raw = await readFile6(target);
7385
- log2(` reapply ${path13.relative(process.cwd(), target)}`);
7651
+ if (!recorded.raw) raw = await readFile7(target);
7652
+ log2(` reapply ${path14.relative(process.cwd(), target)}`);
7386
7653
  } else {
7387
- log2(` changed ${path13.relative(process.cwd(), target)} (upstream)`);
7654
+ log2(` changed ${path14.relative(process.cwd(), target)} (upstream)`);
7388
7655
  }
7389
7656
  } else {
7390
7657
  const superseded = (entry.supersededOutputs ?? []).find(
@@ -7403,7 +7670,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
7403
7670
  }
7404
7671
  }
7405
7672
  log2(
7406
- ` replace ${path13.relative(process.cwd(), target)}` + (opts.force ? " (--force)" : " (previous tracked generation)")
7673
+ ` replace ${path14.relative(process.cwd(), target)}` + (opts.force ? " (--force)" : " (previous tracked generation)")
7407
7674
  );
7408
7675
  }
7409
7676
  }
@@ -7412,7 +7679,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
7412
7679
  } else if (!raw && recorded && cacheDir) {
7413
7680
  if (!reapply) final = await readCachedMedia(cacheDir, recorded.sha256, expectedMediaType);
7414
7681
  if (!final && recordedRaw) raw = await readCachedMedia(cacheDir, recordedRaw, expectedMediaType);
7415
- if (final || raw) log2(` cached ${path13.relative(process.cwd(), target)}`);
7682
+ if (final || raw) log2(` cached ${path14.relative(process.cwd(), target)}`);
7416
7683
  }
7417
7684
  if (!final && !raw) {
7418
7685
  if (!source.url) {
@@ -7439,13 +7706,13 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
7439
7706
  buf = processed.bytes;
7440
7707
  if (processed.changed) {
7441
7708
  rawHash = sha256(raw);
7442
- log2(` snapped ${path13.relative(process.cwd(), target)} to ${wanted.palette.colors.length} colours`);
7709
+ log2(` snapped ${path14.relative(process.cwd(), target)} to ${wanted.palette.colors.length} colours`);
7443
7710
  }
7444
7711
  }
7445
7712
  if (cacheDir) await cacheMedia(cacheDir, buf, mediaType);
7446
- await mkdir3(path13.dirname(target), { recursive: true });
7713
+ await mkdir4(path14.dirname(target), { recursive: true });
7447
7714
  const tmp = `${target}.pixelkiln.tmp`;
7448
- await writeFile3(tmp, buf);
7715
+ await writeFile4(tmp, buf);
7449
7716
  outputs.push({
7450
7717
  path: portableOutputPath(target, spec.root),
7451
7718
  sha256: sha256(buf),
@@ -7455,11 +7722,15 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
7455
7722
  });
7456
7723
  upsert(lock, key, { outputs: mergeOutputs(entry.outputs, outputs) });
7457
7724
  await saveLock(lockPath, lock);
7458
- await rename3(tmp, target);
7725
+ await rename4(tmp, target);
7459
7726
  wrote = true;
7460
- log2(` wrote ${path13.relative(process.cwd(), target)}`);
7727
+ log2(` wrote ${path14.relative(process.cwd(), target)}`);
7461
7728
  }
7462
7729
  if (opts.refresh && !wrote) {
7730
+ if (rotated) {
7731
+ const persistent = sources.filter((source) => shouldPersistSourceUrl(source.url));
7732
+ upsert(lock, key, { sourceUrl: persistent[0]?.url ?? null, sourceUrls: persistent });
7733
+ }
7463
7734
  result.unchanged = (result.unchanged ?? 0) + 1;
7464
7735
  continue;
7465
7736
  }
@@ -7497,10 +7768,10 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
7497
7768
  return result;
7498
7769
  }
7499
7770
  async function readCachedMedia(cacheDir, hash, mediaType) {
7500
- const file = path13.join(cacheDir, cacheFileName(hash, mediaType));
7771
+ const file = path14.join(cacheDir, cacheFileName(hash, mediaType));
7501
7772
  if (!existsSync9(file)) return null;
7502
7773
  try {
7503
- const buf = await readFile6(file);
7774
+ const buf = await readFile7(file);
7504
7775
  if (sha256(buf) !== hash) return null;
7505
7776
  validateMedia(buf, mediaType);
7506
7777
  return buf;
@@ -7511,13 +7782,13 @@ async function readCachedMedia(cacheDir, hash, mediaType) {
7511
7782
  async function cacheMedia(cacheDir, buf, mediaType, knownHash) {
7512
7783
  const hash = knownHash ?? sha256(buf);
7513
7784
  validateMedia(buf, mediaType);
7514
- const file = path13.join(cacheDir, cacheFileName(hash, mediaType));
7785
+ const file = path14.join(cacheDir, cacheFileName(hash, mediaType));
7515
7786
  if (await readCachedMedia(cacheDir, hash, mediaType)) return;
7516
- await mkdir3(cacheDir, { recursive: true });
7787
+ await mkdir4(cacheDir, { recursive: true });
7517
7788
  const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
7518
- await writeFile3(tmp, buf);
7789
+ await writeFile4(tmp, buf);
7519
7790
  try {
7520
- await rename3(tmp, file);
7791
+ await rename4(tmp, file);
7521
7792
  } catch (err) {
7522
7793
  if (!existsSync9(file)) throw err;
7523
7794
  } finally {
@@ -7553,11 +7824,11 @@ async function pushTags(provider, specs, lock, opts = {}) {
7553
7824
 
7554
7825
  // src/pipeline/adopt.ts
7555
7826
  import { existsSync as existsSync11 } from "fs";
7556
- import { readFile as readFile8, writeFile as writeFile5 } from "fs/promises";
7557
- import path14 from "path";
7827
+ import { readFile as readFile9, writeFile as writeFile6 } from "fs/promises";
7828
+ import path15 from "path";
7558
7829
 
7559
7830
  // src/cache.ts
7560
- import { mkdir as mkdir4, readFile as readFile7, rename as rename4, rm as rm5, writeFile as writeFile4 } from "fs/promises";
7831
+ import { mkdir as mkdir5, readFile as readFile8, rename as rename5, rm as rm5, writeFile as writeFile5 } from "fs/promises";
7561
7832
  import { existsSync as existsSync10 } from "fs";
7562
7833
  import pathModule from "path";
7563
7834
  import { z as z3 } from "zod";
@@ -7573,10 +7844,10 @@ function isSha256Hash(value) {
7573
7844
  function parseCache(value) {
7574
7845
  return HashCacheSchema.parse(value);
7575
7846
  }
7576
- async function loadCache(path44) {
7577
- if (!existsSync10(path44)) return { version: 1, hashes: {} };
7847
+ async function loadCache(path45) {
7848
+ if (!existsSync10(path45)) return { version: 1, hashes: {} };
7578
7849
  try {
7579
- const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile7(path44, "utf8")));
7850
+ const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile8(path45, "utf8")));
7580
7851
  if (!parsed.success) return { version: 1, hashes: {} };
7581
7852
  return {
7582
7853
  version: 1,
@@ -7588,18 +7859,18 @@ async function loadCache(path44) {
7588
7859
  return { version: 1, hashes: {} };
7589
7860
  }
7590
7861
  }
7591
- async function saveCache(path44, cache) {
7862
+ async function saveCache(path45, cache) {
7592
7863
  const sorted = {};
7593
7864
  for (const key of Object.keys(cache.hashes).sort()) {
7594
7865
  const hash = cache.hashes[key];
7595
7866
  if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
7596
7867
  sorted[key] = hash;
7597
7868
  }
7598
- await mkdir4(pathModule.dirname(pathModule.resolve(path44)), { recursive: true });
7599
- const tmp = `${path44}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
7869
+ await mkdir5(pathModule.dirname(pathModule.resolve(path45)), { recursive: true });
7870
+ const tmp = `${path45}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
7600
7871
  try {
7601
- await writeFile4(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
7602
- await rename4(tmp, path44);
7872
+ await writeFile5(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
7873
+ await rename5(tmp, path45);
7603
7874
  } finally {
7604
7875
  await rm5(tmp, { force: true });
7605
7876
  }
@@ -7637,7 +7908,7 @@ async function adopt(provider, specs, lock, lockPath, opts = {}) {
7637
7908
  unmatchedLocal.push(`${spec.styleId}/${spec.assetId} (no file at ${spec.outFile})`);
7638
7909
  continue;
7639
7910
  }
7640
- const bytes = await readFile8(spec.outFile);
7911
+ const bytes = await readFile9(spec.outFile);
7641
7912
  try {
7642
7913
  decodePng(bytes);
7643
7914
  } catch (err) {
@@ -7757,7 +8028,7 @@ async function writePromptsBack(manifestPath, lock, opts = {}) {
7757
8028
  const log2 = opts.onProgress ?? (() => {
7758
8029
  });
7759
8030
  const selectedAssets = opts.assetIds ? new Set(opts.assetIds) : null;
7760
- const raw = JSON.parse(await readFile8(manifestPath, "utf8"));
8031
+ const raw = JSON.parse(await readFile9(manifestPath, "utf8"));
7761
8032
  const best = /* @__PURE__ */ new Map();
7762
8033
  for (const entry of Object.values(lock.entries)) {
7763
8034
  if (opts.provider && entry.provider !== opts.provider) continue;
@@ -7779,7 +8050,7 @@ async function writePromptsBack(manifestPath, lock, opts = {}) {
7779
8050
  stillEmpty.push(id);
7780
8051
  }
7781
8052
  }
7782
- await writeFile5(manifestPath, JSON.stringify(raw, null, 2) + "\n");
8053
+ await writeFile6(manifestPath, JSON.stringify(raw, null, 2) + "\n");
7783
8054
  log2(` filled ${filled} prompt(s) from adopted objects`);
7784
8055
  return { filled, stillEmpty };
7785
8056
  }
@@ -7791,14 +8062,14 @@ function formatUnmatchedRemote(objects, limit = 15) {
7791
8062
 
7792
8063
  // src/pipeline/adopt-characters.ts
7793
8064
  import { existsSync as existsSync12 } from "fs";
7794
- import { mkdir as mkdir5, readFile as readFile9, writeFile as writeFile6 } from "fs/promises";
7795
- import path15 from "path";
8065
+ import { mkdir as mkdir6, readFile as readFile10, writeFile as writeFile7 } from "fs/promises";
8066
+ import path16 from "path";
7796
8067
  async function adoptCharacters(provider, specs, lock, lockPath, opts) {
7797
8068
  const log2 = opts.onProgress ?? (() => {
7798
8069
  });
7799
8070
  const result = { scanned: 0, matched: 0, unmatched: [], remaining: [], written: [], differing: [] };
7800
8071
  if (!provider.listCharacters || !provider.getCharacter) return result;
7801
- const wanted = specs.filter((spec) => spec.character);
8072
+ const wanted = specs.filter((spec) => spec.character && !spec.mirror);
7802
8073
  if (!wanted.length) return result;
7803
8074
  const remote = [];
7804
8075
  for await (const character of provider.listCharacters()) remote.push(character);
@@ -7844,11 +8115,11 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
7844
8115
  const hash = sha256(bytes);
7845
8116
  const file = expectedOutputPath(spec, source.role, index, sources.length);
7846
8117
  if (existsSync12(file)) {
7847
- if (sha256(await readFile9(file)) !== hash) result.differing.push(path15.relative(process.cwd(), file));
8118
+ if (sha256(await readFile10(file)) !== hash) result.differing.push(path16.relative(process.cwd(), file));
7848
8119
  } else {
7849
- await mkdir5(path15.dirname(file), { recursive: true });
7850
- await writeFile6(file, bytes);
7851
- result.written.push(path15.relative(process.cwd(), file));
8120
+ await mkdir6(path16.dirname(file), { recursive: true });
8121
+ await writeFile7(file, bytes);
8122
+ result.written.push(path16.relative(process.cwd(), file));
7852
8123
  }
7853
8124
  outputs.push({ path: portableOutputPath(file, spec.root), sha256: hash, ...source.role ? { role: source.role } : {} });
7854
8125
  }
@@ -7887,10 +8158,10 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
7887
8158
  } else {
7888
8159
  const south = expectedOutputPath(spec, "south", 0, spec.character.directions);
7889
8160
  if (!existsSync12(south)) {
7890
- result.unmatched.push(`${key}: no remoteId and no ${path15.relative(process.cwd(), south)} to match by`);
8161
+ result.unmatched.push(`${key}: no remoteId and no ${path16.relative(process.cwd(), south)} to match by`);
7891
8162
  return false;
7892
8163
  }
7893
- const localHash = sha256(await readFile9(south));
8164
+ const localHash = sha256(await readFile10(south));
7894
8165
  for (const candidate of remote) {
7895
8166
  if (claimed.has(candidate.id)) continue;
7896
8167
  if (await southHash(candidate) === localHash) {
@@ -7899,7 +8170,7 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
7899
8170
  }
7900
8171
  }
7901
8172
  if (!match) {
7902
- result.unmatched.push(`${key}: no character's south rotation has the bytes of ${path15.relative(process.cwd(), south)}`);
8173
+ result.unmatched.push(`${key}: no character's south rotation has the bytes of ${path16.relative(process.cwd(), south)}`);
7903
8174
  return false;
7904
8175
  }
7905
8176
  }
@@ -7969,12 +8240,12 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
7969
8240
  if (!existsSync12(first)) {
7970
8241
  const byName = candidates.find((a) => a.name === characterAnimationName(spec));
7971
8242
  if (!byName) {
7972
- result.unmatched.push(`${key}: no remoteId and no ${path15.relative(process.cwd(), first)} to match by`);
8243
+ result.unmatched.push(`${key}: no remoteId and no ${path16.relative(process.cwd(), first)} to match by`);
7973
8244
  return false;
7974
8245
  }
7975
8246
  match = byName;
7976
8247
  } else {
7977
- const localHash = sha256(await readFile9(first));
8248
+ const localHash = sha256(await readFile10(first));
7978
8249
  for (const candidate of candidates) {
7979
8250
  if (sha256(await download(candidate.frames[0])) === localHash) {
7980
8251
  match = candidate;
@@ -7982,7 +8253,7 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
7982
8253
  }
7983
8254
  }
7984
8255
  if (!match) {
7985
- result.unmatched.push(`${key}: no ${animation.direction} animation of character ${parentId} starts with the bytes of ${path15.relative(process.cwd(), first)}`);
8256
+ result.unmatched.push(`${key}: no ${animation.direction} animation of character ${parentId} starts with the bytes of ${path16.relative(process.cwd(), first)}`);
7986
8257
  return false;
7987
8258
  }
7988
8259
  }
@@ -8042,9 +8313,9 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
8042
8313
  }
8043
8314
 
8044
8315
  // src/pipeline/salvage.ts
8045
- import { readFile as readFile10 } from "fs/promises";
8316
+ import { readFile as readFile11 } from "fs/promises";
8046
8317
  import { existsSync as existsSync13 } from "fs";
8047
- import path16 from "path";
8318
+ import path17 from "path";
8048
8319
  function providerClaimId(provider, objectId) {
8049
8320
  return `${encodeURIComponent(provider)}:${encodeURIComponent(objectId)}`;
8050
8321
  }
@@ -8054,7 +8325,7 @@ async function loadClaims(lockPaths, opts = {}) {
8054
8325
  if (!existsSync13(p)) throw new Error(`Claim lockfile not found: ${p}`);
8055
8326
  let parsed;
8056
8327
  try {
8057
- parsed = parseLock(JSON.parse(await readFile10(p, "utf8")));
8328
+ parsed = parseLock(JSON.parse(await readFile11(p, "utf8")));
8058
8329
  } catch {
8059
8330
  throw new Error(`Claim lockfile is malformed: ${p}`);
8060
8331
  }
@@ -8102,20 +8373,20 @@ function matchStyleByPattern(prompt, manifest) {
8102
8373
  return null;
8103
8374
  }
8104
8375
  async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, claimPaths) {
8105
- const own = path16.resolve(ownManifestPath);
8376
+ const own = path17.resolve(ownManifestPath);
8106
8377
  const siblingManifestPaths = [
8107
8378
  .../* @__PURE__ */ new Set([
8108
8379
  ...workspaceManifestPaths,
8109
- ...claimPaths.map((c) => path16.join(path16.dirname(path16.resolve(c)), "pixelkiln.manifest.json"))
8380
+ ...claimPaths.map((c) => path17.join(path17.dirname(path17.resolve(c)), "pixelkiln.manifest.json"))
8110
8381
  ])
8111
8382
  ];
8112
8383
  const siblings = [];
8113
8384
  for (const siblingManifestPath of siblingManifestPaths) {
8114
- if (path16.resolve(siblingManifestPath) === own) continue;
8385
+ if (path17.resolve(siblingManifestPath) === own) continue;
8115
8386
  if (!existsSync13(siblingManifestPath)) continue;
8116
8387
  try {
8117
8388
  const { manifest } = await loadManifest(siblingManifestPath);
8118
- siblings.push({ label: path16.basename(path16.dirname(siblingManifestPath)), manifest });
8389
+ siblings.push({ label: path17.basename(path17.dirname(siblingManifestPath)), manifest });
8119
8390
  } catch {
8120
8391
  }
8121
8392
  }
@@ -8231,9 +8502,9 @@ async function applyTags(provider, decisions, existing, opts = {}) {
8231
8502
  }
8232
8503
 
8233
8504
  // src/workspace.ts
8234
- import { mkdir as mkdir6, readFile as readFile11, rename as rename5, rm as rm6, writeFile as writeFile7 } from "fs/promises";
8505
+ import { mkdir as mkdir7, readFile as readFile12, rename as rename6, rm as rm6, writeFile as writeFile8 } from "fs/promises";
8235
8506
  import { existsSync as existsSync14 } from "fs";
8236
- import path17 from "path";
8507
+ import path18 from "path";
8237
8508
  import { z as z4 } from "zod";
8238
8509
  var WorkspaceProjectSchema = z4.object({
8239
8510
  id: z4.string().min(1),
@@ -8261,7 +8532,7 @@ async function loadWorkspace(workspacePath) {
8261
8532
  if (!existsSync14(workspacePath)) return { version: 1, projects: [] };
8262
8533
  let raw;
8263
8534
  try {
8264
- raw = JSON.parse(await readFile11(workspacePath, "utf8"));
8535
+ raw = JSON.parse(await readFile12(workspacePath, "utf8"));
8265
8536
  } catch (err) {
8266
8537
  throw new ProjectError(
8267
8538
  `Workspace catalog at ${workspacePath} is malformed:
@@ -8275,22 +8546,22 @@ async function saveWorkspace(workspacePath, ws) {
8275
8546
  version: 1,
8276
8547
  projects: [...ws.projects].sort((a, b) => a.id.localeCompare(b.id))
8277
8548
  };
8278
- await mkdir6(path17.dirname(path17.resolve(workspacePath)), { recursive: true });
8549
+ await mkdir7(path18.dirname(path18.resolve(workspacePath)), { recursive: true });
8279
8550
  const tmp = `${workspacePath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
8280
8551
  try {
8281
- await writeFile7(tmp, JSON.stringify(sorted, null, 2) + "\n");
8282
- await rename5(tmp, workspacePath);
8552
+ await writeFile8(tmp, JSON.stringify(sorted, null, 2) + "\n");
8553
+ await rename6(tmp, workspacePath);
8283
8554
  } finally {
8284
8555
  await rm6(tmp, { force: true });
8285
8556
  }
8286
8557
  }
8287
8558
  function toPortablePath(dir, absolute) {
8288
- return path17.relative(dir, absolute).split(path17.sep).join("/");
8559
+ return path18.relative(dir, absolute).split(path18.sep).join("/");
8289
8560
  }
8290
8561
  function resolveProject(dir, project) {
8291
8562
  return {
8292
- manifestPath: path17.resolve(dir, project.manifest.split("/").join(path17.sep)),
8293
- lockPath: path17.resolve(dir, project.lock.split("/").join(path17.sep))
8563
+ manifestPath: path18.resolve(dir, project.manifest.split("/").join(path18.sep)),
8564
+ lockPath: path18.resolve(dir, project.lock.split("/").join(path18.sep))
8294
8565
  };
8295
8566
  }
8296
8567
  function validateWorkspace(ws, dir) {
@@ -8303,7 +8574,7 @@ function validateWorkspace(ws, dir) {
8303
8574
  const { manifestPath, lockPath } = resolveProject(dir, project);
8304
8575
  lockOwners.set(lockPath, [...lockOwners.get(lockPath) ?? [], project.id]);
8305
8576
  manifestOwners.set(manifestPath, [...manifestOwners.get(manifestPath) ?? [], project.id]);
8306
- if (path17.isAbsolute(project.manifest) || path17.isAbsolute(project.lock)) {
8577
+ if (path18.isAbsolute(project.manifest) || path18.isAbsolute(project.lock)) {
8307
8578
  diagnostics.push({
8308
8579
  id: "absolute-path",
8309
8580
  level: "warning",
@@ -8364,8 +8635,8 @@ function validateWorkspace(ws, dir) {
8364
8635
  }
8365
8636
 
8366
8637
  // src/pick/salvage-server.ts
8367
- import { mkdir as mkdir7, writeFile as writeFile8, readFile as readFile13 } from "fs/promises";
8368
- import path18 from "path";
8638
+ import { mkdir as mkdir8, writeFile as writeFile9, readFile as readFile14 } from "fs/promises";
8639
+ import path19 from "path";
8369
8640
 
8370
8641
  // src/pick/salvage-sheet.ts
8371
8642
  var escapeHtml = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
@@ -8525,7 +8796,7 @@ refresh();
8525
8796
 
8526
8797
  // src/pick/review-server.ts
8527
8798
  import { createServer } from "http";
8528
- import { readFile as readFile12 } from "fs/promises";
8799
+ import { readFile as readFile13 } from "fs/promises";
8529
8800
 
8530
8801
  // src/open.ts
8531
8802
  import { spawn } from "child_process";
@@ -8552,7 +8823,7 @@ function serveReviewPage(opts) {
8552
8823
  if (req.method === "GET" && req.url && opts.assets?.has(req.url)) {
8553
8824
  try {
8554
8825
  const asset = opts.assets.get(req.url);
8555
- const bytes = await readFile12(asset.path);
8826
+ const bytes = await readFile13(asset.path);
8556
8827
  res.writeHead(200, {
8557
8828
  "Content-Type": asset.contentType,
8558
8829
  "Cache-Control": "no-store",
@@ -8629,7 +8900,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
8629
8900
  });
8630
8901
  const html = renderSalvageSheet(orphans, {
8631
8902
  styleId: ctx.styleId,
8632
- importDir: path18.relative(process.cwd(), ctx.importDir) || "."
8903
+ importDir: path19.relative(process.cwd(), ctx.importDir) || "."
8633
8904
  });
8634
8905
  const byId = new Map(orphans.map((o) => [o.id, o]));
8635
8906
  const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
@@ -8658,10 +8929,10 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
8658
8929
  if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
8659
8930
  decodePng(buf);
8660
8931
  const assetId = idFromPrompt(orphan.prompt, taken);
8661
- const rel = path18.join("_salvaged", `${assetId}.png`);
8662
- const outFile = path18.resolve(ctx.importDir, rel);
8663
- await mkdir7(path18.dirname(outFile), { recursive: true });
8664
- await writeFile8(outFile, buf);
8932
+ const rel = path19.join("_salvaged", `${assetId}.png`);
8933
+ const outFile = path19.resolve(ctx.importDir, rel);
8934
+ await mkdir8(path19.dirname(outFile), { recursive: true });
8935
+ await writeFile9(outFile, buf);
8665
8936
  ctx.manifest.assets[assetId] = {
8666
8937
  prompt: orphan.prompt,
8667
8938
  promptByStyle: {},
@@ -8693,7 +8964,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
8693
8964
  sourceUrl: durableSource,
8694
8965
  sourceUrls: durableSource ? [{ url: durableSource }] : [],
8695
8966
  outputs: [{
8696
- path: portableOutputPath(outFile, path18.dirname(ctx.manifestPath)),
8967
+ path: portableOutputPath(outFile, path19.dirname(ctx.manifestPath)),
8697
8968
  sha256: sha256(buf)
8698
8969
  }],
8699
8970
  submittedAt: orphan.createdAt,
@@ -8717,9 +8988,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
8717
8988
  }
8718
8989
  }
8719
8990
  await applyTags(provider, decisions, existingTags, { onProgress: log2 });
8720
- const raw = JSON.parse(await readFile13(ctx.manifestPath, "utf8"));
8991
+ const raw = JSON.parse(await readFile14(ctx.manifestPath, "utf8"));
8721
8992
  for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
8722
- await writeFile8(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
8993
+ await writeFile9(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
8723
8994
  await saveLock(ctx.lockPath, ctx.lock);
8724
8995
  return result;
8725
8996
  }
@@ -8727,19 +8998,19 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
8727
8998
  }
8728
8999
 
8729
9000
  // src/cli/project.ts
8730
- import path20 from "path";
9001
+ import path21 from "path";
8731
9002
  import { existsSync as existsSync15 } from "fs";
8732
9003
 
8733
9004
  // src/project.ts
8734
- import path19 from "path";
9005
+ import path20 from "path";
8735
9006
  async function openProject(manifestPath = "pixelkiln.manifest.json", opts = {}) {
8736
- const absolute = path19.resolve(manifestPath);
8737
- const root = path19.dirname(absolute);
9007
+ const absolute = path20.resolve(manifestPath);
9008
+ const root = path20.dirname(absolute);
8738
9009
  if (opts.env !== false) {
8739
9010
  loadEnvFiles(root);
8740
- if (path19.resolve(process.cwd()) !== root) loadEnvFiles(process.cwd());
9011
+ if (path20.resolve(process.cwd()) !== root) loadEnvFiles(process.cwd());
8741
9012
  }
8742
- const lockPath = path19.resolve(opts.lockPath ?? path19.join(root, "pixelkiln.lock.json"));
9013
+ const lockPath = path20.resolve(opts.lockPath ?? path20.join(root, "pixelkiln.lock.json"));
8743
9014
  const loaded = await loadManifest(absolute);
8744
9015
  const specs = await resolveSpecs(loaded, { styles: opts.styles ?? [], assets: opts.assets ?? [] });
8745
9016
  const lock = await loadLock(lockPath);
@@ -8870,7 +9141,7 @@ async function workspaceStatus(ws, dir) {
8870
9141
 
8871
9142
  // src/cli/project.ts
8872
9143
  async function provenanceFile(id, file) {
8873
- const absolute = path20.resolve(file);
9144
+ const absolute = path21.resolve(file);
8874
9145
  return {
8875
9146
  id,
8876
9147
  path: absolute,
@@ -8949,7 +9220,7 @@ async function requireCompleteWorkspaceClaims(workspacePath) {
8949
9220
  if (!existsSync15(workspacePath)) {
8950
9221
  throw new ProjectError(`Workspace catalog not found: ${workspacePath}`);
8951
9222
  }
8952
- const dir = path20.dirname(path20.resolve(workspacePath));
9223
+ const dir = path21.dirname(path21.resolve(workspacePath));
8953
9224
  const ws = await loadWorkspace(workspacePath);
8954
9225
  const diagnostics = validateWorkspace(ws, dir);
8955
9226
  const errors = diagnostics.filter((d) => d.level === "error");
@@ -8972,9 +9243,9 @@ async function openAccountProject(args) {
8972
9243
  return project;
8973
9244
  }
8974
9245
  async function open(args) {
8975
- if (!existsSync15(path20.resolve(args.manifest))) {
9246
+ if (!existsSync15(path21.resolve(args.manifest))) {
8976
9247
  throw new ProjectError(
8977
- `No manifest at ${path20.resolve(args.manifest)}. Pass --manifest, or run \`pixelkiln init --from <dir>\`.`
9248
+ `No manifest at ${path21.resolve(args.manifest)}. Pass --manifest, or run \`pixelkiln init --from <dir>\`.`
8978
9249
  );
8979
9250
  }
8980
9251
  const project = await openProject(args.manifest, { lockPath: args.lock, styles: args.styles, assets: args.assets });
@@ -9016,7 +9287,7 @@ function providerCache(mode2) {
9016
9287
 
9017
9288
  // src/cli/commands/account.ts
9018
9289
  async function runBalance(args) {
9019
- loadEnvFiles(path21.dirname(path21.resolve(args.manifest)));
9290
+ loadEnvFiles(path22.dirname(path22.resolve(args.manifest)));
9020
9291
  loadEnvFiles(process.cwd());
9021
9292
  const loaded = await loadManifest(args.manifest);
9022
9293
  const selectedProvider = accountProviderId(loaded.manifest, args.provider, "balance");
@@ -9089,12 +9360,12 @@ async function runAdopt(args) {
9089
9360
  tagged ${n} object(s) upstream`);
9090
9361
  }
9091
9362
  if (args.writePrompts) {
9092
- const { filled, stillEmpty } = await writePromptsBack(path21.resolve(args.manifest), lock, {
9363
+ const { filled, stillEmpty } = await writePromptsBack(path22.resolve(args.manifest), lock, {
9093
9364
  onProgress: log,
9094
9365
  provider: provider.id,
9095
9366
  assetIds: specs.map((spec) => spec.assetId)
9096
9367
  });
9097
- log(` recovered ${filled} prompt(s) into ${path21.relative(process.cwd(), args.manifest)}`);
9368
+ log(` recovered ${filled} prompt(s) into ${path22.relative(process.cwd(), args.manifest)}`);
9098
9369
  const reloaded = await loadManifest(args.manifest);
9099
9370
  const rebased = (await resolveSpecs(reloaded, {
9100
9371
  styles: args.styles,
@@ -9124,12 +9395,12 @@ async function runSalvage2(args) {
9124
9395
  const provider = providerFor(accountProvider);
9125
9396
  const jsonMode = args.dryRun && args.json;
9126
9397
  const diag = jsonMode ? (msg = "") => console.error(msg) : log;
9127
- const ownLock = path21.resolve(args.lock);
9398
+ const ownLock = path22.resolve(args.lock);
9128
9399
  let workspaceProjects = [];
9129
9400
  let workspaceDir = "";
9130
9401
  if (args.workspace) {
9131
- const workspacePath = path21.resolve(args.workspace);
9132
- workspaceDir = path21.dirname(workspacePath);
9402
+ const workspacePath = path22.resolve(args.workspace);
9403
+ workspaceDir = path22.dirname(workspacePath);
9133
9404
  const complete = await requireCompleteWorkspaceClaims(workspacePath);
9134
9405
  workspaceProjects = complete.ws.projects;
9135
9406
  for (const d of complete.diagnostics) diag(` WARN ${d.id}: ${d.message}`);
@@ -9139,11 +9410,11 @@ async function runSalvage2(args) {
9139
9410
  .../* @__PURE__ */ new Set([
9140
9411
  ...workspaceLockPaths,
9141
9412
  ...existsSync16(ownLock) ? [ownLock] : [],
9142
- ...args.claims.map((c) => path21.resolve(c))
9413
+ ...args.claims.map((c) => path22.resolve(c))
9143
9414
  ])
9144
9415
  ];
9145
9416
  diag(` claim set (${lockPaths.length} lockfile(s)):`);
9146
- for (const p of lockPaths) diag(` ${path21.relative(process.cwd(), p)}`);
9417
+ for (const p of lockPaths) diag(` ${path22.relative(process.cwd(), p)}`);
9147
9418
  if (!args.claims.length && !args.workspace) {
9148
9419
  diag(
9149
9420
  `
@@ -9151,7 +9422,7 @@ async function runSalvage2(args) {
9151
9422
  pass every other project's lockfile via --claims a.json,b.json, or
9152
9423
  register every project in a workspace catalog and pass --workspace.`
9153
9424
  );
9154
- } else if (args.workspace && !workspaceProjects.some((p) => resolveProject(workspaceDir, p).manifestPath === path21.resolve(args.manifest))) {
9425
+ } else if (args.workspace && !workspaceProjects.some((p) => resolveProject(workspaceDir, p).manifestPath === path22.resolve(args.manifest))) {
9155
9426
  diag(
9156
9427
  `
9157
9428
  This project's manifest is not registered in the workspace catalog. Its own
@@ -9235,7 +9506,7 @@ async function runSalvage2(args) {
9235
9506
  manifestPath: loaded.path,
9236
9507
  manifest: accountManifest,
9237
9508
  styleId,
9238
- importDir: path21.resolve(loaded.root, style.outDir),
9509
+ importDir: path22.resolve(loaded.root, style.outDir),
9239
9510
  lock,
9240
9511
  lockPath: args.lock
9241
9512
  },
@@ -9336,17 +9607,17 @@ async function runTag(args) {
9336
9607
  }
9337
9608
 
9338
9609
  // src/cli/commands/edit.ts
9339
- import path24 from "path";
9610
+ import path25 from "path";
9340
9611
 
9341
9612
  // src/pipeline/hand-edit.ts
9342
9613
  import { existsSync as existsSync17 } from "fs";
9343
- import { copyFile, mkdir as mkdir8, readFile as readFile15, rename as rename7, rm as rm8, writeFile as writeFile10 } from "fs/promises";
9344
- import path23 from "path";
9614
+ import { copyFile, mkdir as mkdir9, readFile as readFile16, rename as rename8, rm as rm8, writeFile as writeFile11 } from "fs/promises";
9615
+ import path24 from "path";
9345
9616
  import { z as z6 } from "zod";
9346
9617
 
9347
9618
  // src/manifest-edit.ts
9348
- import { readFile as readFile14, rename as rename6, rm as rm7, writeFile as writeFile9 } from "fs/promises";
9349
- import path22 from "path";
9619
+ import { readFile as readFile15, rename as rename7, rm as rm7, writeFile as writeFile10 } from "fs/promises";
9620
+ import path23 from "path";
9350
9621
  import { z as z5 } from "zod";
9351
9622
  var HexSha = z5.string().regex(/^[0-9a-f]{64}$/);
9352
9623
  var AssetPatchSchema = z5.object({
@@ -9561,8 +9832,8 @@ function applyEdit(raw, edit) {
9561
9832
  }
9562
9833
  }
9563
9834
  async function applyManifestEdit(manifestPath, edit) {
9564
- const absolute = path22.resolve(manifestPath);
9565
- const original = await readFile14(absolute, "utf8");
9835
+ const absolute = path23.resolve(manifestPath);
9836
+ const original = await readFile15(absolute, "utf8");
9566
9837
  const before = sha256(original);
9567
9838
  if (before !== edit.expectedSha256) throw new ManifestDriftError(absolute);
9568
9839
  let raw;
@@ -9576,12 +9847,12 @@ async function applyManifestEdit(manifestPath, edit) {
9576
9847
  applyEdit(raw, edit);
9577
9848
  const next = serializeLike(original, raw);
9578
9849
  if (next === original) return { manifestPath: absolute, sha256: before, changed: false };
9579
- const tmp = path22.join(
9580
- path22.dirname(absolute),
9581
- `.${path22.basename(absolute)}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`
9850
+ const tmp = path23.join(
9851
+ path23.dirname(absolute),
9852
+ `.${path23.basename(absolute)}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`
9582
9853
  );
9583
9854
  try {
9584
- await writeFile9(tmp, next);
9855
+ await writeFile10(tmp, next);
9585
9856
  try {
9586
9857
  const loaded = await loadManifest(tmp);
9587
9858
  await resolveSpecs(loaded);
@@ -9589,7 +9860,7 @@ async function applyManifestEdit(manifestPath, edit) {
9589
9860
  const message6 = error instanceof Error ? error.message : String(error);
9590
9861
  throw new ManifestEditError(message6.replace(tmp, absolute));
9591
9862
  }
9592
- await rename6(tmp, absolute);
9863
+ await rename7(tmp, absolute);
9593
9864
  } finally {
9594
9865
  await rm7(tmp, { force: true });
9595
9866
  }
@@ -9600,11 +9871,11 @@ async function applyManifestEdit(manifestPath, edit) {
9600
9871
  var HAND_EDIT_DIR = "edits";
9601
9872
  function handEditPath(loaded, spec) {
9602
9873
  const style = loaded.manifest.styles[spec.styleId];
9603
- const outDir = path23.resolve(loaded.root, style?.outDir ?? ".");
9604
- const relative = path23.relative(outDir, spec.outFile);
9605
- const inside = relative && !relative.startsWith("..") && !path23.isAbsolute(relative);
9606
- const file = inside ? relative : path23.basename(spec.outFile);
9607
- return path23.join(outDir, HAND_EDIT_DIR, file.replace(/\.gif$/i, ".png"));
9874
+ const outDir = path24.resolve(loaded.root, style?.outDir ?? ".");
9875
+ const relative = path24.relative(outDir, spec.outFile);
9876
+ const inside = relative && !relative.startsWith("..") && !path24.isAbsolute(relative);
9877
+ const file = inside ? relative : path24.basename(spec.outFile);
9878
+ return path24.join(outDir, HAND_EDIT_DIR, file.replace(/\.gif$/i, ".png"));
9608
9879
  }
9609
9880
  function handEditBases(spec, lock) {
9610
9881
  const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
@@ -9646,7 +9917,7 @@ async function startHandEdit(loaded, lock, spec, opts = {}) {
9646
9917
  `${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`
9647
9918
  );
9648
9919
  }
9649
- const existing = spec.source ? path23.resolve(loaded.root, spec.source) : null;
9920
+ const existing = spec.source ? path24.resolve(loaded.root, spec.source) : null;
9650
9921
  const editPath = existing ?? handEditPath(loaded, spec);
9651
9922
  const members = handEditMembers(loaded, spec, lock, editPath);
9652
9923
  let created = false;
@@ -9657,7 +9928,7 @@ async function startHandEdit(loaded, lock, spec, opts = {}) {
9657
9928
  `${spec.styleId}/${spec.assetId} has no generated PNG on disk to start from; generate or restore it first`
9658
9929
  );
9659
9930
  }
9660
- await mkdir8(path23.dirname(member.path), { recursive: true });
9931
+ await mkdir9(path24.dirname(member.path), { recursive: true });
9661
9932
  await copyFile(member.base, member.path);
9662
9933
  created = true;
9663
9934
  }
@@ -9736,7 +10007,7 @@ function handEditProjectPath(editPath) {
9736
10007
  async function readHandEditCompanion(editPath) {
9737
10008
  let text;
9738
10009
  try {
9739
- text = await readFile15(handEditCompanionPath(editPath), "utf8");
10010
+ text = await readFile16(handEditCompanionPath(editPath), "utf8");
9740
10011
  } catch {
9741
10012
  return null;
9742
10013
  }
@@ -9779,7 +10050,7 @@ async function saveHandEdit(loaded, lock, spec, input) {
9779
10050
  }
9780
10051
  });
9781
10052
  const bases = handEditBases(spec, lock);
9782
- const expectedSize = bases?.[0] ? decodePng(await readFile15(bases[0].path)) : { width: spec.width, height: spec.height };
10053
+ const expectedSize = bases?.[0] ? decodePng(await readFile16(bases[0].path)) : { width: spec.width, height: spec.height };
9783
10054
  for (const [index, image] of decoded.entries()) {
9784
10055
  if (image.width !== expectedSize.width || image.height !== expectedSize.height) {
9785
10056
  const which = memberSet ? `member ${incoming[index].role ?? index}` : "the edit";
@@ -9795,7 +10066,7 @@ async function saveHandEdit(loaded, lock, spec, input) {
9795
10066
  editor: input.editor,
9796
10067
  protocol: input.protocol,
9797
10068
  savedAt: (input.now ?? /* @__PURE__ */ new Date()).toISOString(),
9798
- project: projectPath ? path23.basename(projectPath) : null,
10069
+ project: projectPath ? path24.basename(projectPath) : null,
9799
10070
  outputs: incoming.map((frame, index) => ({
9800
10071
  role: frame.role,
9801
10072
  basedOn: basedOnHash(entry, index),
@@ -9825,8 +10096,8 @@ function basedOnHash(entry, index) {
9825
10096
  async function replaceFile(file, bytes) {
9826
10097
  const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
9827
10098
  try {
9828
- await writeFile10(tmp, bytes);
9829
- await rename7(tmp, file);
10099
+ await writeFile11(tmp, bytes);
10100
+ await rename8(tmp, file);
9830
10101
  } finally {
9831
10102
  await rm8(tmp, { force: true });
9832
10103
  }
@@ -9855,7 +10126,7 @@ async function runEdit(args) {
9855
10126
  const set = started.members.length > 1;
9856
10127
  const kind = spec.generator === "frames" ? "frames" : "members";
9857
10128
  log(` ${started.created ? "created" : "found"} ${started.source}${set ? ` (${started.members.length} ${kind})` : ""}${started.declared ? " and declared it as the asset's source" : ""}`);
9858
- if (set) for (const member of started.members) log(` ${member.role ?? ""} ${path24.relative(process.cwd(), member.path)}`);
10129
+ if (set) for (const member of started.members) log(` ${member.role ?? ""} ${path25.relative(process.cwd(), member.path)}`);
9859
10130
  if (args.noOpen) {
9860
10131
  log(` edit ${set ? "them" : "it"} with your own tool, then run pixelkiln plan; mount and pack place ${set ? "them" : "it"} in place of the generated art`);
9861
10132
  } else {
@@ -9865,17 +10136,17 @@ async function runEdit(args) {
9865
10136
  }
9866
10137
 
9867
10138
  // src/cli/commands/gallery.ts
9868
- import path31 from "path";
10139
+ import path32 from "path";
9869
10140
  import { existsSync as existsSync23 } from "fs";
9870
10141
 
9871
10142
  // src/gallery/snapshot.ts
9872
10143
  import { createHash as createHash3 } from "crypto";
9873
10144
  import { existsSync as existsSync19 } from "fs";
9874
- import { readFile as readFile16, stat as stat2 } from "fs/promises";
9875
- import path27 from "path";
10145
+ import { readFile as readFile17, stat as stat2 } from "fs/promises";
10146
+ import path28 from "path";
9876
10147
 
9877
10148
  // src/gallery/edit.ts
9878
- import path25 from "path";
10149
+ import path26 from "path";
9879
10150
  import { z as z7 } from "zod";
9880
10151
  var Base64 = z7.string().regex(/^[A-Za-z0-9+/]*={0,2}$/).max(Math.ceil(MAX_HAND_EDIT_BYTES / 3) * 4);
9881
10152
  var HandEditRequestSchema = z7.discriminatedUnion("action", [
@@ -9965,7 +10236,7 @@ function createGalleryEditHandler(opts) {
9965
10236
  }
9966
10237
  const result = await applyManifestEdit(manifestPath, parsed.data);
9967
10238
  if (result.changed) {
9968
- const relative = path25.relative(process.cwd(), result.manifestPath);
10239
+ const relative = path26.relative(process.cwd(), result.manifestPath);
9969
10240
  const shown = !relative || relative.startsWith("..") ? result.manifestPath : relative;
9970
10241
  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;
9971
10242
  log2(` manifest edited: ${parsed.data.action === "add-asset" ? "added" : "changed"} ${subject} in ${shown}`);
@@ -9976,7 +10247,7 @@ function createGalleryEditHandler(opts) {
9976
10247
 
9977
10248
  // src/pipeline/history.ts
9978
10249
  import { existsSync as existsSync18 } from "fs";
9979
- import path26 from "path";
10250
+ import path27 from "path";
9980
10251
  var HISTORY_ENV = "PIXELKILN_HISTORY";
9981
10252
  function historyLimit(manifest, env = process.env) {
9982
10253
  if (manifest?.history !== void 0) return manifest.history;
@@ -10065,7 +10336,7 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
10065
10336
  if (!existsSync18(file)) continue;
10066
10337
  if (await sha256File(file) !== entry.outputs[i].sha256 && !opts.force) {
10067
10338
  throw new OverwriteRefusedError(
10068
- `${path26.relative(process.cwd(), file)} was changed after download; keep it with pixelkiln edit, or pass --force to replace it`,
10339
+ `${path27.relative(process.cwd(), file)} was changed after download; keep it with pixelkiln edit, or pass --force to replace it`,
10069
10340
  [file]
10070
10341
  );
10071
10342
  }
@@ -10117,7 +10388,7 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
10117
10388
  // src/gallery/snapshot.ts
10118
10389
  var PROJECT_FILE_TYPE = "application/octet-stream";
10119
10390
  function galleryMediaId(absolutePath) {
10120
- return createHash3("sha256").update(path27.resolve(absolutePath)).digest("hex").slice(0, 24);
10391
+ return createHash3("sha256").update(path28.resolve(absolutePath)).digest("hex").slice(0, 24);
10121
10392
  }
10122
10393
  function galleryMediaRoute(id) {
10123
10394
  return `/media/${id}`;
@@ -10136,7 +10407,7 @@ function describeHistory(media, entry, cacheDir) {
10136
10407
  return (entry.history ?? []).map((generation, i) => {
10137
10408
  const outputs = generation.outputs.map((output) => {
10138
10409
  const mediaType = output.mediaType ?? MediaType.PNG;
10139
- const file = path27.join(cacheDir, cacheFileName(output.sha256, mediaType));
10410
+ const file = path28.join(cacheDir, cacheFileName(output.sha256, mediaType));
10140
10411
  let url = null;
10141
10412
  if (existsSync19(file)) {
10142
10413
  const id = galleryMediaId(file);
@@ -10167,7 +10438,7 @@ function describeHistory(media, entry, cacheDir) {
10167
10438
  var MAX_COMPARED_BYTES = 8 * 1024 * 1024;
10168
10439
  async function samePixels(a, b) {
10169
10440
  try {
10170
- const [left, right] = await Promise.all([readFile16(a), readFile16(b)]);
10441
+ const [left, right] = await Promise.all([readFile17(a), readFile17(b)]);
10171
10442
  if (left.length > MAX_COMPARED_BYTES || right.length > MAX_COMPARED_BYTES) return false;
10172
10443
  const x = decodePng(left);
10173
10444
  const y = decodePng(right);
@@ -10177,6 +10448,32 @@ async function samePixels(a, b) {
10177
10448
  }
10178
10449
  }
10179
10450
  var metadataFps = (entry) => entry ? frameSetFps(entry) : null;
10451
+ function describeCharacter(spec, entry) {
10452
+ const recorded = entry?.providerMetadata?.[entry.provider]?.character;
10453
+ if (spec?.character) {
10454
+ const character = spec.character;
10455
+ return {
10456
+ kind: character.kind,
10457
+ parentKey: character.parentAssetId ? lockKey(spec.styleId, character.parentAssetId) : null,
10458
+ mode: character.kind === "animation" ? character.animation.mode : character.mode,
10459
+ directions: character.directions,
10460
+ direction: character.animation?.direction ?? null,
10461
+ characterId: recorded?.characterId ?? entry?.objectId?.split("#")[0] ?? null
10462
+ };
10463
+ }
10464
+ if (entry?.generator === "character") {
10465
+ const kind = recorded?.kind === "animation" || entry.outputs.some((o) => o.role?.startsWith("frame-")) ? "animation" : "base";
10466
+ return {
10467
+ kind,
10468
+ parentKey: null,
10469
+ mode: recorded?.mode ?? "standard",
10470
+ directions: recorded?.directions ?? entry.outputs.length,
10471
+ direction: recorded?.direction ?? null,
10472
+ characterId: recorded?.characterId ?? entry.objectId?.split("#")[0] ?? null
10473
+ };
10474
+ }
10475
+ return null;
10476
+ }
10180
10477
  async function fileInfo(absolutePath) {
10181
10478
  try {
10182
10479
  const info = await stat2(absolutePath);
@@ -10211,9 +10508,9 @@ async function describeOutput(media, root, absolutePath, recorded = {}) {
10211
10508
  async function describeQuality(media, root, inspection) {
10212
10509
  const outputPaths = inspection.outputs ?? [inspection.output];
10213
10510
  const outputs = await Promise.all(
10214
- outputPaths.map((file) => describeOutput(media, root, path27.resolve(root, file)))
10511
+ outputPaths.map((file) => describeOutput(media, root, path28.resolve(root, file)))
10215
10512
  );
10216
- const recordPath = path27.resolve(root, inspection.record);
10513
+ const recordPath = path28.resolve(root, inspection.record);
10217
10514
  const recordExists = existsSync19(recordPath);
10218
10515
  let options = null;
10219
10516
  let check = null;
@@ -10233,7 +10530,7 @@ async function describeQuality(media, root, inspection) {
10233
10530
  return {
10234
10531
  state: inspection.state,
10235
10532
  reason: inspection.reason,
10236
- output: portableOutputPath(path27.resolve(root, inspection.output), root),
10533
+ output: portableOutputPath(path28.resolve(root, inspection.output), root),
10237
10534
  record: portableOutputPath(recordPath, root),
10238
10535
  recordExists,
10239
10536
  outputs,
@@ -10270,9 +10567,9 @@ async function buildGallerySnapshot(opts) {
10270
10567
  const media = /* @__PURE__ */ new Map();
10271
10568
  const plan = await buildPlan(specs, lock);
10272
10569
  const items = [];
10273
- const manifestText = await readFile16(loaded.path, "utf8");
10570
+ const manifestText = await readFile17(loaded.path, "utf8");
10274
10571
  const manifestSha256 = sha256(manifestText);
10275
- const cacheDir = path27.resolve(path27.dirname(opts.lockPath), ".pixelkiln", "cache");
10572
+ const cacheDir = path28.resolve(path28.dirname(opts.lockPath), ".pixelkiln", "cache");
10276
10573
  let rawStyles = {};
10277
10574
  try {
10278
10575
  const raw = JSON.parse(manifestText);
@@ -10296,7 +10593,7 @@ async function buildGallerySnapshot(opts) {
10296
10593
  )
10297
10594
  );
10298
10595
  if (spec.source) {
10299
- const editPath = path27.resolve(root, spec.source);
10596
+ const editPath = path28.resolve(root, spec.source);
10300
10597
  const memberCount = sourceIsStem(spec.source, entry, root) ? entry.outputs.length : 1;
10301
10598
  const editShas = await Promise.all(
10302
10599
  Array.from({ length: memberCount }, async (_, index) => {
@@ -10345,7 +10642,7 @@ async function buildGallerySnapshot(opts) {
10345
10642
  editStatus = edits.some((member) => !member.exists) ? "missing" : editChanged.every((changed) => !changed) ? "same" : regenerated ? "regenerated-since" : "edited";
10346
10643
  }
10347
10644
  } else if (spec.source) {
10348
- outputs = [await describeOutput(media, root, path27.resolve(root, spec.source))];
10645
+ outputs = [await describeOutput(media, root, path28.resolve(root, spec.source))];
10349
10646
  } else if (!entry && existsSync19(spec.outFile)) {
10350
10647
  outputs = [await describeOutput(media, root, spec.outFile)];
10351
10648
  } else {
@@ -10385,6 +10682,8 @@ async function buildGallerySnapshot(opts) {
10385
10682
  currentSpecHash: spec.specHash,
10386
10683
  revision: entry?.revision ?? null,
10387
10684
  revisionParentKey: spec.revision ? lockKey(spec.styleId, spec.revision.sourceAssetId) : entry?.revision ? lockKey(spec.styleId, entry.revision.sourceAssetId) : null,
10685
+ mirrorOfKey: spec.mirror ? lockKey(spec.styleId, spec.mirror.sourceAssetId) : entry?.mirror ? lockKey(spec.styleId, entry.mirror.sourceAssetId) : null,
10686
+ character: describeCharacter(spec, entry),
10388
10687
  outputs,
10389
10688
  quality,
10390
10689
  providerMetadata: entry?.providerMetadata ?? {},
@@ -10446,6 +10745,8 @@ async function buildGallerySnapshot(opts) {
10446
10745
  currentSpecHash: null,
10447
10746
  revision: entry.revision,
10448
10747
  revisionParentKey: entry.revision ? lockKey(entry.styleId, entry.revision.sourceAssetId) : null,
10748
+ mirrorOfKey: entry.mirror ? lockKey(entry.styleId, entry.mirror.sourceAssetId) : null,
10749
+ character: describeCharacter(void 0, entry),
10449
10750
  outputs,
10450
10751
  quality: null,
10451
10752
  providerMetadata: entry.providerMetadata ?? {},
@@ -10502,6 +10803,11 @@ async function buildGallerySnapshot(opts) {
10502
10803
  view: style?.view ?? null,
10503
10804
  noBackground: style?.noBackground ?? true,
10504
10805
  quality: Boolean(style?.quality),
10806
+ characters: style?.generator === "character" ? {
10807
+ bases: styleItems.filter((item) => item.character?.kind === "base").length,
10808
+ states: styleItems.filter((item) => item.character?.kind === "state").length,
10809
+ animations: styleItems.filter((item) => item.character?.kind === "animation").length
10810
+ } : null,
10505
10811
  tags: style?.tags ?? [],
10506
10812
  extends: typeof rawStyle?.extends === "string" ? rawStyle.extends : null,
10507
10813
  ownFields: rawStyle ? Object.keys(rawStyle).filter((key) => key !== "extends") : [],
@@ -10528,7 +10834,7 @@ async function buildGallerySnapshot(opts) {
10528
10834
  id: loaded.manifest.name,
10529
10835
  name: loaded.manifest.name,
10530
10836
  manifest: loaded.path,
10531
- lock: path27.resolve(opts.lockPath),
10837
+ lock: path28.resolve(opts.lockPath),
10532
10838
  root,
10533
10839
  provider: loaded.manifest.provider,
10534
10840
  account: null,
@@ -10554,7 +10860,7 @@ async function buildGallerySnapshot(opts) {
10554
10860
  return { snapshot, media };
10555
10861
  }
10556
10862
  async function buildWorkspaceGallerySnapshot(opts) {
10557
- const dir = path27.dirname(path27.resolve(opts.workspacePath));
10863
+ const dir = path28.dirname(path28.resolve(opts.workspacePath));
10558
10864
  const media = /* @__PURE__ */ new Map();
10559
10865
  const projects = [];
10560
10866
  const items = [];
@@ -10597,11 +10903,11 @@ async function buildWorkspaceGallerySnapshot(opts) {
10597
10903
  id: project.id,
10598
10904
  name: loaded.manifest.name,
10599
10905
  manifest: manifestPath,
10600
- lock: path27.resolve(lockPath),
10906
+ lock: path28.resolve(lockPath),
10601
10907
  root: loaded.root,
10602
10908
  provider: loaded.manifest.provider,
10603
10909
  account: project.account ?? null,
10604
- manifestSha256: manifestSha256 ?? sha256(await readFile16(manifestPath, "utf8")),
10910
+ manifestSha256: manifestSha256 ?? sha256(await readFile17(manifestPath, "utf8")),
10605
10911
  historyLimit: historyLimit(loaded.manifest),
10606
10912
  entries: Object.keys(lock.entries).length,
10607
10913
  items: projectItems.length,
@@ -10613,8 +10919,8 @@ async function buildWorkspaceGallerySnapshot(opts) {
10613
10919
  id: project.id,
10614
10920
  name: project.id,
10615
10921
  manifest: manifestPath,
10616
- lock: path27.resolve(lockPath),
10617
- root: path27.dirname(manifestPath),
10922
+ lock: path28.resolve(lockPath),
10923
+ root: path28.dirname(manifestPath),
10618
10924
  provider: project.provider,
10619
10925
  account: project.account ?? null,
10620
10926
  manifestSha256: null,
@@ -10641,7 +10947,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
10641
10947
  version: 1,
10642
10948
  generatedAt: (opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
10643
10949
  project: null,
10644
- workspace: { path: path27.resolve(opts.workspacePath), dir, projects },
10950
+ workspace: { path: path28.resolve(opts.workspacePath), dir, projects },
10645
10951
  filter,
10646
10952
  totals: {
10647
10953
  entries: projects.reduce((sum, project) => sum + project.entries, 0),
@@ -10660,7 +10966,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
10660
10966
  // src/gallery/server.ts
10661
10967
  import { randomBytes } from "crypto";
10662
10968
  import { createServer as createServer2 } from "http";
10663
- import { readFile as readFile17 } from "fs/promises";
10969
+ import { readFile as readFile18 } from "fs/promises";
10664
10970
 
10665
10971
  // src/gallery/page.ts
10666
10972
  import { readFileSync as readFileSync4 } from "fs";
@@ -10901,7 +11207,7 @@ async function serveGallery(opts) {
10901
11207
  const asset = reviewAssets.get(jobId)?.get(url2.pathname);
10902
11208
  if (!asset) return fail(404, "no such review asset");
10903
11209
  try {
10904
- const bytes = await readFile17(asset.path);
11210
+ const bytes = await readFile18(asset.path);
10905
11211
  res.writeHead(200, {
10906
11212
  "Content-Type": asset.contentType,
10907
11213
  "Cache-Control": "no-store",
@@ -10977,7 +11283,7 @@ async function serveGallery(opts) {
10977
11283
  if (!asset) return fail(404, "no such gallery media");
10978
11284
  let bytes;
10979
11285
  try {
10980
- bytes = await readFile17(asset.path);
11286
+ bytes = await readFile18(asset.path);
10981
11287
  } catch {
10982
11288
  return fail(404, "gallery media is no longer on disk; refresh the page");
10983
11289
  }
@@ -11121,7 +11427,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
11121
11427
  });
11122
11428
  const estimates = new Map(items.map((item) => [
11123
11429
  item.key,
11124
- validateCostEstimate(provider.id, provider.estimate(item.spec))
11430
+ item.spec.mirror ? { amount: 0, unit: item.spec.costUnit, candidates: 1 } : validateCostEstimate(provider.id, provider.estimate(item.spec))
11125
11431
  ]));
11126
11432
  const units = new Set([...estimates.values()].map((estimate) => estimate.unit));
11127
11433
  if (units.size > 1) {
@@ -11171,6 +11477,34 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
11171
11477
  }
11172
11478
  for (const { spec, key } of items) {
11173
11479
  const estimate = estimates.get(key);
11480
+ if (spec.mirror) {
11481
+ try {
11482
+ await requireRevisionReady(spec, lock);
11483
+ await deriveMirror(spec, lock, lockPath, { onProgress: log2 });
11484
+ submitted++;
11485
+ } catch (err) {
11486
+ const message6 = err instanceof Error ? err.message : String(err);
11487
+ failed++;
11488
+ upsert(lock, key, {
11489
+ styleId: spec.styleId,
11490
+ assetId: spec.assetId,
11491
+ specHash: spec.specHash,
11492
+ generator: spec.generator,
11493
+ prompt: "",
11494
+ width: spec.width,
11495
+ height: spec.height,
11496
+ status: "failed",
11497
+ error: message6,
11498
+ outputs: lock.entries[key]?.outputs ?? [],
11499
+ cost: 0,
11500
+ costUnit: spec.costUnit,
11501
+ provider: provider.id
11502
+ });
11503
+ await saveLock(lockPath, lock);
11504
+ log2(` FAILED ${key}: ${message6}`);
11505
+ }
11506
+ continue;
11507
+ }
11174
11508
  await waitForSlot();
11175
11509
  const since = Date.now() - lastSubmitAt;
11176
11510
  if (since < spacing) await sleep2(spacing - since);
@@ -11296,12 +11630,12 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
11296
11630
 
11297
11631
  // src/pick/server.ts
11298
11632
  import { existsSync as existsSync21 } from "fs";
11299
- import { readFile as readFile19 } from "fs/promises";
11633
+ import { readFile as readFile20 } from "fs/promises";
11300
11634
 
11301
11635
  // src/pipeline/init.ts
11302
- import { readdir, readFile as readFile18, writeFile as writeFile11 } from "fs/promises";
11636
+ import { readdir, readFile as readFile19, writeFile as writeFile12 } from "fs/promises";
11303
11637
  import { existsSync as existsSync20 } from "fs";
11304
- import path28 from "path";
11638
+ import path29 from "path";
11305
11639
  function pngSize(buf) {
11306
11640
  if (buf.length < 24) return null;
11307
11641
  const isPng = buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834;
@@ -11315,7 +11649,7 @@ function slugify(value) {
11315
11649
  async function walk(dir, exclude) {
11316
11650
  const out = [];
11317
11651
  for (const entry of await readdir(dir, { withFileTypes: true })) {
11318
- const full = path28.join(dir, entry.name);
11652
+ const full = path29.join(dir, entry.name);
11319
11653
  if (exclude.some((x) => full.includes(x))) continue;
11320
11654
  if (entry.isDirectory()) out.push(...await walk(full, exclude));
11321
11655
  else if (entry.name.toLowerCase().endsWith(".png")) out.push(full);
@@ -11329,14 +11663,14 @@ async function scanAssets(root, opts = {}) {
11329
11663
  const skipped = [];
11330
11664
  const seen = /* @__PURE__ */ new Map();
11331
11665
  for (const file of files.sort()) {
11332
- const size = pngSize(await readFile18(file));
11666
+ const size = pngSize(await readFile19(file));
11333
11667
  if (!size) {
11334
11668
  skipped.push(`${file} (not a readable PNG)`);
11335
11669
  continue;
11336
11670
  }
11337
- const rel = path28.relative(root, file);
11338
- const category = path28.dirname(rel) === "." ? "" : path28.dirname(rel);
11339
- let id = slugify(path28.basename(rel));
11671
+ const rel = path29.relative(root, file);
11672
+ const category = path29.dirname(rel) === "." ? "" : path29.dirname(rel);
11673
+ let id = slugify(path29.basename(rel));
11340
11674
  if (seen.has(id)) {
11341
11675
  const n = seen.get(id) + 1;
11342
11676
  seen.set(id, n);
@@ -11397,7 +11731,7 @@ async function writeManifestFile(target, manifest) {
11397
11731
  if (existsSync20(target)) {
11398
11732
  throw new Error(`${target} already exists; refusing to overwrite an existing manifest.`);
11399
11733
  }
11400
- await writeFile11(target, JSON.stringify(manifest, null, 2) + "\n");
11734
+ await writeFile12(target, JSON.stringify(manifest, null, 2) + "\n");
11401
11735
  }
11402
11736
 
11403
11737
  // src/pick/sheet.ts
@@ -11803,7 +12137,7 @@ async function currentArt(entry, spec) {
11803
12137
  let width = entry.width;
11804
12138
  let height = entry.height;
11805
12139
  if (contentType === "image/png") {
11806
- const size = pngSize(await readFile19(file));
12140
+ const size = pngSize(await readFile20(file));
11807
12141
  if (size) ({ width, height } = size);
11808
12142
  }
11809
12143
  return { path: file, contentType, width, height };
@@ -12296,15 +12630,15 @@ function createGenerateHandlers(opts) {
12296
12630
  }
12297
12631
 
12298
12632
  // src/gallery/editor.ts
12299
- import { readFile as readFile21 } from "fs/promises";
12300
- import path30 from "path";
12633
+ import { readFile as readFile22 } from "fs/promises";
12634
+ import path31 from "path";
12301
12635
 
12302
12636
  // src/editor/install.ts
12303
12637
  import { createHash as createHash4 } from "crypto";
12304
12638
  import { existsSync as existsSync22 } from "fs";
12305
- import { mkdir as mkdir9, readFile as readFile20, rename as rename8, rm as rm9, stat as stat3, writeFile as writeFile12 } from "fs/promises";
12639
+ import { mkdir as mkdir10, readFile as readFile21, rename as rename9, rm as rm9, stat as stat3, writeFile as writeFile13 } from "fs/promises";
12306
12640
  import os2 from "os";
12307
- import path29 from "path";
12641
+ import path30 from "path";
12308
12642
 
12309
12643
  // tools/pixelorama-bridge/pin.json
12310
12644
  var pin_default = {
@@ -12368,15 +12702,15 @@ var EditorInstallError = class extends Error {
12368
12702
  };
12369
12703
  function editorToolsRoot() {
12370
12704
  const override = process.env.PIXELKILN_TOOLS_DIR?.trim();
12371
- if (override) return path29.resolve(override);
12372
- if (process.platform === "darwin") return path29.join(os2.homedir(), "Library", "Caches", "pixelkiln", "tools");
12705
+ if (override) return path30.resolve(override);
12706
+ if (process.platform === "darwin") return path30.join(os2.homedir(), "Library", "Caches", "pixelkiln", "tools");
12373
12707
  if (process.platform === "win32") {
12374
- return path29.join(process.env.LOCALAPPDATA ?? path29.join(os2.homedir(), "AppData", "Local"), "pixelkiln", "tools");
12708
+ return path30.join(process.env.LOCALAPPDATA ?? path30.join(os2.homedir(), "AppData", "Local"), "pixelkiln", "tools");
12375
12709
  }
12376
- return path29.join(process.env.XDG_CACHE_HOME ?? path29.join(os2.homedir(), ".cache"), "pixelkiln", "tools");
12710
+ return path30.join(process.env.XDG_CACHE_HOME ?? path30.join(os2.homedir(), ".cache"), "pixelkiln", "tools");
12377
12711
  }
12378
12712
  function editorDir(pin = EDITOR_PIN, root = editorToolsRoot()) {
12379
- return path29.join(root, "pixelorama", pin.release ?? "unpublished");
12713
+ return path30.join(root, "pixelorama", pin.release ?? "unpublished");
12380
12714
  }
12381
12715
  function editorBaseUrl(pin = EDITOR_PIN) {
12382
12716
  const override = process.env.PIXELKILN_EDITOR_URL?.trim();
@@ -12386,7 +12720,7 @@ function editorBaseUrl(pin = EDITOR_PIN) {
12386
12720
  }
12387
12721
  async function sha256Of(file) {
12388
12722
  try {
12389
- return createHash4("sha256").update(await readFile20(file)).digest("hex");
12723
+ return createHash4("sha256").update(await readFile21(file)).digest("hex");
12390
12724
  } catch {
12391
12725
  return null;
12392
12726
  }
@@ -12399,7 +12733,7 @@ async function editorStatus(opts = {}) {
12399
12733
  let totalBytes = 0;
12400
12734
  for (const [name, expected] of Object.entries(pin.files)) {
12401
12735
  totalBytes += expected.bytes;
12402
- const file = path29.join(dir, name);
12736
+ const file = path30.join(dir, name);
12403
12737
  const info = existsSync22(file) ? await stat3(file).catch(() => null) : null;
12404
12738
  if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
12405
12739
  installedBytes += expected.bytes;
@@ -12429,7 +12763,7 @@ async function installEditor(opts = {}) {
12429
12763
  );
12430
12764
  }
12431
12765
  if (!baseUrl) throw new EditorInstallError("no download location for the editor build");
12432
- await mkdir9(dir, { recursive: true });
12766
+ await mkdir10(dir, { recursive: true });
12433
12767
  const downloaded = [];
12434
12768
  const skipped = Object.keys(pin.files).filter((name) => !before.missing.includes(name));
12435
12769
  let fetchedBytes = before.installedBytes;
@@ -12457,11 +12791,11 @@ async function installEditor(opts = {}) {
12457
12791
  if (digest2 !== expected.sha256) {
12458
12792
  throw new EditorInstallError(`${name}: sha256 ${digest2.slice(0, 12)}\u2026 does not match the pinned ${expected.sha256.slice(0, 12)}\u2026`, name);
12459
12793
  }
12460
- const final = path29.join(dir, name);
12794
+ const final = path30.join(dir, name);
12461
12795
  const tmp = `${final}.${process.pid}.part`;
12462
12796
  try {
12463
- await writeFile12(tmp, bytes);
12464
- await rename8(tmp, final);
12797
+ await writeFile13(tmp, bytes);
12798
+ await rename9(tmp, final);
12465
12799
  } finally {
12466
12800
  await rm9(tmp, { force: true });
12467
12801
  }
@@ -12503,7 +12837,7 @@ var EDITOR_CONTENT_TYPES = {
12503
12837
  ".json": "application/json; charset=utf-8"
12504
12838
  };
12505
12839
  function editorContentType(name) {
12506
- return EDITOR_CONTENT_TYPES[path29.extname(name).toLowerCase()] ?? null;
12840
+ return EDITOR_CONTENT_TYPES[path30.extname(name).toLowerCase()] ?? null;
12507
12841
  }
12508
12842
 
12509
12843
  // src/gallery/editor.ts
@@ -12579,7 +12913,7 @@ function createGalleryEditorHandlers(opts = {}) {
12579
12913
  if (!status.installed) return null;
12580
12914
  let bytes;
12581
12915
  try {
12582
- bytes = await readFile21(path30.join(dir, name));
12916
+ bytes = await readFile22(path31.join(dir, name));
12583
12917
  } catch {
12584
12918
  verified = null;
12585
12919
  return null;
@@ -12619,13 +12953,13 @@ async function serveUntilStopped(initial, reload, args, access2) {
12619
12953
  };
12620
12954
  const loadProject = async (project) => {
12621
12955
  const ctx = await access2.loadProject(project);
12622
- const dir = path31.dirname(ctx.loaded.path);
12956
+ const dir = path32.dirname(ctx.loaded.path);
12623
12957
  const declared = readEnvFiles(dir);
12624
12958
  const credentialNames = new Set(availableProviders().flatMap((id) => providerCredentialEnvs(providerFactory(id))));
12625
12959
  for (const [name, value] of Object.entries(declared)) {
12626
12960
  if (credentialNames.has(name) && name in process.env && process.env[name] !== value) {
12627
12961
  throw new Error(
12628
- `${path31.relative(process.cwd(), dir) || "."} sets ${name} to a different value than the one this gallery already loaded; run a separate gallery for that project so its work uses its own account.`
12962
+ `${path32.relative(process.cwd(), dir) || "."} sets ${name} to a different value than the one this gallery already loaded; run a separate gallery for that project so its work uses its own account.`
12629
12963
  );
12630
12964
  }
12631
12965
  }
@@ -12670,7 +13004,7 @@ async function serveUntilStopped(initial, reload, args, access2) {
12670
13004
  }
12671
13005
  async function runGallery(args) {
12672
13006
  if (args.workspace) {
12673
- const workspacePath = path31.resolve(args.workspace);
13007
+ const workspacePath = path32.resolve(args.workspace);
12674
13008
  if (!existsSync23(workspacePath)) {
12675
13009
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
12676
13010
  }
@@ -12691,7 +13025,7 @@ async function runGallery(args) {
12691
13025
  const registered = async (projectId) => {
12692
13026
  const project = (await loadWorkspace(workspacePath)).projects.find((candidate) => candidate.id === projectId);
12693
13027
  if (!projectId || !project) throw new Error(`unknown workspace project "${projectId ?? ""}"`);
12694
- return resolveProject(path31.dirname(workspacePath), project);
13028
+ return resolveProject(path32.dirname(workspacePath), project);
12695
13029
  };
12696
13030
  await serveUntilStopped(initial, build2, args, {
12697
13031
  manifestFor: async (projectId) => (await registered(projectId)).manifestPath,
@@ -12716,7 +13050,7 @@ async function runGallery(args) {
12716
13050
  return buildGallerySnapshot({ ...fresh, filter });
12717
13051
  };
12718
13052
  await serveUntilStopped(await build(), reload, args, {
12719
- manifestFor: () => path31.resolve(args.manifest),
13053
+ manifestFor: () => path32.resolve(args.manifest),
12720
13054
  loadProject: () => openProject(args.manifest, { lockPath: args.lock, env: false })
12721
13055
  });
12722
13056
  }
@@ -12959,7 +13293,7 @@ async function runLifecycle(args, wave, spent) {
12959
13293
  log(` Packaging stays blocked until each refined PNG has a current human approval.`);
12960
13294
  }
12961
13295
  }
12962
- return args.command === "gen" && submitted > 0 && total.downloaded > 0 && !total.failed && specs.some((spec) => spec.character?.parentSpec || spec.revision);
13296
+ return args.command === "gen" && submitted > 0 && total.downloaded > 0 && !total.failed && specs.some((spec) => spec.character?.parentSpec || spec.revision || spec.mirror);
12963
13297
  }
12964
13298
  return false;
12965
13299
  }
@@ -13025,14 +13359,14 @@ import { existsSync as existsSync25 } from "fs";
13025
13359
 
13026
13360
  // src/pipeline/cache-health.ts
13027
13361
  import { existsSync as existsSync24 } from "fs";
13028
- import { readFile as readFile22, readdir as readdir2, rm as rm10 } from "fs/promises";
13029
- import path32 from "path";
13362
+ import { readFile as readFile23, readdir as readdir2, rm as rm10 } from "fs/promises";
13363
+ import path33 from "path";
13030
13364
  async function inspectCaches(lock, lockPath, options = {}) {
13031
13365
  if (options.prune && !existsSync24(lockPath)) {
13032
- throw new Error(`Refusing to prune without an existing lockfile at ${path32.resolve(lockPath)}`);
13366
+ throw new Error(`Refusing to prune without an existing lockfile at ${path33.resolve(lockPath)}`);
13033
13367
  }
13034
- const contentDir = path32.resolve(path32.dirname(lockPath), ".pixelkiln", "cache");
13035
- const remotePath = path32.resolve(cachePathFor(lockPath));
13368
+ const contentDir = path33.resolve(path33.dirname(lockPath), ".pixelkiln", "cache");
13369
+ const remotePath = path33.resolve(cachePathFor(lockPath));
13036
13370
  const referenced = referencedHashes(lock);
13037
13371
  let content = await inspectContentCache(contentDir, referenced);
13038
13372
  let remoteHashes = await inspectRemoteHashCache(remotePath);
@@ -13044,7 +13378,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
13044
13378
  ]);
13045
13379
  for (const name of names) {
13046
13380
  try {
13047
- await rm10(path32.join(contentDir, name), { force: true });
13381
+ await rm10(path33.join(contentDir, name), { force: true });
13048
13382
  removed.contentFiles++;
13049
13383
  } catch {
13050
13384
  }
@@ -13053,7 +13387,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
13053
13387
  await saveCache(remotePath, { version: 1, hashes: {} });
13054
13388
  removed.resetRemoteHashCache = true;
13055
13389
  } else if (remoteHashes.invalidIds.length) {
13056
- const cache = parseCache(JSON.parse(await readFile22(remotePath, "utf8")));
13390
+ const cache = parseCache(JSON.parse(await readFile23(remotePath, "utf8")));
13057
13391
  for (const id of remoteHashes.invalidIds) delete cache.hashes[id];
13058
13392
  removed.remoteHashEntries = remoteHashes.invalidIds.length;
13059
13393
  await saveCache(remotePath, cache);
@@ -13092,12 +13426,12 @@ async function inspectContentCache(contentDir, referenced) {
13092
13426
  continue;
13093
13427
  }
13094
13428
  report.files++;
13095
- const file = path32.join(contentDir, entry.name);
13429
+ const file = path33.join(contentDir, entry.name);
13096
13430
  const mediaType = mediaTypeFromExtension(entry.name);
13097
13431
  const expected = mediaType ? entry.name.slice(0, -4) : "";
13098
13432
  let bytes;
13099
13433
  try {
13100
- bytes = await readFile22(file);
13434
+ bytes = await readFile23(file);
13101
13435
  report.bytes += bytes.length;
13102
13436
  } catch (err) {
13103
13437
  report.invalid.push({
@@ -13144,7 +13478,7 @@ async function inspectRemoteHashCache(remotePath) {
13144
13478
  if (!report.exists) return report;
13145
13479
  let cache;
13146
13480
  try {
13147
- cache = parseCache(JSON.parse(await readFile22(remotePath, "utf8")));
13481
+ cache = parseCache(JSON.parse(await readFile23(remotePath, "utf8")));
13148
13482
  } catch (err) {
13149
13483
  report.error = err instanceof Error ? err.message : String(err);
13150
13484
  return report;
@@ -13263,42 +13597,42 @@ This drops their provenance from the lockfile. The art on disk is`);
13263
13597
  }
13264
13598
 
13265
13599
  // src/cli/commands/init.ts
13266
- import path33 from "path";
13600
+ import path34 from "path";
13267
13601
  import { existsSync as existsSync26 } from "fs";
13268
13602
  async function runInit(args) {
13269
13603
  if (!args.from) throw new Error("init needs --from <dir> pointing at your existing PNGs.");
13270
- const root = path33.resolve(args.from);
13604
+ const root = path34.resolve(args.from);
13271
13605
  if (!existsSync26(root)) throw new Error(`No directory at ${root}`);
13272
13606
  const generator = args.generator ?? "map";
13273
13607
  if (generator !== "1dir" && generator !== "map") {
13274
13608
  throw new Error(`--generator must be "1dir" or "map", got "${args.generator}".`);
13275
13609
  }
13276
- const target = path33.resolve(args.out ?? "pixelkiln.manifest.json");
13610
+ const target = path34.resolve(args.out ?? "pixelkiln.manifest.json");
13277
13611
  const { assets, skipped } = await scanAssets(root, { exclude: args.exclude });
13278
13612
  if (!assets.length) throw new Error(`No PNGs found under ${root}`);
13279
13613
  const manifest = buildManifest(
13280
- args.name ?? path33.basename(path33.dirname(target)),
13614
+ args.name ?? path34.basename(path34.dirname(target)),
13281
13615
  args.styles[0] ?? "base",
13282
13616
  generator,
13283
- path33.relative(path33.dirname(target), root) || ".",
13617
+ path34.relative(path34.dirname(target), root) || ".",
13284
13618
  assets
13285
13619
  );
13286
13620
  await writeManifestFile(target, manifest);
13287
- log(` scanned ${assets.length} PNG(s) under ${path33.relative(process.cwd(), root)}`);
13621
+ log(` scanned ${assets.length} PNG(s) under ${path34.relative(process.cwd(), root)}`);
13288
13622
  if (skipped.length) log(` skipped ${skipped.length} unreadable file(s)`);
13289
- log(` wrote ${path33.relative(process.cwd(), target)}`);
13623
+ log(` wrote ${path34.relative(process.cwd(), target)}`);
13290
13624
  log(`
13291
13625
  Prompts are intentionally empty. To recover the real ones from your`);
13292
13626
  log(` PixelLab account instead of inventing them:`);
13293
13627
  log(`
13294
- pixelkiln adopt --manifest ${path33.relative(process.cwd(), target)} --write-prompts
13628
+ pixelkiln adopt --manifest ${path34.relative(process.cwd(), target)} --write-prompts
13295
13629
  `);
13296
13630
  }
13297
13631
 
13298
13632
  // src/pipeline/doctor.ts
13299
13633
  import { constants, existsSync as existsSync27 } from "fs";
13300
13634
  import { access, stat as stat4 } from "fs/promises";
13301
- import path34 from "path";
13635
+ import path35 from "path";
13302
13636
  async function doctor(loaded, specs, lock, lockPath, opts = {}) {
13303
13637
  const checks = [];
13304
13638
  const add = (id, level, message6) => checks.push({ id, level, message: message6 });
@@ -13309,8 +13643,8 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
13309
13643
  );
13310
13644
  add("lock", "ok", `${Object.keys(lock.entries).length} valid v2 lock entr(ies)`);
13311
13645
  const dirs = [...new Set(specs.flatMap((spec) => [
13312
- path34.dirname(spec.outFile),
13313
- ...spec.quality ? [path34.dirname(spec.quality.outFile)] : []
13646
+ path35.dirname(spec.outFile),
13647
+ ...spec.quality ? [path35.dirname(spec.quality.outFile)] : []
13314
13648
  ]))];
13315
13649
  const unwritable = [];
13316
13650
  for (const dir of dirs) {
@@ -13333,7 +13667,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
13333
13667
  for (let index = 0; index < entry.outputs.length; index++) {
13334
13668
  const output = entry.outputs[index];
13335
13669
  const spec = specByKey.get(key);
13336
- const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, path34.dirname(lockPath));
13670
+ const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, path35.dirname(lockPath));
13337
13671
  const owner = outputOwners.get(absolute);
13338
13672
  if (owner && owner !== key) duplicateOutputs.push(`${output.path} (${owner}, ${key})`);
13339
13673
  outputOwners.set(absolute, key);
@@ -13348,7 +13682,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
13348
13682
  } else {
13349
13683
  add("lock-outputs", "ok", `${outputOwners.size} recorded output path(s) are uniquely owned`);
13350
13684
  }
13351
- const cacheDir = path34.join(path34.dirname(lockPath), ".pixelkiln", "cache");
13685
+ const cacheDir = path35.join(path35.dirname(lockPath), ".pixelkiln", "cache");
13352
13686
  const stranded = [];
13353
13687
  const invalidState = [];
13354
13688
  const staleMapUrls = [];
@@ -13364,7 +13698,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
13364
13698
  const hasSource = Boolean(entry.sourceUrl || entry.sourceUrls?.length);
13365
13699
  let hasCache = false;
13366
13700
  for (const output of entry.outputs) {
13367
- const cached = path34.join(
13701
+ const cached = path35.join(
13368
13702
  cacheDir,
13369
13703
  cacheFileName(output.sha256, output.mediaType ?? MediaType.PNG)
13370
13704
  );
@@ -13464,16 +13798,16 @@ async function nearestExistingDirectory(start) {
13464
13798
  if ((await stat4(current)).isDirectory()) return current;
13465
13799
  } catch {
13466
13800
  }
13467
- const parent = path34.dirname(current);
13801
+ const parent = path35.dirname(current);
13468
13802
  if (parent === current) return current;
13469
13803
  current = parent;
13470
13804
  }
13471
13805
  }
13472
13806
 
13473
13807
  // src/pipeline/audit.ts
13474
- import { readFile as readFile23 } from "fs/promises";
13808
+ import { readFile as readFile24 } from "fs/promises";
13475
13809
  import { existsSync as existsSync28 } from "fs";
13476
- import path35 from "path";
13810
+ import path36 from "path";
13477
13811
  function paletteDistance(asset, reference) {
13478
13812
  if (!asset.length || !reference.length) return 0;
13479
13813
  let total = 0;
@@ -13514,7 +13848,7 @@ async function auditStyle(loaded, specs, styleId, lock) {
13514
13848
  continue;
13515
13849
  }
13516
13850
  try {
13517
- const png = decodePng(await readFile23(output.absolutePath));
13851
+ const png = decodePng(await readFile24(output.absolutePath));
13518
13852
  const palette = extractPalette(png, 12);
13519
13853
  assets.push({
13520
13854
  assetId: spec.assetId,
@@ -13538,10 +13872,10 @@ async function auditStyle(loaded, specs, styleId, lock) {
13538
13872
  let referenceFromStyleImages = false;
13539
13873
  const refPalettes = [];
13540
13874
  for (const img of style.styleImages) {
13541
- const abs = path35.resolve(loaded.root, img.path);
13875
+ const abs = path36.resolve(loaded.root, img.path);
13542
13876
  if (!existsSync28(abs)) continue;
13543
13877
  try {
13544
- refPalettes.push(extractPalette(decodePng(await readFile23(abs)), 12));
13878
+ refPalettes.push(extractPalette(decodePng(await readFile24(abs)), 12));
13545
13879
  } catch {
13546
13880
  }
13547
13881
  }
@@ -13761,24 +14095,24 @@ async function runAudit(args) {
13761
14095
  }
13762
14096
 
13763
14097
  // src/cli/commands/pack.ts
13764
- import path37 from "path";
13765
- import { readFile as readFile24 } from "fs/promises";
14098
+ import path38 from "path";
14099
+ import { readFile as readFile25 } from "fs/promises";
13766
14100
 
13767
14101
  // src/pipeline/pack.ts
13768
14102
  import { readFileSync as readFileSync5 } from "fs";
13769
- import path36 from "path";
14103
+ import path37 from "path";
13770
14104
  var DEFAULT_FRAME_SET_FPS = 12;
13771
14105
  function resolvePackInputs(raw, inputsFilePath) {
13772
14106
  if (!Array.isArray(raw) || !raw.length) {
13773
14107
  throw new Error("--inputs must be a non-empty JSON array of { id, path }");
13774
14108
  }
13775
- const baseDir = path36.dirname(path36.resolve(inputsFilePath));
14109
+ const baseDir = path37.dirname(path37.resolve(inputsFilePath));
13776
14110
  return raw.map((entry, i) => {
13777
14111
  const e = entry;
13778
14112
  if (typeof e.id !== "string" || typeof e.path !== "string") {
13779
14113
  throw new Error(`--inputs[${i}] needs string "id" and "path"`);
13780
14114
  }
13781
- return { id: e.id, path: path36.resolve(baseDir, e.path) };
14115
+ return { id: e.id, path: path37.resolve(baseDir, e.path) };
13782
14116
  });
13783
14117
  }
13784
14118
  function packSprites(inputs, options = {}) {
@@ -13850,7 +14184,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
13850
14184
  }
13851
14185
  const inputs2 = Object.entries(options.sourceOverrides).map(([id, source]) => ({
13852
14186
  id,
13853
- path: path36.resolve(source)
14187
+ path: path37.resolve(source)
13854
14188
  }));
13855
14189
  if (!inputs2.length) throw new Error(`No approved quality outputs for style "${styleId}".`);
13856
14190
  const packed2 = packSprites(inputs2, options);
@@ -13874,7 +14208,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
13874
14208
  let outputs = resolveEntryOutputs(entry, id, manifestDir);
13875
14209
  const source = sources[id];
13876
14210
  if (source && outputs.length <= 1) {
13877
- inputs.push({ id, path: path36.resolve(manifestDir, source) });
14211
+ inputs.push({ id, path: path37.resolve(manifestDir, source) });
13878
14212
  continue;
13879
14213
  }
13880
14214
  if (source && sourceIsStem(source, entry, manifestDir)) {
@@ -13908,7 +14242,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
13908
14242
  }
13909
14243
  }
13910
14244
  for (const [id, source] of Object.entries(sources)) {
13911
- if (!locked.has(id)) inputs.push({ id, path: path36.resolve(manifestDir, source) });
14245
+ if (!locked.has(id)) inputs.push({ id, path: path37.resolve(manifestDir, source) });
13912
14246
  }
13913
14247
  if (!inputs.length) {
13914
14248
  throw new Error(`No readable sprites for style "${styleId}"; ${noOutput.length} skipped.`);
@@ -14016,7 +14350,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
14016
14350
  const source = sources[id];
14017
14351
  const entry = lock.entries[`${styleId}/${id}`];
14018
14352
  if (source && !(entry && sourceIsStem(source, entry, manifestDir))) {
14019
- placements.push({ id, path: path36.resolve(manifestDir, source), cell });
14353
+ placements.push({ id, path: path37.resolve(manifestDir, source), cell });
14020
14354
  continue;
14021
14355
  }
14022
14356
  if (source && entry) {
@@ -14049,7 +14383,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
14049
14383
  `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}`
14050
14384
  );
14051
14385
  }
14052
- const basePng = mount.base ? readFileSync5(path36.resolve(manifestDir, mount.base)) : void 0;
14386
+ const basePng = mount.base ? readFileSync5(path37.resolve(manifestDir, mount.base)) : void 0;
14053
14387
  const mounted = mountSprites(placements, {
14054
14388
  cellWidth: mount.cellWidth,
14055
14389
  cellHeight: mount.cellHeight,
@@ -14059,7 +14393,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
14059
14393
  ...mounted,
14060
14394
  sources: mount.base ? [{
14061
14395
  id: "$base",
14062
- path: path36.resolve(manifestDir, mount.base),
14396
+ path: path37.resolve(manifestDir, mount.base),
14063
14397
  sha256: sha256(basePng),
14064
14398
  included: true
14065
14399
  }, ...mounted.sources] : mounted.sources,
@@ -14426,12 +14760,12 @@ async function runPack(args) {
14426
14760
  if (args.primaryOnly || args.outputRoles.length) {
14427
14761
  throw new Error("--primary-only and --output-role require manifest-driven pack");
14428
14762
  }
14429
- const raw = JSON.parse(await readFile24(path37.resolve(args.inputs), "utf8"));
14763
+ const raw = JSON.parse(await readFile25(path38.resolve(args.inputs), "utf8"));
14430
14764
  const inputs = resolvePackInputs(raw, args.inputs);
14431
14765
  const { png, atlas, skipped, sources } = packSprites(inputs, { columns: args.columns });
14432
14766
  const format = sheetFormat(args);
14433
- const base = path37.resolve(args.out.replace(/\.(?:png|json|tres)$/i, ""));
14434
- const { extension, document } = renderSheetDocument(format, atlas, { imageName: path37.basename(`${base}.png`) });
14767
+ const base = path38.resolve(args.out.replace(/\.(?:png|json|tres)$/i, ""));
14768
+ const { extension, document } = renderSheetDocument(format, atlas, { imageName: path38.basename(`${base}.png`) });
14435
14769
  const outputs = [
14436
14770
  { path: `${base}.png`, data: png },
14437
14771
  { path: `${base}${extension}`, data: document }
@@ -14444,7 +14778,7 @@ async function runPack(args) {
14444
14778
  log(
14445
14779
  ` ${atlas.frames.length} sprite(s), ${atlas.sheet.width}x${atlas.sheet.height} in ${atlas.columns} column(s), ${(png.length / 1024).toFixed(1)} KB (${format})`
14446
14780
  );
14447
- log(` ${path37.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
14781
+ log(` ${path38.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
14448
14782
  for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
14449
14783
  return;
14450
14784
  }
@@ -14452,7 +14786,7 @@ async function runPack(args) {
14452
14786
  if (args.primaryOnly && args.outputRoles.length) {
14453
14787
  throw new Error("pack accepts either --primary-only or --output-role, not both");
14454
14788
  }
14455
- const manifestDir = path37.dirname(path37.resolve(args.manifest));
14789
+ const manifestDir = path38.dirname(path38.resolve(args.manifest));
14456
14790
  const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
14457
14791
  for (const styleId of styleIds) {
14458
14792
  const packagingSpecs = await resolveSpecs(loaded, { styles: [styleId] });
@@ -14465,9 +14799,9 @@ async function runPack(args) {
14465
14799
  sources: manifestSources(loaded.manifest, styleId)
14466
14800
  });
14467
14801
  const style = loaded.manifest.styles[styleId];
14468
- const base = args.out ? path37.resolve(args.out.replace(/\.(?:png|json|tres)$/i, "")) : path37.resolve(manifestDir, style.outDir, `${styleId}-sheet`);
14802
+ const base = args.out ? path38.resolve(args.out.replace(/\.(?:png|json|tres)$/i, "")) : path38.resolve(manifestDir, style.outDir, `${styleId}-sheet`);
14469
14803
  const format = sheetFormat(args);
14470
- const { extension, document } = renderSheetDocument(format, atlas, { imageName: path37.basename(`${base}.png`) });
14804
+ const { extension, document } = renderSheetDocument(format, atlas, { imageName: path38.basename(`${base}.png`) });
14471
14805
  const outputs = [
14472
14806
  { path: `${base}.png`, data: png },
14473
14807
  { path: `${base}${extension}`, data: document }
@@ -14498,13 +14832,13 @@ async function runPack(args) {
14498
14832
  log(
14499
14833
  ` ${styleId}: ${atlas.frames.length} sprite(s), ${atlas.sheet.width}x${atlas.sheet.height} in ${atlas.columns} column(s)` + (atlas.sets?.length ? `, ${atlas.sets.length} set(s)` : "") + ` (${format})`
14500
14834
  );
14501
- log(` ${path37.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
14835
+ log(` ${path38.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
14502
14836
  for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
14503
14837
  }
14504
14838
  }
14505
14839
  async function runMount(args) {
14506
14840
  const { loaded, lock } = await openProject2(args);
14507
- const manifestDir = path37.dirname(path37.resolve(args.manifest));
14841
+ const manifestDir = path38.dirname(path38.resolve(args.manifest));
14508
14842
  const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
14509
14843
  for (const styleId of styleIds) {
14510
14844
  const style = loaded.manifest.styles[styleId];
@@ -14543,9 +14877,9 @@ async function runMount(args) {
14543
14877
  sources,
14544
14878
  outputRoles
14545
14879
  );
14546
- const out = path37.resolve(manifestDir, style.mount.out);
14880
+ const out = path38.resolve(manifestDir, style.mount.out);
14547
14881
  const format = sheetFormat(args);
14548
- const { extension, document } = renderSheetDocument(format, atlas, { imageName: path37.basename(out) });
14882
+ const { extension, document } = renderSheetDocument(format, atlas, { imageName: path38.basename(out) });
14549
14883
  const metadata = out.replace(/\.png$/, "") + extension;
14550
14884
  const companion = out.replace(/\.png$/, "") + ".pixelkiln.json";
14551
14885
  const outputs = [
@@ -14565,7 +14899,7 @@ async function runMount(args) {
14565
14899
  await provenanceFile("$lock", args.lock),
14566
14900
  ...qualityRecords,
14567
14901
  ...artifactSources.filter(
14568
- (source) => source.id !== "$base" || path37.resolve(source.path) !== out
14902
+ (source) => source.id !== "$base" || path38.resolve(source.path) !== out
14569
14903
  )
14570
14904
  ],
14571
14905
  options: {
@@ -14579,14 +14913,14 @@ async function runMount(args) {
14579
14913
  log(
14580
14914
  ` ${styleId}: ${atlas.frames.length} cell(s) into ${atlas.sheet.width}x${atlas.sheet.height}` + (overBase ? ` over ${style.mount.base}` : " (new sheet)")
14581
14915
  );
14582
- log(` ${path37.relative(process.cwd(), out)} + atlas/provenance JSON`);
14916
+ log(` ${path38.relative(process.cwd(), out)} + atlas/provenance JSON`);
14583
14917
  for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
14584
14918
  }
14585
14919
  }
14586
14920
  async function runExport(args) {
14587
14921
  const { loaded, specs, lock } = await openProject2(args);
14588
14922
  const format = args.format ?? "generic";
14589
- const manifestDir = path37.dirname(path37.resolve(args.manifest));
14923
+ const manifestDir = path38.dirname(path38.resolve(args.manifest));
14590
14924
  const selected = specs.filter((spec) => {
14591
14925
  if (spec.generator !== "tiles") return false;
14592
14926
  if (args.styles.length && !args.styles.includes(spec.styleId)) return false;
@@ -14602,12 +14936,12 @@ async function runExport(args) {
14602
14936
  for (const spec of selected) {
14603
14937
  const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
14604
14938
  const style = loaded.manifest.styles[spec.styleId];
14605
- const defaultBase = path37.resolve(manifestDir, style.outDir, `${spec.assetId}-tileset`);
14606
- const base = args.out ? path37.resolve(args.out.replace(/\.(?:png|json|tsj|tres)$/i, "")) : defaultBase;
14939
+ const defaultBase = path38.resolve(manifestDir, style.outDir, `${spec.assetId}-tileset`);
14940
+ const base = args.out ? path38.resolve(args.out.replace(/\.(?:png|json|tsj|tres)$/i, "")) : defaultBase;
14607
14941
  const result = exportTileset(entry, spec, {
14608
14942
  format,
14609
14943
  manifestDir,
14610
- imageName: path37.basename(`${base}.png`),
14944
+ imageName: path38.basename(`${base}.png`),
14611
14945
  columns: args.columns
14612
14946
  });
14613
14947
  const outputs = [
@@ -14625,7 +14959,7 @@ async function runExport(args) {
14625
14959
  asset: spec.assetId,
14626
14960
  columns: args.columns ?? null,
14627
14961
  format,
14628
- image: path37.basename(`${base}.png`),
14962
+ image: path38.basename(`${base}.png`),
14629
14963
  providerRules: result.generic.providerRules,
14630
14964
  style: spec.styleId,
14631
14965
  tileType: spec.tileType ?? null
@@ -14635,7 +14969,7 @@ async function runExport(args) {
14635
14969
  ` ${spec.styleId}/${spec.assetId}: ${result.generic.tiles.length} tile(s), ${result.generic.sheet.width}x${result.generic.sheet.height} (${format})`
14636
14970
  );
14637
14971
  log(
14638
- ` ${path37.relative(process.cwd(), base)}.png + ${path37.basename(base)}${result.extension} + .pixelkiln.json`
14972
+ ` ${path38.relative(process.cwd(), base)}.png + ${path38.basename(base)}${result.extension} + .pixelkiln.json`
14639
14973
  );
14640
14974
  }
14641
14975
  }
@@ -14648,19 +14982,19 @@ function sheetFormat(args) {
14648
14982
  }
14649
14983
 
14650
14984
  // src/cli/commands/quality.ts
14651
- import path39 from "path";
14652
- import { readFile as readFile26 } from "fs/promises";
14985
+ import path40 from "path";
14986
+ import { readFile as readFile27 } from "fs/promises";
14653
14987
 
14654
14988
  // src/pipeline/quality-regression.ts
14655
- import path38 from "path";
14989
+ import path39 from "path";
14656
14990
  import { existsSync as existsSync29 } from "fs";
14657
- import { readFile as readFile25 } from "fs/promises";
14991
+ import { readFile as readFile26 } from "fs/promises";
14658
14992
  import { z as z9 } from "zod";
14659
14993
  var SHA256_RE = /^[0-9a-f]{64}$/;
14660
14994
  var HEX_RE = /^#[0-9a-f]{6}$/;
14661
14995
  var MAX_REDMEAN_DISTANCE = 765;
14662
14996
  var QualityPathSchema = z9.string().min(1).refine(
14663
- (value) => !path38.posix.isAbsolute(value) && !value.includes("\\"),
14997
+ (value) => !path39.posix.isAbsolute(value) && !value.includes("\\"),
14664
14998
  "expected a portable relative path"
14665
14999
  );
14666
15000
  var DEFAULT_QUALITY_TOLERANCES = {
@@ -14807,10 +15141,10 @@ function measureDecodedImageQuality(png, digest2) {
14807
15141
  };
14808
15142
  }
14809
15143
  async function measureImageQuality(file) {
14810
- const absolute = path38.resolve(file);
15144
+ const absolute = path39.resolve(file);
14811
15145
  let bytes;
14812
15146
  try {
14813
- bytes = await readFile25(absolute);
15147
+ bytes = await readFile26(absolute);
14814
15148
  } catch (error) {
14815
15149
  throw new Error(`Cannot read quality image ${absolute}: ${message4(error)}`, { cause: error });
14816
15150
  }
@@ -14829,11 +15163,11 @@ function resolveQualityInputs(raw, inputsFilePath) {
14829
15163
  }
14830
15164
  const ids = parsed.data.map((entry) => entry.id);
14831
15165
  if (new Set(ids).size !== ids.length) throw new Error("--inputs contains a duplicate quality case id.");
14832
- const root = path38.dirname(path38.resolve(inputsFilePath));
15166
+ const root = path39.dirname(path39.resolve(inputsFilePath));
14833
15167
  return parsed.data.map((entry) => ({
14834
15168
  id: entry.id,
14835
- file: path38.resolve(root, entry.path),
14836
- ...entry.record ? { record: path38.resolve(root, entry.record) } : {},
15169
+ file: path39.resolve(root, entry.path),
15170
+ ...entry.record ? { record: path39.resolve(root, entry.record) } : {},
14837
15171
  tolerances: QualityToleranceSchema.parse({
14838
15172
  ...DEFAULT_QUALITY_TOLERANCES,
14839
15173
  ...entry.tolerances
@@ -14841,7 +15175,7 @@ function resolveQualityInputs(raw, inputsFilePath) {
14841
15175
  }));
14842
15176
  }
14843
15177
  function portableRelative2(from, to) {
14844
- return path38.relative(from, path38.resolve(to)).split(path38.sep).join("/") || ".";
15178
+ return path39.relative(from, path39.resolve(to)).split(path39.sep).join("/") || ".";
14845
15179
  }
14846
15180
  async function verifyRecordForImage(recordPath, imagePath) {
14847
15181
  const verification = await verifyArtifactBundle(recordPath);
@@ -14856,20 +15190,20 @@ async function verifyRecordForImage(recordPath, imagePath) {
14856
15190
  if (record.kind !== "refine" || options === null || typeof options !== "object" || options.schema !== "pixelkiln-quality") {
14857
15191
  throw new Error(`Quality record ${recordPath} is not a PixelKiln refinement record.`);
14858
15192
  }
14859
- const outputs = record.outputs.map((output) => path38.resolve(path38.dirname(recordPath), output.path));
14860
- if (!outputs.includes(path38.resolve(imagePath))) {
15193
+ const outputs = record.outputs.map((output) => path39.resolve(path39.dirname(recordPath), output.path));
15194
+ if (!outputs.includes(path39.resolve(imagePath))) {
14861
15195
  throw new Error(`Quality record ${recordPath} does not own ${imagePath}.`);
14862
15196
  }
14863
15197
  }
14864
15198
  async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
14865
15199
  if (!inputs.length) throw new Error("Cannot snapshot an empty quality baseline.");
14866
- const absolute = path38.resolve(baselinePath);
15200
+ const absolute = path39.resolve(baselinePath);
14867
15201
  for (const input of inputs) {
14868
- if (path38.resolve(input.file) === absolute || input.record && path38.resolve(input.record) === absolute) {
15202
+ if (path39.resolve(input.file) === absolute || input.record && path39.resolve(input.record) === absolute) {
14869
15203
  throw new Error(`Quality baseline output would overwrite an input: ${absolute}.`);
14870
15204
  }
14871
15205
  }
14872
- const root = path38.dirname(absolute);
15206
+ const root = path39.dirname(absolute);
14873
15207
  const cases = [];
14874
15208
  for (const input of [...inputs].sort((a, b) => a.id.localeCompare(b.id))) {
14875
15209
  const expected = await measureImageQuality(input.file);
@@ -14897,7 +15231,7 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
14897
15231
  });
14898
15232
  const data = Buffer.from(JSON.stringify(baseline, null, 2) + "\n");
14899
15233
  if (existsSync29(absolute) && !options.force) {
14900
- const current = await readFile25(absolute);
15234
+ const current = await readFile26(absolute);
14901
15235
  if (!current.equals(data)) {
14902
15236
  throw new OverwriteRefusedError(
14903
15237
  `Quality baseline already exists with different content: ${absolute}. Pass --force to replace it.`,
@@ -14909,10 +15243,10 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
14909
15243
  return { baseline, path: absolute, changed: result.changed.length > 0 };
14910
15244
  }
14911
15245
  async function readQualityBaseline(baselinePath) {
14912
- const absolute = path38.resolve(baselinePath);
15246
+ const absolute = path39.resolve(baselinePath);
14913
15247
  let raw;
14914
15248
  try {
14915
- raw = JSON.parse(await readFile25(absolute, "utf8"));
15249
+ raw = JSON.parse(await readFile26(absolute, "utf8"));
14916
15250
  } catch (error) {
14917
15251
  throw new Error(`Could not read quality baseline ${absolute}: ${message4(error)}`, { cause: error });
14918
15252
  }
@@ -14982,12 +15316,12 @@ function regressionViolations(expected, actual, tolerances) {
14982
15316
  return { violations, warnings };
14983
15317
  }
14984
15318
  async function checkQualityBaseline(baselinePath) {
14985
- const absolute = path38.resolve(baselinePath);
15319
+ const absolute = path39.resolve(baselinePath);
14986
15320
  const baseline = await readQualityBaseline(absolute);
14987
- const root = path38.dirname(absolute);
15321
+ const root = path39.dirname(absolute);
14988
15322
  const cases = [];
14989
15323
  for (const entry of baseline.cases) {
14990
- const file = path38.resolve(root, entry.file);
15324
+ const file = path39.resolve(root, entry.file);
14991
15325
  let actual = null;
14992
15326
  const violations = [];
14993
15327
  const warnings = [];
@@ -15000,7 +15334,7 @@ async function checkQualityBaseline(baselinePath) {
15000
15334
  violations.push(message4(error));
15001
15335
  }
15002
15336
  if (entry.record) {
15003
- const recordPath = path38.resolve(root, entry.record.path);
15337
+ const recordPath = path39.resolve(root, entry.record.path);
15004
15338
  try {
15005
15339
  const recordHash = await sha256File(recordPath);
15006
15340
  if (recordHash !== entry.record.sha256) violations.push("quality record hash changed");
@@ -15042,15 +15376,15 @@ async function runQuality(args) {
15042
15376
  if (!args.out) throw new Error("quality snapshot needs --out <pixelkiln.quality.json>.");
15043
15377
  let raw;
15044
15378
  try {
15045
- raw = JSON.parse(await readFile26(path39.resolve(args.inputs), "utf8"));
15379
+ raw = JSON.parse(await readFile27(path40.resolve(args.inputs), "utf8"));
15046
15380
  } catch (error) {
15047
15381
  throw new Error(
15048
- `Could not read quality inputs ${path39.resolve(args.inputs)}: ${error instanceof Error ? error.message : String(error)}`,
15382
+ `Could not read quality inputs ${path40.resolve(args.inputs)}: ${error instanceof Error ? error.message : String(error)}`,
15049
15383
  { cause: error }
15050
15384
  );
15051
15385
  }
15052
15386
  const inputs = resolveQualityInputs(raw, args.inputs);
15053
- if (path39.resolve(args.inputs) === path39.resolve(args.out)) {
15387
+ if (path40.resolve(args.inputs) === path40.resolve(args.out)) {
15054
15388
  throw new Error("quality snapshot --out must not overwrite its --inputs file.");
15055
15389
  }
15056
15390
  const result = await snapshotQualityBaseline(inputs, args.out, { force: args.force });
@@ -15063,7 +15397,7 @@ async function runQuality(args) {
15063
15397
  }, null, 2));
15064
15398
  } else {
15065
15399
  log(
15066
- ` ${result.changed ? "wrote" : "unchanged"} ${path39.relative(process.cwd(), result.path)} (${result.baseline.cases.length} case(s))`
15400
+ ` ${result.changed ? "wrote" : "unchanged"} ${path40.relative(process.cwd(), result.path)} (${result.baseline.cases.length} case(s))`
15067
15401
  );
15068
15402
  log(` Review the tolerances, commit the baseline, then run quality check in CI.`);
15069
15403
  }
@@ -15089,12 +15423,12 @@ async function runQuality(args) {
15089
15423
  }
15090
15424
 
15091
15425
  // src/cli/commands/recipe.ts
15092
- import path41 from "path";
15426
+ import path42 from "path";
15093
15427
 
15094
15428
  // src/recipes.ts
15095
- import path40 from "path";
15429
+ import path41 from "path";
15096
15430
  import { existsSync as existsSync30 } from "fs";
15097
- import { readdir as readdir3, readFile as readFile27 } from "fs/promises";
15431
+ import { readdir as readdir3, readFile as readFile28 } from "fs/promises";
15098
15432
  import { fileURLToPath } from "url";
15099
15433
  import { z as z10 } from "zod";
15100
15434
  var SHA256_RE2 = /^[0-9a-f]{64}$/;
@@ -15103,7 +15437,7 @@ var VERSION_RE = /^\d+\.\d+\.\d+$/;
15103
15437
  var RECIPE_FILE = "pixelkiln.recipe.json";
15104
15438
  var RECIPE_DIR_TOKEN = "{{recipeDir}}";
15105
15439
  var PortablePathSchema = z10.string().min(1).refine((value) => {
15106
- if (value.includes("\\") || path40.posix.isAbsolute(value)) return false;
15440
+ if (value.includes("\\") || path41.posix.isAbsolute(value)) return false;
15107
15441
  const parts = value.split("/");
15108
15442
  return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
15109
15443
  }, "expected a portable relative path without '.', '..', or backslashes");
@@ -15279,10 +15613,10 @@ function message5(error) {
15279
15613
  return error instanceof Error ? error.message : String(error);
15280
15614
  }
15281
15615
  async function readRecipeFile(recipePath, bundled) {
15282
- const absolute = path40.resolve(recipePath);
15616
+ const absolute = path41.resolve(recipePath);
15283
15617
  let raw;
15284
15618
  try {
15285
- raw = JSON.parse(await readFile27(absolute, "utf8"));
15619
+ raw = JSON.parse(await readFile28(absolute, "utf8"));
15286
15620
  } catch (error) {
15287
15621
  throw new Error(`Could not read recipe ${absolute}: ${message5(error)}`, { cause: error });
15288
15622
  }
@@ -15293,13 +15627,13 @@ async function readRecipeFile(recipePath, bundled) {
15293
15627
  ` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
15294
15628
  );
15295
15629
  }
15296
- return { recipe: parsed.data, authored: raw, path: absolute, dir: path40.dirname(absolute), bundled };
15630
+ return { recipe: parsed.data, authored: raw, path: absolute, dir: path41.dirname(absolute), bundled };
15297
15631
  }
15298
15632
  async function walkRecipeFiles(root) {
15299
15633
  if (!existsSync30(root)) return [];
15300
15634
  const out = [];
15301
15635
  for (const entry of await readdir3(root, { withFileTypes: true })) {
15302
- const resolved = path40.join(root, entry.name);
15636
+ const resolved = path41.join(root, entry.name);
15303
15637
  if (entry.isDirectory()) out.push(...await walkRecipeFiles(resolved));
15304
15638
  else if (entry.isFile() && entry.name === RECIPE_FILE) out.push(resolved);
15305
15639
  }
@@ -15310,7 +15644,7 @@ function bundledRecipeRoot() {
15310
15644
  }
15311
15645
  async function listBundledRecipes(root = bundledRecipeRoot()) {
15312
15646
  const recipes = await Promise.all(
15313
- (await walkRecipeFiles(path40.resolve(root))).map((file) => readRecipeFile(file, true))
15647
+ (await walkRecipeFiles(path41.resolve(root))).map((file) => readRecipeFile(file, true))
15314
15648
  );
15315
15649
  return recipes.sort(
15316
15650
  (a, b) => a.recipe.id.localeCompare(b.recipe.id) || compareVersions(b.recipe.version, a.recipe.version)
@@ -15333,9 +15667,9 @@ function parseSelector(selector) {
15333
15667
  return { id: match[1], version: match[2] };
15334
15668
  }
15335
15669
  async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
15336
- const local = path40.resolve(target);
15670
+ const local = path41.resolve(target);
15337
15671
  if (existsSync30(local)) {
15338
- const recipePath = path40.basename(local) === RECIPE_FILE ? local : path40.join(local, RECIPE_FILE);
15672
+ const recipePath = path41.basename(local) === RECIPE_FILE ? local : path41.join(local, RECIPE_FILE);
15339
15673
  return readRecipeFile(recipePath, false);
15340
15674
  }
15341
15675
  if (target.startsWith(".") || target.startsWith("/") || target.includes("\\")) {
@@ -15349,7 +15683,7 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
15349
15683
  return matches[0];
15350
15684
  }
15351
15685
  async function verifyFile(root, file) {
15352
- const absolute = path40.resolve(root, ...file.path.split("/"));
15686
+ const absolute = path41.resolve(root, ...file.path.split("/"));
15353
15687
  if (!existsSync30(absolute)) {
15354
15688
  return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
15355
15689
  }
@@ -15365,7 +15699,7 @@ async function verifyRecipe(target, options = {}) {
15365
15699
  const loaded = await resolveRecipe(target, options.bundledRoot);
15366
15700
  const actualIntegrity = recipeDigest(loaded.authored);
15367
15701
  const files = await Promise.all(loaded.recipe.files.map((file) => verifyFile(loaded.dir, file)));
15368
- const modelRoot = options.modelRoot ? path40.resolve(options.modelRoot) : null;
15702
+ const modelRoot = options.modelRoot ? path41.resolve(options.modelRoot) : null;
15369
15703
  const models = modelRoot ? await Promise.all(loaded.recipe.models.map(async (model) => ({
15370
15704
  ...await verifyFile(modelRoot, model),
15371
15705
  source: model.source,
@@ -15399,7 +15733,7 @@ async function verifyRecipe(target, options = {}) {
15399
15733
  };
15400
15734
  }
15401
15735
  function renderStyle(style, destination, cwd) {
15402
- const portableDir = path40.relative(cwd, destination).split(path40.sep).join("/") || ".";
15736
+ const portableDir = path41.relative(cwd, destination).split(path41.sep).join("/") || ".";
15403
15737
  const replace = (value) => {
15404
15738
  if (typeof value === "string") return value.replaceAll(RECIPE_DIR_TOKEN, portableDir);
15405
15739
  if (Array.isArray(value)) return value.map(replace);
@@ -15411,25 +15745,25 @@ function renderStyle(style, destination, cwd) {
15411
15745
  return replace(style);
15412
15746
  }
15413
15747
  async function installRecipe(target, options = {}) {
15414
- const cwd = path40.resolve(options.cwd ?? process.cwd());
15748
+ const cwd = path41.resolve(options.cwd ?? process.cwd());
15415
15749
  const loaded = await resolveRecipe(target, options.bundledRoot);
15416
15750
  const verification = await verifyRecipe(loaded.path);
15417
15751
  if (!verification.ok) {
15418
15752
  throw new Error(`Recipe ${loaded.recipe.id}@${loaded.recipe.version} failed integrity verification.`);
15419
15753
  }
15420
- const destination = options.out ? path40.resolve(cwd, options.out) : path40.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
15754
+ const destination = options.out ? path41.resolve(cwd, options.out) : path41.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
15421
15755
  const sources = [
15422
15756
  { path: RECIPE_FILE, source: loaded.path },
15423
- ...loaded.recipe.files.map((file) => ({ path: file.path, source: path40.join(loaded.dir, ...file.path.split("/")) }))
15757
+ ...loaded.recipe.files.map((file) => ({ path: file.path, source: path41.join(loaded.dir, ...file.path.split("/")) }))
15424
15758
  ];
15425
15759
  const files = await Promise.all(sources.map(async (file) => ({
15426
- path: path40.join(destination, ...file.path.split("/")),
15427
- data: await readFile27(file.source)
15760
+ path: path41.join(destination, ...file.path.split("/")),
15761
+ data: await readFile28(file.source)
15428
15762
  })));
15429
15763
  if (!options.force) {
15430
15764
  for (const file of files) {
15431
15765
  if (!existsSync30(file.path)) continue;
15432
- const current = await readFile27(file.path);
15766
+ const current = await readFile28(file.path);
15433
15767
  if (!current.equals(file.data)) {
15434
15768
  throw new OverwriteRefusedError(
15435
15769
  `Recipe destination has local changes: ${file.path}. Choose another --out or pass --force to replace declared recipe files.`,
@@ -15523,14 +15857,14 @@ async function runRecipe(args) {
15523
15857
  }, null, 2));
15524
15858
  } else {
15525
15859
  log(` installed ${result.recipe.id}@${result.recipe.version}`);
15526
- log(` ${path41.relative(process.cwd(), result.destination) || "."}`);
15860
+ log(` ${path42.relative(process.cwd(), result.destination) || "."}`);
15527
15861
  log(`
15528
15862
  Add this entry under your manifest's styles object:`);
15529
15863
  log(JSON.stringify({ [result.styleId]: result.style }, null, 2));
15530
15864
  if (result.recipe.models.length) {
15531
15865
  log(`
15532
15866
  Models are not downloaded automatically. Verify them with:`);
15533
- log(` pixelkiln recipe verify ${path41.relative(process.cwd(), result.destination)} --model-root <ComfyUI/models>`);
15867
+ log(` pixelkiln recipe verify ${path42.relative(process.cwd(), result.destination)} --model-root <ComfyUI/models>`);
15534
15868
  }
15535
15869
  }
15536
15870
  return;
@@ -15538,7 +15872,7 @@ async function runRecipe(args) {
15538
15872
  }
15539
15873
 
15540
15874
  // src/cli/commands/refine.ts
15541
- import path42 from "path";
15875
+ import path43 from "path";
15542
15876
  async function runRefine(args) {
15543
15877
  if (args.from || args.subcommand === "approve") {
15544
15878
  if (!args.from) {
@@ -15568,12 +15902,12 @@ async function runRefine(args) {
15568
15902
  ` recovered ${result3.detection.columns}x${result3.detection.rows} native grid (${result3.detection.confidence}: ${result3.detection.consensus})`
15569
15903
  );
15570
15904
  log(` enforced ${result3.palette.length}-color palette without dithering`);
15571
- log(` wrote ${path42.relative(process.cwd(), result3.output)}`);
15572
- log(` quality record: ${path42.relative(process.cwd(), result3.record)}`);
15905
+ log(` wrote ${path43.relative(process.cwd(), result3.output)}`);
15906
+ log(` quality record: ${path43.relative(process.cwd(), result3.record)}`);
15573
15907
  log(`
15574
15908
  Automated checks passed. Human 1\xD7 review is still required:`);
15575
15909
  log(
15576
- ` pixelkiln refine approve --from ${path42.relative(process.cwd(), result3.record)} --reviewer "Your Name"`
15910
+ ` pixelkiln refine approve --from ${path43.relative(process.cwd(), result3.record)} --reviewer "Your Name"`
15577
15911
  );
15578
15912
  }
15579
15913
  return;
@@ -15601,7 +15935,7 @@ async function runRefine(args) {
15601
15935
  if (args.json) log(JSON.stringify(result3, null, 2));
15602
15936
  else {
15603
15937
  log(
15604
- result3.options.frameSet ? ` approved ${result3.outputs.length} frames in ${path42.relative(process.cwd(), result3.record)} by ${args.reviewer.trim()}` : ` approved ${path42.relative(process.cwd(), result3.output)} by ${args.reviewer.trim()}`
15938
+ result3.options.frameSet ? ` approved ${result3.outputs.length} frames in ${path43.relative(process.cwd(), result3.record)} by ${args.reviewer.trim()}` : ` approved ${path43.relative(process.cwd(), result3.output)} by ${args.reviewer.trim()}`
15605
15939
  );
15606
15940
  }
15607
15941
  return;
@@ -15610,7 +15944,7 @@ async function runRefine(args) {
15610
15944
  if (args.json) {
15611
15945
  log(JSON.stringify(result2, null, 2));
15612
15946
  } else {
15613
- log(` ${result2.safe ? "release-ready" : "not release-ready"}: ${path42.relative(process.cwd(), result2.output || result2.record)}`);
15947
+ log(` ${result2.safe ? "release-ready" : "not release-ready"}: ${path43.relative(process.cwd(), result2.output || result2.record)}`);
15614
15948
  for (const reason of result2.reasons) log(` ${reason}`);
15615
15949
  }
15616
15950
  if (!result2.safe) process.exitCode = 1;
@@ -15656,7 +15990,7 @@ async function runRefine(args) {
15656
15990
  log(` ${item.state.padEnd(16)} ${item.key} ${item.reason}`);
15657
15991
  if (item.state === "needs-approval") {
15658
15992
  log(
15659
- ` pixelkiln refine approve --from ${path42.relative(process.cwd(), item.record)} --reviewer "Your Name"`
15993
+ ` pixelkiln refine approve --from ${path43.relative(process.cwd(), item.record)} --reviewer "Your Name"`
15660
15994
  );
15661
15995
  }
15662
15996
  }
@@ -15708,15 +16042,15 @@ async function runTools(args) {
15708
16042
  }
15709
16043
 
15710
16044
  // src/cli/commands/workspace.ts
15711
- import path43 from "path";
16045
+ import path44 from "path";
15712
16046
  import { existsSync as existsSync31 } from "fs";
15713
16047
  async function runWorkspace(args) {
15714
- const workspacePath = path43.resolve(args.workspace ?? "pixelkiln.workspace.json");
15715
- const dir = path43.dirname(workspacePath);
16048
+ const workspacePath = path44.resolve(args.workspace ?? "pixelkiln.workspace.json");
16049
+ const dir = path44.dirname(workspacePath);
15716
16050
  if (args.subcommand === "add") {
15717
- const manifestPath = path43.resolve(args.target);
16051
+ const manifestPath = path44.resolve(args.target);
15718
16052
  const loadedTarget = await loadManifest(manifestPath);
15719
- const lockPath = args.explicitLock ? path43.resolve(args.explicitLock) : path43.join(path43.dirname(manifestPath), "pixelkiln.lock.json");
16053
+ const lockPath = args.explicitLock ? path44.resolve(args.explicitLock) : path44.join(path44.dirname(manifestPath), "pixelkiln.lock.json");
15720
16054
  const ws = await loadWorkspace(workspacePath);
15721
16055
  const id = args.name ?? loadedTarget.manifest.name;
15722
16056
  if (ws.projects.some((p) => p.id === id)) {
@@ -15736,7 +16070,7 @@ async function runWorkspace(args) {
15736
16070
  ...args.account ? { account: args.account } : {}
15737
16071
  };
15738
16072
  await saveWorkspace(workspacePath, { version: 1, projects: [...ws.projects, project] });
15739
- log(` registered "${id}" in ${path43.relative(process.cwd(), workspacePath)}`);
16073
+ log(` registered "${id}" in ${path44.relative(process.cwd(), workspacePath)}`);
15740
16074
  log(` manifest: ${project.manifest}`);
15741
16075
  log(` lock: ${project.lock}`);
15742
16076
  if (!existsSync31(lockPath)) {
@@ -15748,7 +16082,7 @@ async function runWorkspace(args) {
15748
16082
  }
15749
16083
  if (args.subcommand === "remove") {
15750
16084
  const ws = await loadWorkspace(workspacePath);
15751
- const resolvedTarget = path43.resolve(args.target);
16085
+ const resolvedTarget = path44.resolve(args.target);
15752
16086
  const match = ws.projects.find(
15753
16087
  (p) => p.id === args.target || resolveProject(dir, p).manifestPath === resolvedTarget
15754
16088
  );
@@ -15759,7 +16093,7 @@ async function runWorkspace(args) {
15759
16093
  version: 1,
15760
16094
  projects: ws.projects.filter((p) => p !== match)
15761
16095
  });
15762
- log(` removed "${match.id}" from ${path43.relative(process.cwd(), workspacePath)}`);
16096
+ log(` removed "${match.id}" from ${path44.relative(process.cwd(), workspacePath)}`);
15763
16097
  return;
15764
16098
  }
15765
16099
  if (args.subcommand === "list") {
@@ -15771,9 +16105,9 @@ async function runWorkspace(args) {
15771
16105
  if (args.json) {
15772
16106
  log(JSON.stringify({ version: 1, workspace: workspacePath, projects: ws.projects, diagnostics }, null, 2));
15773
16107
  } else if (!ws.projects.length) {
15774
- log(` no projects registered in ${path43.relative(process.cwd(), workspacePath)}`);
16108
+ log(` no projects registered in ${path44.relative(process.cwd(), workspacePath)}`);
15775
16109
  } else {
15776
- log(` ${ws.projects.length} project(s) in ${path43.relative(process.cwd(), workspacePath)}:`);
16110
+ log(` ${ws.projects.length} project(s) in ${path44.relative(process.cwd(), workspacePath)}:`);
15777
16111
  for (const p of ws.projects) {
15778
16112
  log(` ${p.id.padEnd(24)} ${p.manifest.padEnd(40)} (${p.provider}${p.account ? `, ${p.account}` : ""})`);
15779
16113
  }
@@ -15791,7 +16125,7 @@ async function runWorkspace(args) {
15791
16125
  if (args.json) {
15792
16126
  log(JSON.stringify({ ...report, workspace: workspacePath }, null, 2));
15793
16127
  } else {
15794
- log(` workspace: ${path43.relative(process.cwd(), workspacePath)}`);
16128
+ log(` workspace: ${path44.relative(process.cwd(), workspacePath)}`);
15795
16129
  for (const p of report.projects) {
15796
16130
  if (p.error) {
15797
16131
  log(`
@@ -15846,7 +16180,7 @@ async function runVersion() {
15846
16180
  for (const up of ["../package.json", "../../package.json"]) {
15847
16181
  const url = new URL(up, import.meta.url);
15848
16182
  if (!existsSync32(url)) continue;
15849
- const pkg = JSON.parse(await readFile28(url, "utf8"));
16183
+ const pkg = JSON.parse(await readFile29(url, "utf8"));
15850
16184
  if (pkg.name === "pixelkiln") {
15851
16185
  log(`${pkg.name} ${pkg.version}`);
15852
16186
  return;