pixelkiln 0.41.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
  }
@@ -5536,7 +5713,7 @@ var PixelLabProvider = class _PixelLabProvider {
5536
5713
  seed: spec.seed
5537
5714
  });
5538
5715
  const jobId = randomUUID3();
5539
- writeFileSync(path11.join(_PixelLabProvider.cacheDir(), `${jobId}.png`), png);
5716
+ writeFileSync(path12.join(_PixelLabProvider.cacheDir(), `${jobId}.png`), png);
5540
5717
  return { jobId };
5541
5718
  }
5542
5719
  if (spec.generator === "tiles") {
@@ -5577,7 +5754,7 @@ var PixelLabProvider = class _PixelLabProvider {
5577
5754
  }
5578
5755
  async poll(jobId, generator, context) {
5579
5756
  if (generator === "pixflux") {
5580
- const file = path11.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
5757
+ const file = path12.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
5581
5758
  if (existsSync7(file)) {
5582
5759
  const sourceUrl = `file://${file}`;
5583
5760
  return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }] };
@@ -5861,9 +6038,9 @@ var RetroDiffusionClient = class {
5861
6038
  if (!response.ok) throw new ProviderError("retrodiffusion", `Retro Diffusion download failed (${response.status})`, { status: response.status });
5862
6039
  return Buffer.from(await response.arrayBuffer());
5863
6040
  }
5864
- async call(path44, init = {}) {
6041
+ async call(path45, init = {}) {
5865
6042
  if (!this.token) throw new Error("RD_API_KEY is not set");
5866
- const response = await this.request(`${this.baseUrl}${path44}`, {
6043
+ const response = await this.request(`${this.baseUrl}${path45}`, {
5867
6044
  ...init,
5868
6045
  headers: {
5869
6046
  "Content-Type": "application/json",
@@ -6720,13 +6897,13 @@ registerProvider({
6720
6897
  });
6721
6898
 
6722
6899
  // src/manifest.ts
6723
- import { readFile as readFile5 } from "fs/promises";
6900
+ import { readFile as readFile6 } from "fs/promises";
6724
6901
  import { existsSync as existsSync8 } from "fs";
6725
- import path12 from "path";
6902
+ import path13 from "path";
6726
6903
  async function loadManifest(manifestPath) {
6727
- const abs = path12.resolve(manifestPath);
6904
+ const abs = path13.resolve(manifestPath);
6728
6905
  if (!existsSync8(abs)) throw new ProjectError(`No manifest at ${abs}`);
6729
- const input = ManifestInputSchema.safeParse(JSON.parse(await readFile5(abs, "utf8")));
6906
+ const input = ManifestInputSchema.safeParse(JSON.parse(await readFile6(abs, "utf8")));
6730
6907
  if (!input.success) {
6731
6908
  const issues = formatManifestIssues(input.error.issues);
6732
6909
  throw new ProjectError(`Manifest at ${abs} is invalid:
@@ -6811,7 +6988,7 @@ ${issues}`);
6811
6988
  throw new ProjectError(`Manifest at ${abs} is invalid:
6812
6989
  ${unknownReferences.map((i) => ` ${i}`).join("\n")}`);
6813
6990
  }
6814
- return { manifest: parsed.data, root: path12.dirname(abs), path: abs };
6991
+ return { manifest: parsed.data, root: path13.dirname(abs), path: abs };
6815
6992
  }
6816
6993
  function parentAssetId(asset) {
6817
6994
  return asset?.revision?.from ?? asset?.state?.of ?? asset?.animation?.of;
@@ -6911,11 +7088,11 @@ async function resolveSpecs(loaded, filter) {
6911
7088
  }
6912
7089
  const styleImageCache = /* @__PURE__ */ new Map();
6913
7090
  async function loadStyleImage(rel) {
6914
- const abs = path12.resolve(root, rel);
7091
+ const abs = path13.resolve(root, rel);
6915
7092
  let hit = styleImageCache.get(abs);
6916
7093
  if (!hit) {
6917
7094
  if (!existsSync8(abs)) throw new Error(`Style image not found: ${abs}`);
6918
- const buf = await readFile5(abs);
7095
+ const buf = await readFile6(abs);
6919
7096
  const metadata = imageMetadata(buf);
6920
7097
  if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${abs}`);
6921
7098
  if (metadata.width < 1 || metadata.height < 1) {
@@ -6941,7 +7118,7 @@ async function resolveSpecs(loaded, filter) {
6941
7118
  styleImageDimensions.push(loadedImage);
6942
7119
  }
6943
7120
  const resolvedImages = style.styleImages.map((image) => {
6944
- const hit = styleImageCache.get(path12.resolve(root, image.path));
7121
+ const hit = styleImageCache.get(path13.resolve(root, image.path));
6945
7122
  return { base64: hit.base64, width: hit.width, height: hit.height, format: hit.format };
6946
7123
  });
6947
7124
  const styleSpecs = /* @__PURE__ */ new Map();
@@ -6994,11 +7171,11 @@ async function resolveSpecs(loaded, filter) {
6994
7171
  );
6995
7172
  }
6996
7173
  const characterKind = asset.animation ? "animation" : asset.state ? "state" : "base";
6997
- const subject = asset.promptByStyle[styleId] ?? asset.prompt;
7174
+ const subject = asset.promptByStyle[styleId] ?? asset.prompt ?? "";
6998
7175
  const prompt = generator === "character" && characterKind !== "base" ? subject.trim() : [style.promptPrefix, subject, style.promptSuffix].map((p) => p.trim()).filter(Boolean).join(", ");
6999
- const relFile = asset.file ?? path12.join(asset.category ?? "", `${assetId}.png`);
7000
- const outFile = path12.resolve(root, style.outDir, relFile);
7001
- 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;
7002
7179
  const tileSize = generator === "tiles" ? size : style.tileSize ?? 32;
7003
7180
  const tileVariations = tileFeatureOutputCount(generator === "tiles" ? style.tileFeature : void 0) ?? tileVariationCount(countNumberedDescriptions(prompt));
7004
7181
  const base = {
@@ -7051,7 +7228,7 @@ async function resolveSpecs(loaded, filter) {
7051
7228
  minGridConfidence: style.quality.minGridConfidence,
7052
7229
  ...style.quality.minTransparency == null ? {} : { minTransparency: style.quality.minTransparency },
7053
7230
  ...style.quality.fixerRevision ? { fixerRevision: style.quality.fixerRevision } : {},
7054
- ...style.quality.fixerPython ? { fixerPython: path12.resolve(root, style.quality.fixerPython) } : {},
7231
+ ...style.quality.fixerPython ? { fixerPython: path13.resolve(root, style.quality.fixerPython) } : {},
7055
7232
  ...style.quality.fps == null ? {} : { fps: style.quality.fps }
7056
7233
  }
7057
7234
  } : {},
@@ -7069,11 +7246,42 @@ async function resolveSpecs(loaded, filter) {
7069
7246
  const resolved = styleSpecs.get(assetId);
7070
7247
  if (!resolved) {
7071
7248
  throw new Error(
7072
- `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.`
7073
7250
  );
7074
7251
  }
7075
7252
  if (finalized.has(assetId)) return resolved;
7076
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
+ }
7077
7285
  const characterParent = asset.state?.of ?? asset.animation?.of;
7078
7286
  if (resolved.character && characterParent) {
7079
7287
  const parentSpec = await finalize(characterParent);
@@ -7097,9 +7305,9 @@ async function resolveSpecs(loaded, filter) {
7097
7305
  );
7098
7306
  }
7099
7307
  const sourceSpec = await finalize(asset.revision.from);
7100
- 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);
7101
7309
  const sourceImage = await optionalRevisionImage(sourceFile, "revision source");
7102
- 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;
7103
7311
  const maskImage = maskFile ? await optionalRevisionImage(maskFile, "revision mask", "png") : null;
7104
7312
  if (sourceImage && maskImage && (sourceImage.width !== maskImage.width || sourceImage.height !== maskImage.height)) {
7105
7313
  throw new Error(
@@ -7180,12 +7388,12 @@ async function resolveSpecs(loaded, filter) {
7180
7388
  return specs;
7181
7389
  }
7182
7390
  function pngPath(file) {
7183
- const extension = path12.extname(file);
7391
+ const extension = path13.extname(file);
7184
7392
  return extension ? `${file.slice(0, -extension.length)}.png` : `${file}.png`;
7185
7393
  }
7186
7394
  async function optionalRevisionImage(file, label, requiredFormat) {
7187
7395
  if (!existsSync8(file)) return null;
7188
- const bytes = await readFile5(file);
7396
+ const bytes = await readFile6(file);
7189
7397
  const metadata = imageMetadata(bytes);
7190
7398
  if (!metadata) throw new Error(`${label} is not a readable PNG or JPEG: ${file}`);
7191
7399
  if (requiredFormat && metadata.format !== requiredFormat) {
@@ -7198,8 +7406,8 @@ async function resolveStyleImages(loaded, styleId) {
7198
7406
  if (!style) throw new Error(`Unknown style "${styleId}"`);
7199
7407
  const out = [];
7200
7408
  for (const img of style.styleImages) {
7201
- const file = path12.resolve(loaded.root, img.path);
7202
- const buf = await readFile5(file);
7409
+ const file = path13.resolve(loaded.root, img.path);
7410
+ const buf = await readFile6(file);
7203
7411
  const metadata = imageMetadata(buf);
7204
7412
  if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${file}`);
7205
7413
  if (metadata.width < 1 || metadata.height < 1) {
@@ -7268,8 +7476,8 @@ function resolveCharacterShape(asset, style, kind) {
7268
7476
 
7269
7477
  // src/pipeline/fetch.ts
7270
7478
  import { existsSync as existsSync9, readFileSync as readFileSync3 } from "fs";
7271
- import { mkdir as mkdir3, readFile as readFile6, rename as rename3, rm as rm4, writeFile as writeFile3 } from "fs/promises";
7272
- 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";
7273
7481
 
7274
7482
  // src/source-url.ts
7275
7483
  var SENSITIVE_QUERY_KEYS = /* @__PURE__ */ new Set([
@@ -7315,9 +7523,10 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
7315
7523
  const result = { downloaded: 0, skipped: 0, failed: 0, ...opts.refresh ? { unchanged: 0 } : {} };
7316
7524
  const specByKey = new Map(specs.map((s) => [lockKey(s.styleId, s.assetId), s]));
7317
7525
  normalizeLockOutputPaths(lock, specs);
7318
- 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"));
7319
7527
  const pending = Object.entries(lock.entries).filter(([key, e]) => {
7320
7528
  if (e.provider !== provider.id || !specByKey.has(key)) return false;
7529
+ if (specByKey.get(key)?.mirror) return false;
7321
7530
  if (e.status === "selected" || e.status === "download-failed") return true;
7322
7531
  if (e.status !== "downloaded") return false;
7323
7532
  if (opts.refresh) return e.outputs.length > 0 && Boolean(e.sourceUrls?.length || e.sourceUrl);
@@ -7416,7 +7625,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
7416
7625
  if (!source.url) throw new Error(`no durable source URL to refresh ${source.role ?? "asset"} from`);
7417
7626
  fresh = await provider.download(source.url);
7418
7627
  if (recorded && sha256(fresh) === recordedRaw && !reapply) {
7419
- log2(` current ${path13.relative(process.cwd(), target)}`);
7628
+ log2(` current ${path14.relative(process.cwd(), target)}`);
7420
7629
  outputs.push({ ...recorded, path: portableOutputPath(target, spec.root) });
7421
7630
  continue;
7422
7631
  }
@@ -7430,7 +7639,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
7430
7639
  if (cacheDir) {
7431
7640
  await cacheMedia(
7432
7641
  cacheDir,
7433
- await readFile6(target),
7642
+ await readFile7(target),
7434
7643
  recorded.mediaType ?? MediaType.PNG,
7435
7644
  recorded.sha256
7436
7645
  );
@@ -7439,10 +7648,10 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
7439
7648
  continue;
7440
7649
  }
7441
7650
  if (reapply && !fresh) {
7442
- if (!recorded.raw) raw = await readFile6(target);
7443
- log2(` reapply ${path13.relative(process.cwd(), target)}`);
7651
+ if (!recorded.raw) raw = await readFile7(target);
7652
+ log2(` reapply ${path14.relative(process.cwd(), target)}`);
7444
7653
  } else {
7445
- log2(` changed ${path13.relative(process.cwd(), target)} (upstream)`);
7654
+ log2(` changed ${path14.relative(process.cwd(), target)} (upstream)`);
7446
7655
  }
7447
7656
  } else {
7448
7657
  const superseded = (entry.supersededOutputs ?? []).find(
@@ -7461,7 +7670,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
7461
7670
  }
7462
7671
  }
7463
7672
  log2(
7464
- ` 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)")
7465
7674
  );
7466
7675
  }
7467
7676
  }
@@ -7470,7 +7679,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
7470
7679
  } else if (!raw && recorded && cacheDir) {
7471
7680
  if (!reapply) final = await readCachedMedia(cacheDir, recorded.sha256, expectedMediaType);
7472
7681
  if (!final && recordedRaw) raw = await readCachedMedia(cacheDir, recordedRaw, expectedMediaType);
7473
- if (final || raw) log2(` cached ${path13.relative(process.cwd(), target)}`);
7682
+ if (final || raw) log2(` cached ${path14.relative(process.cwd(), target)}`);
7474
7683
  }
7475
7684
  if (!final && !raw) {
7476
7685
  if (!source.url) {
@@ -7497,13 +7706,13 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
7497
7706
  buf = processed.bytes;
7498
7707
  if (processed.changed) {
7499
7708
  rawHash = sha256(raw);
7500
- 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`);
7501
7710
  }
7502
7711
  }
7503
7712
  if (cacheDir) await cacheMedia(cacheDir, buf, mediaType);
7504
- await mkdir3(path13.dirname(target), { recursive: true });
7713
+ await mkdir4(path14.dirname(target), { recursive: true });
7505
7714
  const tmp = `${target}.pixelkiln.tmp`;
7506
- await writeFile3(tmp, buf);
7715
+ await writeFile4(tmp, buf);
7507
7716
  outputs.push({
7508
7717
  path: portableOutputPath(target, spec.root),
7509
7718
  sha256: sha256(buf),
@@ -7513,9 +7722,9 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
7513
7722
  });
7514
7723
  upsert(lock, key, { outputs: mergeOutputs(entry.outputs, outputs) });
7515
7724
  await saveLock(lockPath, lock);
7516
- await rename3(tmp, target);
7725
+ await rename4(tmp, target);
7517
7726
  wrote = true;
7518
- log2(` wrote ${path13.relative(process.cwd(), target)}`);
7727
+ log2(` wrote ${path14.relative(process.cwd(), target)}`);
7519
7728
  }
7520
7729
  if (opts.refresh && !wrote) {
7521
7730
  if (rotated) {
@@ -7559,10 +7768,10 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
7559
7768
  return result;
7560
7769
  }
7561
7770
  async function readCachedMedia(cacheDir, hash, mediaType) {
7562
- const file = path13.join(cacheDir, cacheFileName(hash, mediaType));
7771
+ const file = path14.join(cacheDir, cacheFileName(hash, mediaType));
7563
7772
  if (!existsSync9(file)) return null;
7564
7773
  try {
7565
- const buf = await readFile6(file);
7774
+ const buf = await readFile7(file);
7566
7775
  if (sha256(buf) !== hash) return null;
7567
7776
  validateMedia(buf, mediaType);
7568
7777
  return buf;
@@ -7573,13 +7782,13 @@ async function readCachedMedia(cacheDir, hash, mediaType) {
7573
7782
  async function cacheMedia(cacheDir, buf, mediaType, knownHash) {
7574
7783
  const hash = knownHash ?? sha256(buf);
7575
7784
  validateMedia(buf, mediaType);
7576
- const file = path13.join(cacheDir, cacheFileName(hash, mediaType));
7785
+ const file = path14.join(cacheDir, cacheFileName(hash, mediaType));
7577
7786
  if (await readCachedMedia(cacheDir, hash, mediaType)) return;
7578
- await mkdir3(cacheDir, { recursive: true });
7787
+ await mkdir4(cacheDir, { recursive: true });
7579
7788
  const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
7580
- await writeFile3(tmp, buf);
7789
+ await writeFile4(tmp, buf);
7581
7790
  try {
7582
- await rename3(tmp, file);
7791
+ await rename4(tmp, file);
7583
7792
  } catch (err) {
7584
7793
  if (!existsSync9(file)) throw err;
7585
7794
  } finally {
@@ -7615,11 +7824,11 @@ async function pushTags(provider, specs, lock, opts = {}) {
7615
7824
 
7616
7825
  // src/pipeline/adopt.ts
7617
7826
  import { existsSync as existsSync11 } from "fs";
7618
- import { readFile as readFile8, writeFile as writeFile5 } from "fs/promises";
7619
- import path14 from "path";
7827
+ import { readFile as readFile9, writeFile as writeFile6 } from "fs/promises";
7828
+ import path15 from "path";
7620
7829
 
7621
7830
  // src/cache.ts
7622
- 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";
7623
7832
  import { existsSync as existsSync10 } from "fs";
7624
7833
  import pathModule from "path";
7625
7834
  import { z as z3 } from "zod";
@@ -7635,10 +7844,10 @@ function isSha256Hash(value) {
7635
7844
  function parseCache(value) {
7636
7845
  return HashCacheSchema.parse(value);
7637
7846
  }
7638
- async function loadCache(path44) {
7639
- if (!existsSync10(path44)) return { version: 1, hashes: {} };
7847
+ async function loadCache(path45) {
7848
+ if (!existsSync10(path45)) return { version: 1, hashes: {} };
7640
7849
  try {
7641
- const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile7(path44, "utf8")));
7850
+ const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile8(path45, "utf8")));
7642
7851
  if (!parsed.success) return { version: 1, hashes: {} };
7643
7852
  return {
7644
7853
  version: 1,
@@ -7650,18 +7859,18 @@ async function loadCache(path44) {
7650
7859
  return { version: 1, hashes: {} };
7651
7860
  }
7652
7861
  }
7653
- async function saveCache(path44, cache) {
7862
+ async function saveCache(path45, cache) {
7654
7863
  const sorted = {};
7655
7864
  for (const key of Object.keys(cache.hashes).sort()) {
7656
7865
  const hash = cache.hashes[key];
7657
7866
  if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
7658
7867
  sorted[key] = hash;
7659
7868
  }
7660
- await mkdir4(pathModule.dirname(pathModule.resolve(path44)), { recursive: true });
7661
- 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`;
7662
7871
  try {
7663
- await writeFile4(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
7664
- await rename4(tmp, path44);
7872
+ await writeFile5(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
7873
+ await rename5(tmp, path45);
7665
7874
  } finally {
7666
7875
  await rm5(tmp, { force: true });
7667
7876
  }
@@ -7699,7 +7908,7 @@ async function adopt(provider, specs, lock, lockPath, opts = {}) {
7699
7908
  unmatchedLocal.push(`${spec.styleId}/${spec.assetId} (no file at ${spec.outFile})`);
7700
7909
  continue;
7701
7910
  }
7702
- const bytes = await readFile8(spec.outFile);
7911
+ const bytes = await readFile9(spec.outFile);
7703
7912
  try {
7704
7913
  decodePng(bytes);
7705
7914
  } catch (err) {
@@ -7819,7 +8028,7 @@ async function writePromptsBack(manifestPath, lock, opts = {}) {
7819
8028
  const log2 = opts.onProgress ?? (() => {
7820
8029
  });
7821
8030
  const selectedAssets = opts.assetIds ? new Set(opts.assetIds) : null;
7822
- const raw = JSON.parse(await readFile8(manifestPath, "utf8"));
8031
+ const raw = JSON.parse(await readFile9(manifestPath, "utf8"));
7823
8032
  const best = /* @__PURE__ */ new Map();
7824
8033
  for (const entry of Object.values(lock.entries)) {
7825
8034
  if (opts.provider && entry.provider !== opts.provider) continue;
@@ -7841,7 +8050,7 @@ async function writePromptsBack(manifestPath, lock, opts = {}) {
7841
8050
  stillEmpty.push(id);
7842
8051
  }
7843
8052
  }
7844
- await writeFile5(manifestPath, JSON.stringify(raw, null, 2) + "\n");
8053
+ await writeFile6(manifestPath, JSON.stringify(raw, null, 2) + "\n");
7845
8054
  log2(` filled ${filled} prompt(s) from adopted objects`);
7846
8055
  return { filled, stillEmpty };
7847
8056
  }
@@ -7853,14 +8062,14 @@ function formatUnmatchedRemote(objects, limit = 15) {
7853
8062
 
7854
8063
  // src/pipeline/adopt-characters.ts
7855
8064
  import { existsSync as existsSync12 } from "fs";
7856
- import { mkdir as mkdir5, readFile as readFile9, writeFile as writeFile6 } from "fs/promises";
7857
- import path15 from "path";
8065
+ import { mkdir as mkdir6, readFile as readFile10, writeFile as writeFile7 } from "fs/promises";
8066
+ import path16 from "path";
7858
8067
  async function adoptCharacters(provider, specs, lock, lockPath, opts) {
7859
8068
  const log2 = opts.onProgress ?? (() => {
7860
8069
  });
7861
8070
  const result = { scanned: 0, matched: 0, unmatched: [], remaining: [], written: [], differing: [] };
7862
8071
  if (!provider.listCharacters || !provider.getCharacter) return result;
7863
- const wanted = specs.filter((spec) => spec.character);
8072
+ const wanted = specs.filter((spec) => spec.character && !spec.mirror);
7864
8073
  if (!wanted.length) return result;
7865
8074
  const remote = [];
7866
8075
  for await (const character of provider.listCharacters()) remote.push(character);
@@ -7906,11 +8115,11 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
7906
8115
  const hash = sha256(bytes);
7907
8116
  const file = expectedOutputPath(spec, source.role, index, sources.length);
7908
8117
  if (existsSync12(file)) {
7909
- 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));
7910
8119
  } else {
7911
- await mkdir5(path15.dirname(file), { recursive: true });
7912
- await writeFile6(file, bytes);
7913
- 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));
7914
8123
  }
7915
8124
  outputs.push({ path: portableOutputPath(file, spec.root), sha256: hash, ...source.role ? { role: source.role } : {} });
7916
8125
  }
@@ -7949,10 +8158,10 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
7949
8158
  } else {
7950
8159
  const south = expectedOutputPath(spec, "south", 0, spec.character.directions);
7951
8160
  if (!existsSync12(south)) {
7952
- 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`);
7953
8162
  return false;
7954
8163
  }
7955
- const localHash = sha256(await readFile9(south));
8164
+ const localHash = sha256(await readFile10(south));
7956
8165
  for (const candidate of remote) {
7957
8166
  if (claimed.has(candidate.id)) continue;
7958
8167
  if (await southHash(candidate) === localHash) {
@@ -7961,7 +8170,7 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
7961
8170
  }
7962
8171
  }
7963
8172
  if (!match) {
7964
- 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)}`);
7965
8174
  return false;
7966
8175
  }
7967
8176
  }
@@ -8031,12 +8240,12 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
8031
8240
  if (!existsSync12(first)) {
8032
8241
  const byName = candidates.find((a) => a.name === characterAnimationName(spec));
8033
8242
  if (!byName) {
8034
- 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`);
8035
8244
  return false;
8036
8245
  }
8037
8246
  match = byName;
8038
8247
  } else {
8039
- const localHash = sha256(await readFile9(first));
8248
+ const localHash = sha256(await readFile10(first));
8040
8249
  for (const candidate of candidates) {
8041
8250
  if (sha256(await download(candidate.frames[0])) === localHash) {
8042
8251
  match = candidate;
@@ -8044,7 +8253,7 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
8044
8253
  }
8045
8254
  }
8046
8255
  if (!match) {
8047
- 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)}`);
8048
8257
  return false;
8049
8258
  }
8050
8259
  }
@@ -8104,9 +8313,9 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
8104
8313
  }
8105
8314
 
8106
8315
  // src/pipeline/salvage.ts
8107
- import { readFile as readFile10 } from "fs/promises";
8316
+ import { readFile as readFile11 } from "fs/promises";
8108
8317
  import { existsSync as existsSync13 } from "fs";
8109
- import path16 from "path";
8318
+ import path17 from "path";
8110
8319
  function providerClaimId(provider, objectId) {
8111
8320
  return `${encodeURIComponent(provider)}:${encodeURIComponent(objectId)}`;
8112
8321
  }
@@ -8116,7 +8325,7 @@ async function loadClaims(lockPaths, opts = {}) {
8116
8325
  if (!existsSync13(p)) throw new Error(`Claim lockfile not found: ${p}`);
8117
8326
  let parsed;
8118
8327
  try {
8119
- parsed = parseLock(JSON.parse(await readFile10(p, "utf8")));
8328
+ parsed = parseLock(JSON.parse(await readFile11(p, "utf8")));
8120
8329
  } catch {
8121
8330
  throw new Error(`Claim lockfile is malformed: ${p}`);
8122
8331
  }
@@ -8164,20 +8373,20 @@ function matchStyleByPattern(prompt, manifest) {
8164
8373
  return null;
8165
8374
  }
8166
8375
  async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, claimPaths) {
8167
- const own = path16.resolve(ownManifestPath);
8376
+ const own = path17.resolve(ownManifestPath);
8168
8377
  const siblingManifestPaths = [
8169
8378
  .../* @__PURE__ */ new Set([
8170
8379
  ...workspaceManifestPaths,
8171
- ...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"))
8172
8381
  ])
8173
8382
  ];
8174
8383
  const siblings = [];
8175
8384
  for (const siblingManifestPath of siblingManifestPaths) {
8176
- if (path16.resolve(siblingManifestPath) === own) continue;
8385
+ if (path17.resolve(siblingManifestPath) === own) continue;
8177
8386
  if (!existsSync13(siblingManifestPath)) continue;
8178
8387
  try {
8179
8388
  const { manifest } = await loadManifest(siblingManifestPath);
8180
- siblings.push({ label: path16.basename(path16.dirname(siblingManifestPath)), manifest });
8389
+ siblings.push({ label: path17.basename(path17.dirname(siblingManifestPath)), manifest });
8181
8390
  } catch {
8182
8391
  }
8183
8392
  }
@@ -8293,9 +8502,9 @@ async function applyTags(provider, decisions, existing, opts = {}) {
8293
8502
  }
8294
8503
 
8295
8504
  // src/workspace.ts
8296
- 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";
8297
8506
  import { existsSync as existsSync14 } from "fs";
8298
- import path17 from "path";
8507
+ import path18 from "path";
8299
8508
  import { z as z4 } from "zod";
8300
8509
  var WorkspaceProjectSchema = z4.object({
8301
8510
  id: z4.string().min(1),
@@ -8323,7 +8532,7 @@ async function loadWorkspace(workspacePath) {
8323
8532
  if (!existsSync14(workspacePath)) return { version: 1, projects: [] };
8324
8533
  let raw;
8325
8534
  try {
8326
- raw = JSON.parse(await readFile11(workspacePath, "utf8"));
8535
+ raw = JSON.parse(await readFile12(workspacePath, "utf8"));
8327
8536
  } catch (err) {
8328
8537
  throw new ProjectError(
8329
8538
  `Workspace catalog at ${workspacePath} is malformed:
@@ -8337,22 +8546,22 @@ async function saveWorkspace(workspacePath, ws) {
8337
8546
  version: 1,
8338
8547
  projects: [...ws.projects].sort((a, b) => a.id.localeCompare(b.id))
8339
8548
  };
8340
- await mkdir6(path17.dirname(path17.resolve(workspacePath)), { recursive: true });
8549
+ await mkdir7(path18.dirname(path18.resolve(workspacePath)), { recursive: true });
8341
8550
  const tmp = `${workspacePath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
8342
8551
  try {
8343
- await writeFile7(tmp, JSON.stringify(sorted, null, 2) + "\n");
8344
- await rename5(tmp, workspacePath);
8552
+ await writeFile8(tmp, JSON.stringify(sorted, null, 2) + "\n");
8553
+ await rename6(tmp, workspacePath);
8345
8554
  } finally {
8346
8555
  await rm6(tmp, { force: true });
8347
8556
  }
8348
8557
  }
8349
8558
  function toPortablePath(dir, absolute) {
8350
- return path17.relative(dir, absolute).split(path17.sep).join("/");
8559
+ return path18.relative(dir, absolute).split(path18.sep).join("/");
8351
8560
  }
8352
8561
  function resolveProject(dir, project) {
8353
8562
  return {
8354
- manifestPath: path17.resolve(dir, project.manifest.split("/").join(path17.sep)),
8355
- 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))
8356
8565
  };
8357
8566
  }
8358
8567
  function validateWorkspace(ws, dir) {
@@ -8365,7 +8574,7 @@ function validateWorkspace(ws, dir) {
8365
8574
  const { manifestPath, lockPath } = resolveProject(dir, project);
8366
8575
  lockOwners.set(lockPath, [...lockOwners.get(lockPath) ?? [], project.id]);
8367
8576
  manifestOwners.set(manifestPath, [...manifestOwners.get(manifestPath) ?? [], project.id]);
8368
- if (path17.isAbsolute(project.manifest) || path17.isAbsolute(project.lock)) {
8577
+ if (path18.isAbsolute(project.manifest) || path18.isAbsolute(project.lock)) {
8369
8578
  diagnostics.push({
8370
8579
  id: "absolute-path",
8371
8580
  level: "warning",
@@ -8426,8 +8635,8 @@ function validateWorkspace(ws, dir) {
8426
8635
  }
8427
8636
 
8428
8637
  // src/pick/salvage-server.ts
8429
- import { mkdir as mkdir7, writeFile as writeFile8, readFile as readFile13 } from "fs/promises";
8430
- import path18 from "path";
8638
+ import { mkdir as mkdir8, writeFile as writeFile9, readFile as readFile14 } from "fs/promises";
8639
+ import path19 from "path";
8431
8640
 
8432
8641
  // src/pick/salvage-sheet.ts
8433
8642
  var escapeHtml = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
@@ -8587,7 +8796,7 @@ refresh();
8587
8796
 
8588
8797
  // src/pick/review-server.ts
8589
8798
  import { createServer } from "http";
8590
- import { readFile as readFile12 } from "fs/promises";
8799
+ import { readFile as readFile13 } from "fs/promises";
8591
8800
 
8592
8801
  // src/open.ts
8593
8802
  import { spawn } from "child_process";
@@ -8614,7 +8823,7 @@ function serveReviewPage(opts) {
8614
8823
  if (req.method === "GET" && req.url && opts.assets?.has(req.url)) {
8615
8824
  try {
8616
8825
  const asset = opts.assets.get(req.url);
8617
- const bytes = await readFile12(asset.path);
8826
+ const bytes = await readFile13(asset.path);
8618
8827
  res.writeHead(200, {
8619
8828
  "Content-Type": asset.contentType,
8620
8829
  "Cache-Control": "no-store",
@@ -8691,7 +8900,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
8691
8900
  });
8692
8901
  const html = renderSalvageSheet(orphans, {
8693
8902
  styleId: ctx.styleId,
8694
- importDir: path18.relative(process.cwd(), ctx.importDir) || "."
8903
+ importDir: path19.relative(process.cwd(), ctx.importDir) || "."
8695
8904
  });
8696
8905
  const byId = new Map(orphans.map((o) => [o.id, o]));
8697
8906
  const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
@@ -8720,10 +8929,10 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
8720
8929
  if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
8721
8930
  decodePng(buf);
8722
8931
  const assetId = idFromPrompt(orphan.prompt, taken);
8723
- const rel = path18.join("_salvaged", `${assetId}.png`);
8724
- const outFile = path18.resolve(ctx.importDir, rel);
8725
- await mkdir7(path18.dirname(outFile), { recursive: true });
8726
- 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);
8727
8936
  ctx.manifest.assets[assetId] = {
8728
8937
  prompt: orphan.prompt,
8729
8938
  promptByStyle: {},
@@ -8755,7 +8964,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
8755
8964
  sourceUrl: durableSource,
8756
8965
  sourceUrls: durableSource ? [{ url: durableSource }] : [],
8757
8966
  outputs: [{
8758
- path: portableOutputPath(outFile, path18.dirname(ctx.manifestPath)),
8967
+ path: portableOutputPath(outFile, path19.dirname(ctx.manifestPath)),
8759
8968
  sha256: sha256(buf)
8760
8969
  }],
8761
8970
  submittedAt: orphan.createdAt,
@@ -8779,9 +8988,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
8779
8988
  }
8780
8989
  }
8781
8990
  await applyTags(provider, decisions, existingTags, { onProgress: log2 });
8782
- const raw = JSON.parse(await readFile13(ctx.manifestPath, "utf8"));
8991
+ const raw = JSON.parse(await readFile14(ctx.manifestPath, "utf8"));
8783
8992
  for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
8784
- await writeFile8(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
8993
+ await writeFile9(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
8785
8994
  await saveLock(ctx.lockPath, ctx.lock);
8786
8995
  return result;
8787
8996
  }
@@ -8789,19 +8998,19 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
8789
8998
  }
8790
8999
 
8791
9000
  // src/cli/project.ts
8792
- import path20 from "path";
9001
+ import path21 from "path";
8793
9002
  import { existsSync as existsSync15 } from "fs";
8794
9003
 
8795
9004
  // src/project.ts
8796
- import path19 from "path";
9005
+ import path20 from "path";
8797
9006
  async function openProject(manifestPath = "pixelkiln.manifest.json", opts = {}) {
8798
- const absolute = path19.resolve(manifestPath);
8799
- const root = path19.dirname(absolute);
9007
+ const absolute = path20.resolve(manifestPath);
9008
+ const root = path20.dirname(absolute);
8800
9009
  if (opts.env !== false) {
8801
9010
  loadEnvFiles(root);
8802
- if (path19.resolve(process.cwd()) !== root) loadEnvFiles(process.cwd());
9011
+ if (path20.resolve(process.cwd()) !== root) loadEnvFiles(process.cwd());
8803
9012
  }
8804
- 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"));
8805
9014
  const loaded = await loadManifest(absolute);
8806
9015
  const specs = await resolveSpecs(loaded, { styles: opts.styles ?? [], assets: opts.assets ?? [] });
8807
9016
  const lock = await loadLock(lockPath);
@@ -8932,7 +9141,7 @@ async function workspaceStatus(ws, dir) {
8932
9141
 
8933
9142
  // src/cli/project.ts
8934
9143
  async function provenanceFile(id, file) {
8935
- const absolute = path20.resolve(file);
9144
+ const absolute = path21.resolve(file);
8936
9145
  return {
8937
9146
  id,
8938
9147
  path: absolute,
@@ -9011,7 +9220,7 @@ async function requireCompleteWorkspaceClaims(workspacePath) {
9011
9220
  if (!existsSync15(workspacePath)) {
9012
9221
  throw new ProjectError(`Workspace catalog not found: ${workspacePath}`);
9013
9222
  }
9014
- const dir = path20.dirname(path20.resolve(workspacePath));
9223
+ const dir = path21.dirname(path21.resolve(workspacePath));
9015
9224
  const ws = await loadWorkspace(workspacePath);
9016
9225
  const diagnostics = validateWorkspace(ws, dir);
9017
9226
  const errors = diagnostics.filter((d) => d.level === "error");
@@ -9034,9 +9243,9 @@ async function openAccountProject(args) {
9034
9243
  return project;
9035
9244
  }
9036
9245
  async function open(args) {
9037
- if (!existsSync15(path20.resolve(args.manifest))) {
9246
+ if (!existsSync15(path21.resolve(args.manifest))) {
9038
9247
  throw new ProjectError(
9039
- `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>\`.`
9040
9249
  );
9041
9250
  }
9042
9251
  const project = await openProject(args.manifest, { lockPath: args.lock, styles: args.styles, assets: args.assets });
@@ -9078,7 +9287,7 @@ function providerCache(mode2) {
9078
9287
 
9079
9288
  // src/cli/commands/account.ts
9080
9289
  async function runBalance(args) {
9081
- loadEnvFiles(path21.dirname(path21.resolve(args.manifest)));
9290
+ loadEnvFiles(path22.dirname(path22.resolve(args.manifest)));
9082
9291
  loadEnvFiles(process.cwd());
9083
9292
  const loaded = await loadManifest(args.manifest);
9084
9293
  const selectedProvider = accountProviderId(loaded.manifest, args.provider, "balance");
@@ -9151,12 +9360,12 @@ async function runAdopt(args) {
9151
9360
  tagged ${n} object(s) upstream`);
9152
9361
  }
9153
9362
  if (args.writePrompts) {
9154
- const { filled, stillEmpty } = await writePromptsBack(path21.resolve(args.manifest), lock, {
9363
+ const { filled, stillEmpty } = await writePromptsBack(path22.resolve(args.manifest), lock, {
9155
9364
  onProgress: log,
9156
9365
  provider: provider.id,
9157
9366
  assetIds: specs.map((spec) => spec.assetId)
9158
9367
  });
9159
- 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)}`);
9160
9369
  const reloaded = await loadManifest(args.manifest);
9161
9370
  const rebased = (await resolveSpecs(reloaded, {
9162
9371
  styles: args.styles,
@@ -9186,12 +9395,12 @@ async function runSalvage2(args) {
9186
9395
  const provider = providerFor(accountProvider);
9187
9396
  const jsonMode = args.dryRun && args.json;
9188
9397
  const diag = jsonMode ? (msg = "") => console.error(msg) : log;
9189
- const ownLock = path21.resolve(args.lock);
9398
+ const ownLock = path22.resolve(args.lock);
9190
9399
  let workspaceProjects = [];
9191
9400
  let workspaceDir = "";
9192
9401
  if (args.workspace) {
9193
- const workspacePath = path21.resolve(args.workspace);
9194
- workspaceDir = path21.dirname(workspacePath);
9402
+ const workspacePath = path22.resolve(args.workspace);
9403
+ workspaceDir = path22.dirname(workspacePath);
9195
9404
  const complete = await requireCompleteWorkspaceClaims(workspacePath);
9196
9405
  workspaceProjects = complete.ws.projects;
9197
9406
  for (const d of complete.diagnostics) diag(` WARN ${d.id}: ${d.message}`);
@@ -9201,11 +9410,11 @@ async function runSalvage2(args) {
9201
9410
  .../* @__PURE__ */ new Set([
9202
9411
  ...workspaceLockPaths,
9203
9412
  ...existsSync16(ownLock) ? [ownLock] : [],
9204
- ...args.claims.map((c) => path21.resolve(c))
9413
+ ...args.claims.map((c) => path22.resolve(c))
9205
9414
  ])
9206
9415
  ];
9207
9416
  diag(` claim set (${lockPaths.length} lockfile(s)):`);
9208
- for (const p of lockPaths) diag(` ${path21.relative(process.cwd(), p)}`);
9417
+ for (const p of lockPaths) diag(` ${path22.relative(process.cwd(), p)}`);
9209
9418
  if (!args.claims.length && !args.workspace) {
9210
9419
  diag(
9211
9420
  `
@@ -9213,7 +9422,7 @@ async function runSalvage2(args) {
9213
9422
  pass every other project's lockfile via --claims a.json,b.json, or
9214
9423
  register every project in a workspace catalog and pass --workspace.`
9215
9424
  );
9216
- } 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))) {
9217
9426
  diag(
9218
9427
  `
9219
9428
  This project's manifest is not registered in the workspace catalog. Its own
@@ -9297,7 +9506,7 @@ async function runSalvage2(args) {
9297
9506
  manifestPath: loaded.path,
9298
9507
  manifest: accountManifest,
9299
9508
  styleId,
9300
- importDir: path21.resolve(loaded.root, style.outDir),
9509
+ importDir: path22.resolve(loaded.root, style.outDir),
9301
9510
  lock,
9302
9511
  lockPath: args.lock
9303
9512
  },
@@ -9398,17 +9607,17 @@ async function runTag(args) {
9398
9607
  }
9399
9608
 
9400
9609
  // src/cli/commands/edit.ts
9401
- import path24 from "path";
9610
+ import path25 from "path";
9402
9611
 
9403
9612
  // src/pipeline/hand-edit.ts
9404
9613
  import { existsSync as existsSync17 } from "fs";
9405
- import { copyFile, mkdir as mkdir8, readFile as readFile15, rename as rename7, rm as rm8, writeFile as writeFile10 } from "fs/promises";
9406
- 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";
9407
9616
  import { z as z6 } from "zod";
9408
9617
 
9409
9618
  // src/manifest-edit.ts
9410
- import { readFile as readFile14, rename as rename6, rm as rm7, writeFile as writeFile9 } from "fs/promises";
9411
- 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";
9412
9621
  import { z as z5 } from "zod";
9413
9622
  var HexSha = z5.string().regex(/^[0-9a-f]{64}$/);
9414
9623
  var AssetPatchSchema = z5.object({
@@ -9623,8 +9832,8 @@ function applyEdit(raw, edit) {
9623
9832
  }
9624
9833
  }
9625
9834
  async function applyManifestEdit(manifestPath, edit) {
9626
- const absolute = path22.resolve(manifestPath);
9627
- const original = await readFile14(absolute, "utf8");
9835
+ const absolute = path23.resolve(manifestPath);
9836
+ const original = await readFile15(absolute, "utf8");
9628
9837
  const before = sha256(original);
9629
9838
  if (before !== edit.expectedSha256) throw new ManifestDriftError(absolute);
9630
9839
  let raw;
@@ -9638,12 +9847,12 @@ async function applyManifestEdit(manifestPath, edit) {
9638
9847
  applyEdit(raw, edit);
9639
9848
  const next = serializeLike(original, raw);
9640
9849
  if (next === original) return { manifestPath: absolute, sha256: before, changed: false };
9641
- const tmp = path22.join(
9642
- path22.dirname(absolute),
9643
- `.${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`
9644
9853
  );
9645
9854
  try {
9646
- await writeFile9(tmp, next);
9855
+ await writeFile10(tmp, next);
9647
9856
  try {
9648
9857
  const loaded = await loadManifest(tmp);
9649
9858
  await resolveSpecs(loaded);
@@ -9651,7 +9860,7 @@ async function applyManifestEdit(manifestPath, edit) {
9651
9860
  const message6 = error instanceof Error ? error.message : String(error);
9652
9861
  throw new ManifestEditError(message6.replace(tmp, absolute));
9653
9862
  }
9654
- await rename6(tmp, absolute);
9863
+ await rename7(tmp, absolute);
9655
9864
  } finally {
9656
9865
  await rm7(tmp, { force: true });
9657
9866
  }
@@ -9662,11 +9871,11 @@ async function applyManifestEdit(manifestPath, edit) {
9662
9871
  var HAND_EDIT_DIR = "edits";
9663
9872
  function handEditPath(loaded, spec) {
9664
9873
  const style = loaded.manifest.styles[spec.styleId];
9665
- const outDir = path23.resolve(loaded.root, style?.outDir ?? ".");
9666
- const relative = path23.relative(outDir, spec.outFile);
9667
- const inside = relative && !relative.startsWith("..") && !path23.isAbsolute(relative);
9668
- const file = inside ? relative : path23.basename(spec.outFile);
9669
- 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"));
9670
9879
  }
9671
9880
  function handEditBases(spec, lock) {
9672
9881
  const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
@@ -9708,7 +9917,7 @@ async function startHandEdit(loaded, lock, spec, opts = {}) {
9708
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`
9709
9918
  );
9710
9919
  }
9711
- const existing = spec.source ? path23.resolve(loaded.root, spec.source) : null;
9920
+ const existing = spec.source ? path24.resolve(loaded.root, spec.source) : null;
9712
9921
  const editPath = existing ?? handEditPath(loaded, spec);
9713
9922
  const members = handEditMembers(loaded, spec, lock, editPath);
9714
9923
  let created = false;
@@ -9719,7 +9928,7 @@ async function startHandEdit(loaded, lock, spec, opts = {}) {
9719
9928
  `${spec.styleId}/${spec.assetId} has no generated PNG on disk to start from; generate or restore it first`
9720
9929
  );
9721
9930
  }
9722
- await mkdir8(path23.dirname(member.path), { recursive: true });
9931
+ await mkdir9(path24.dirname(member.path), { recursive: true });
9723
9932
  await copyFile(member.base, member.path);
9724
9933
  created = true;
9725
9934
  }
@@ -9798,7 +10007,7 @@ function handEditProjectPath(editPath) {
9798
10007
  async function readHandEditCompanion(editPath) {
9799
10008
  let text;
9800
10009
  try {
9801
- text = await readFile15(handEditCompanionPath(editPath), "utf8");
10010
+ text = await readFile16(handEditCompanionPath(editPath), "utf8");
9802
10011
  } catch {
9803
10012
  return null;
9804
10013
  }
@@ -9841,7 +10050,7 @@ async function saveHandEdit(loaded, lock, spec, input) {
9841
10050
  }
9842
10051
  });
9843
10052
  const bases = handEditBases(spec, lock);
9844
- 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 };
9845
10054
  for (const [index, image] of decoded.entries()) {
9846
10055
  if (image.width !== expectedSize.width || image.height !== expectedSize.height) {
9847
10056
  const which = memberSet ? `member ${incoming[index].role ?? index}` : "the edit";
@@ -9857,7 +10066,7 @@ async function saveHandEdit(loaded, lock, spec, input) {
9857
10066
  editor: input.editor,
9858
10067
  protocol: input.protocol,
9859
10068
  savedAt: (input.now ?? /* @__PURE__ */ new Date()).toISOString(),
9860
- project: projectPath ? path23.basename(projectPath) : null,
10069
+ project: projectPath ? path24.basename(projectPath) : null,
9861
10070
  outputs: incoming.map((frame, index) => ({
9862
10071
  role: frame.role,
9863
10072
  basedOn: basedOnHash(entry, index),
@@ -9887,8 +10096,8 @@ function basedOnHash(entry, index) {
9887
10096
  async function replaceFile(file, bytes) {
9888
10097
  const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
9889
10098
  try {
9890
- await writeFile10(tmp, bytes);
9891
- await rename7(tmp, file);
10099
+ await writeFile11(tmp, bytes);
10100
+ await rename8(tmp, file);
9892
10101
  } finally {
9893
10102
  await rm8(tmp, { force: true });
9894
10103
  }
@@ -9917,7 +10126,7 @@ async function runEdit(args) {
9917
10126
  const set = started.members.length > 1;
9918
10127
  const kind = spec.generator === "frames" ? "frames" : "members";
9919
10128
  log(` ${started.created ? "created" : "found"} ${started.source}${set ? ` (${started.members.length} ${kind})` : ""}${started.declared ? " and declared it as the asset's source" : ""}`);
9920
- 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)}`);
9921
10130
  if (args.noOpen) {
9922
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`);
9923
10132
  } else {
@@ -9927,17 +10136,17 @@ async function runEdit(args) {
9927
10136
  }
9928
10137
 
9929
10138
  // src/cli/commands/gallery.ts
9930
- import path31 from "path";
10139
+ import path32 from "path";
9931
10140
  import { existsSync as existsSync23 } from "fs";
9932
10141
 
9933
10142
  // src/gallery/snapshot.ts
9934
10143
  import { createHash as createHash3 } from "crypto";
9935
10144
  import { existsSync as existsSync19 } from "fs";
9936
- import { readFile as readFile16, stat as stat2 } from "fs/promises";
9937
- import path27 from "path";
10145
+ import { readFile as readFile17, stat as stat2 } from "fs/promises";
10146
+ import path28 from "path";
9938
10147
 
9939
10148
  // src/gallery/edit.ts
9940
- import path25 from "path";
10149
+ import path26 from "path";
9941
10150
  import { z as z7 } from "zod";
9942
10151
  var Base64 = z7.string().regex(/^[A-Za-z0-9+/]*={0,2}$/).max(Math.ceil(MAX_HAND_EDIT_BYTES / 3) * 4);
9943
10152
  var HandEditRequestSchema = z7.discriminatedUnion("action", [
@@ -10027,7 +10236,7 @@ function createGalleryEditHandler(opts) {
10027
10236
  }
10028
10237
  const result = await applyManifestEdit(manifestPath, parsed.data);
10029
10238
  if (result.changed) {
10030
- const relative = path25.relative(process.cwd(), result.manifestPath);
10239
+ const relative = path26.relative(process.cwd(), result.manifestPath);
10031
10240
  const shown = !relative || relative.startsWith("..") ? result.manifestPath : relative;
10032
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;
10033
10242
  log2(` manifest edited: ${parsed.data.action === "add-asset" ? "added" : "changed"} ${subject} in ${shown}`);
@@ -10038,7 +10247,7 @@ function createGalleryEditHandler(opts) {
10038
10247
 
10039
10248
  // src/pipeline/history.ts
10040
10249
  import { existsSync as existsSync18 } from "fs";
10041
- import path26 from "path";
10250
+ import path27 from "path";
10042
10251
  var HISTORY_ENV = "PIXELKILN_HISTORY";
10043
10252
  function historyLimit(manifest, env = process.env) {
10044
10253
  if (manifest?.history !== void 0) return manifest.history;
@@ -10127,7 +10336,7 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
10127
10336
  if (!existsSync18(file)) continue;
10128
10337
  if (await sha256File(file) !== entry.outputs[i].sha256 && !opts.force) {
10129
10338
  throw new OverwriteRefusedError(
10130
- `${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`,
10131
10340
  [file]
10132
10341
  );
10133
10342
  }
@@ -10179,7 +10388,7 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
10179
10388
  // src/gallery/snapshot.ts
10180
10389
  var PROJECT_FILE_TYPE = "application/octet-stream";
10181
10390
  function galleryMediaId(absolutePath) {
10182
- 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);
10183
10392
  }
10184
10393
  function galleryMediaRoute(id) {
10185
10394
  return `/media/${id}`;
@@ -10198,7 +10407,7 @@ function describeHistory(media, entry, cacheDir) {
10198
10407
  return (entry.history ?? []).map((generation, i) => {
10199
10408
  const outputs = generation.outputs.map((output) => {
10200
10409
  const mediaType = output.mediaType ?? MediaType.PNG;
10201
- const file = path27.join(cacheDir, cacheFileName(output.sha256, mediaType));
10410
+ const file = path28.join(cacheDir, cacheFileName(output.sha256, mediaType));
10202
10411
  let url = null;
10203
10412
  if (existsSync19(file)) {
10204
10413
  const id = galleryMediaId(file);
@@ -10229,7 +10438,7 @@ function describeHistory(media, entry, cacheDir) {
10229
10438
  var MAX_COMPARED_BYTES = 8 * 1024 * 1024;
10230
10439
  async function samePixels(a, b) {
10231
10440
  try {
10232
- const [left, right] = await Promise.all([readFile16(a), readFile16(b)]);
10441
+ const [left, right] = await Promise.all([readFile17(a), readFile17(b)]);
10233
10442
  if (left.length > MAX_COMPARED_BYTES || right.length > MAX_COMPARED_BYTES) return false;
10234
10443
  const x = decodePng(left);
10235
10444
  const y = decodePng(right);
@@ -10299,9 +10508,9 @@ async function describeOutput(media, root, absolutePath, recorded = {}) {
10299
10508
  async function describeQuality(media, root, inspection) {
10300
10509
  const outputPaths = inspection.outputs ?? [inspection.output];
10301
10510
  const outputs = await Promise.all(
10302
- outputPaths.map((file) => describeOutput(media, root, path27.resolve(root, file)))
10511
+ outputPaths.map((file) => describeOutput(media, root, path28.resolve(root, file)))
10303
10512
  );
10304
- const recordPath = path27.resolve(root, inspection.record);
10513
+ const recordPath = path28.resolve(root, inspection.record);
10305
10514
  const recordExists = existsSync19(recordPath);
10306
10515
  let options = null;
10307
10516
  let check = null;
@@ -10321,7 +10530,7 @@ async function describeQuality(media, root, inspection) {
10321
10530
  return {
10322
10531
  state: inspection.state,
10323
10532
  reason: inspection.reason,
10324
- output: portableOutputPath(path27.resolve(root, inspection.output), root),
10533
+ output: portableOutputPath(path28.resolve(root, inspection.output), root),
10325
10534
  record: portableOutputPath(recordPath, root),
10326
10535
  recordExists,
10327
10536
  outputs,
@@ -10358,9 +10567,9 @@ async function buildGallerySnapshot(opts) {
10358
10567
  const media = /* @__PURE__ */ new Map();
10359
10568
  const plan = await buildPlan(specs, lock);
10360
10569
  const items = [];
10361
- const manifestText = await readFile16(loaded.path, "utf8");
10570
+ const manifestText = await readFile17(loaded.path, "utf8");
10362
10571
  const manifestSha256 = sha256(manifestText);
10363
- const cacheDir = path27.resolve(path27.dirname(opts.lockPath), ".pixelkiln", "cache");
10572
+ const cacheDir = path28.resolve(path28.dirname(opts.lockPath), ".pixelkiln", "cache");
10364
10573
  let rawStyles = {};
10365
10574
  try {
10366
10575
  const raw = JSON.parse(manifestText);
@@ -10384,7 +10593,7 @@ async function buildGallerySnapshot(opts) {
10384
10593
  )
10385
10594
  );
10386
10595
  if (spec.source) {
10387
- const editPath = path27.resolve(root, spec.source);
10596
+ const editPath = path28.resolve(root, spec.source);
10388
10597
  const memberCount = sourceIsStem(spec.source, entry, root) ? entry.outputs.length : 1;
10389
10598
  const editShas = await Promise.all(
10390
10599
  Array.from({ length: memberCount }, async (_, index) => {
@@ -10433,7 +10642,7 @@ async function buildGallerySnapshot(opts) {
10433
10642
  editStatus = edits.some((member) => !member.exists) ? "missing" : editChanged.every((changed) => !changed) ? "same" : regenerated ? "regenerated-since" : "edited";
10434
10643
  }
10435
10644
  } else if (spec.source) {
10436
- outputs = [await describeOutput(media, root, path27.resolve(root, spec.source))];
10645
+ outputs = [await describeOutput(media, root, path28.resolve(root, spec.source))];
10437
10646
  } else if (!entry && existsSync19(spec.outFile)) {
10438
10647
  outputs = [await describeOutput(media, root, spec.outFile)];
10439
10648
  } else {
@@ -10473,6 +10682,7 @@ async function buildGallerySnapshot(opts) {
10473
10682
  currentSpecHash: spec.specHash,
10474
10683
  revision: entry?.revision ?? null,
10475
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,
10476
10686
  character: describeCharacter(spec, entry),
10477
10687
  outputs,
10478
10688
  quality,
@@ -10535,6 +10745,7 @@ async function buildGallerySnapshot(opts) {
10535
10745
  currentSpecHash: null,
10536
10746
  revision: entry.revision,
10537
10747
  revisionParentKey: entry.revision ? lockKey(entry.styleId, entry.revision.sourceAssetId) : null,
10748
+ mirrorOfKey: entry.mirror ? lockKey(entry.styleId, entry.mirror.sourceAssetId) : null,
10538
10749
  character: describeCharacter(void 0, entry),
10539
10750
  outputs,
10540
10751
  quality: null,
@@ -10623,7 +10834,7 @@ async function buildGallerySnapshot(opts) {
10623
10834
  id: loaded.manifest.name,
10624
10835
  name: loaded.manifest.name,
10625
10836
  manifest: loaded.path,
10626
- lock: path27.resolve(opts.lockPath),
10837
+ lock: path28.resolve(opts.lockPath),
10627
10838
  root,
10628
10839
  provider: loaded.manifest.provider,
10629
10840
  account: null,
@@ -10649,7 +10860,7 @@ async function buildGallerySnapshot(opts) {
10649
10860
  return { snapshot, media };
10650
10861
  }
10651
10862
  async function buildWorkspaceGallerySnapshot(opts) {
10652
- const dir = path27.dirname(path27.resolve(opts.workspacePath));
10863
+ const dir = path28.dirname(path28.resolve(opts.workspacePath));
10653
10864
  const media = /* @__PURE__ */ new Map();
10654
10865
  const projects = [];
10655
10866
  const items = [];
@@ -10692,11 +10903,11 @@ async function buildWorkspaceGallerySnapshot(opts) {
10692
10903
  id: project.id,
10693
10904
  name: loaded.manifest.name,
10694
10905
  manifest: manifestPath,
10695
- lock: path27.resolve(lockPath),
10906
+ lock: path28.resolve(lockPath),
10696
10907
  root: loaded.root,
10697
10908
  provider: loaded.manifest.provider,
10698
10909
  account: project.account ?? null,
10699
- manifestSha256: manifestSha256 ?? sha256(await readFile16(manifestPath, "utf8")),
10910
+ manifestSha256: manifestSha256 ?? sha256(await readFile17(manifestPath, "utf8")),
10700
10911
  historyLimit: historyLimit(loaded.manifest),
10701
10912
  entries: Object.keys(lock.entries).length,
10702
10913
  items: projectItems.length,
@@ -10708,8 +10919,8 @@ async function buildWorkspaceGallerySnapshot(opts) {
10708
10919
  id: project.id,
10709
10920
  name: project.id,
10710
10921
  manifest: manifestPath,
10711
- lock: path27.resolve(lockPath),
10712
- root: path27.dirname(manifestPath),
10922
+ lock: path28.resolve(lockPath),
10923
+ root: path28.dirname(manifestPath),
10713
10924
  provider: project.provider,
10714
10925
  account: project.account ?? null,
10715
10926
  manifestSha256: null,
@@ -10736,7 +10947,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
10736
10947
  version: 1,
10737
10948
  generatedAt: (opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
10738
10949
  project: null,
10739
- workspace: { path: path27.resolve(opts.workspacePath), dir, projects },
10950
+ workspace: { path: path28.resolve(opts.workspacePath), dir, projects },
10740
10951
  filter,
10741
10952
  totals: {
10742
10953
  entries: projects.reduce((sum, project) => sum + project.entries, 0),
@@ -10755,7 +10966,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
10755
10966
  // src/gallery/server.ts
10756
10967
  import { randomBytes } from "crypto";
10757
10968
  import { createServer as createServer2 } from "http";
10758
- import { readFile as readFile17 } from "fs/promises";
10969
+ import { readFile as readFile18 } from "fs/promises";
10759
10970
 
10760
10971
  // src/gallery/page.ts
10761
10972
  import { readFileSync as readFileSync4 } from "fs";
@@ -10996,7 +11207,7 @@ async function serveGallery(opts) {
10996
11207
  const asset = reviewAssets.get(jobId)?.get(url2.pathname);
10997
11208
  if (!asset) return fail(404, "no such review asset");
10998
11209
  try {
10999
- const bytes = await readFile17(asset.path);
11210
+ const bytes = await readFile18(asset.path);
11000
11211
  res.writeHead(200, {
11001
11212
  "Content-Type": asset.contentType,
11002
11213
  "Cache-Control": "no-store",
@@ -11072,7 +11283,7 @@ async function serveGallery(opts) {
11072
11283
  if (!asset) return fail(404, "no such gallery media");
11073
11284
  let bytes;
11074
11285
  try {
11075
- bytes = await readFile17(asset.path);
11286
+ bytes = await readFile18(asset.path);
11076
11287
  } catch {
11077
11288
  return fail(404, "gallery media is no longer on disk; refresh the page");
11078
11289
  }
@@ -11216,7 +11427,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
11216
11427
  });
11217
11428
  const estimates = new Map(items.map((item) => [
11218
11429
  item.key,
11219
- 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))
11220
11431
  ]));
11221
11432
  const units = new Set([...estimates.values()].map((estimate) => estimate.unit));
11222
11433
  if (units.size > 1) {
@@ -11266,6 +11477,34 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
11266
11477
  }
11267
11478
  for (const { spec, key } of items) {
11268
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
+ }
11269
11508
  await waitForSlot();
11270
11509
  const since = Date.now() - lastSubmitAt;
11271
11510
  if (since < spacing) await sleep2(spacing - since);
@@ -11391,12 +11630,12 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
11391
11630
 
11392
11631
  // src/pick/server.ts
11393
11632
  import { existsSync as existsSync21 } from "fs";
11394
- import { readFile as readFile19 } from "fs/promises";
11633
+ import { readFile as readFile20 } from "fs/promises";
11395
11634
 
11396
11635
  // src/pipeline/init.ts
11397
- import { readdir, readFile as readFile18, writeFile as writeFile11 } from "fs/promises";
11636
+ import { readdir, readFile as readFile19, writeFile as writeFile12 } from "fs/promises";
11398
11637
  import { existsSync as existsSync20 } from "fs";
11399
- import path28 from "path";
11638
+ import path29 from "path";
11400
11639
  function pngSize(buf) {
11401
11640
  if (buf.length < 24) return null;
11402
11641
  const isPng = buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834;
@@ -11410,7 +11649,7 @@ function slugify(value) {
11410
11649
  async function walk(dir, exclude) {
11411
11650
  const out = [];
11412
11651
  for (const entry of await readdir(dir, { withFileTypes: true })) {
11413
- const full = path28.join(dir, entry.name);
11652
+ const full = path29.join(dir, entry.name);
11414
11653
  if (exclude.some((x) => full.includes(x))) continue;
11415
11654
  if (entry.isDirectory()) out.push(...await walk(full, exclude));
11416
11655
  else if (entry.name.toLowerCase().endsWith(".png")) out.push(full);
@@ -11424,14 +11663,14 @@ async function scanAssets(root, opts = {}) {
11424
11663
  const skipped = [];
11425
11664
  const seen = /* @__PURE__ */ new Map();
11426
11665
  for (const file of files.sort()) {
11427
- const size = pngSize(await readFile18(file));
11666
+ const size = pngSize(await readFile19(file));
11428
11667
  if (!size) {
11429
11668
  skipped.push(`${file} (not a readable PNG)`);
11430
11669
  continue;
11431
11670
  }
11432
- const rel = path28.relative(root, file);
11433
- const category = path28.dirname(rel) === "." ? "" : path28.dirname(rel);
11434
- 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));
11435
11674
  if (seen.has(id)) {
11436
11675
  const n = seen.get(id) + 1;
11437
11676
  seen.set(id, n);
@@ -11492,7 +11731,7 @@ async function writeManifestFile(target, manifest) {
11492
11731
  if (existsSync20(target)) {
11493
11732
  throw new Error(`${target} already exists; refusing to overwrite an existing manifest.`);
11494
11733
  }
11495
- await writeFile11(target, JSON.stringify(manifest, null, 2) + "\n");
11734
+ await writeFile12(target, JSON.stringify(manifest, null, 2) + "\n");
11496
11735
  }
11497
11736
 
11498
11737
  // src/pick/sheet.ts
@@ -11898,7 +12137,7 @@ async function currentArt(entry, spec) {
11898
12137
  let width = entry.width;
11899
12138
  let height = entry.height;
11900
12139
  if (contentType === "image/png") {
11901
- const size = pngSize(await readFile19(file));
12140
+ const size = pngSize(await readFile20(file));
11902
12141
  if (size) ({ width, height } = size);
11903
12142
  }
11904
12143
  return { path: file, contentType, width, height };
@@ -12391,15 +12630,15 @@ function createGenerateHandlers(opts) {
12391
12630
  }
12392
12631
 
12393
12632
  // src/gallery/editor.ts
12394
- import { readFile as readFile21 } from "fs/promises";
12395
- import path30 from "path";
12633
+ import { readFile as readFile22 } from "fs/promises";
12634
+ import path31 from "path";
12396
12635
 
12397
12636
  // src/editor/install.ts
12398
12637
  import { createHash as createHash4 } from "crypto";
12399
12638
  import { existsSync as existsSync22 } from "fs";
12400
- 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";
12401
12640
  import os2 from "os";
12402
- import path29 from "path";
12641
+ import path30 from "path";
12403
12642
 
12404
12643
  // tools/pixelorama-bridge/pin.json
12405
12644
  var pin_default = {
@@ -12463,15 +12702,15 @@ var EditorInstallError = class extends Error {
12463
12702
  };
12464
12703
  function editorToolsRoot() {
12465
12704
  const override = process.env.PIXELKILN_TOOLS_DIR?.trim();
12466
- if (override) return path29.resolve(override);
12467
- 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");
12468
12707
  if (process.platform === "win32") {
12469
- 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");
12470
12709
  }
12471
- 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");
12472
12711
  }
12473
12712
  function editorDir(pin = EDITOR_PIN, root = editorToolsRoot()) {
12474
- return path29.join(root, "pixelorama", pin.release ?? "unpublished");
12713
+ return path30.join(root, "pixelorama", pin.release ?? "unpublished");
12475
12714
  }
12476
12715
  function editorBaseUrl(pin = EDITOR_PIN) {
12477
12716
  const override = process.env.PIXELKILN_EDITOR_URL?.trim();
@@ -12481,7 +12720,7 @@ function editorBaseUrl(pin = EDITOR_PIN) {
12481
12720
  }
12482
12721
  async function sha256Of(file) {
12483
12722
  try {
12484
- return createHash4("sha256").update(await readFile20(file)).digest("hex");
12723
+ return createHash4("sha256").update(await readFile21(file)).digest("hex");
12485
12724
  } catch {
12486
12725
  return null;
12487
12726
  }
@@ -12494,7 +12733,7 @@ async function editorStatus(opts = {}) {
12494
12733
  let totalBytes = 0;
12495
12734
  for (const [name, expected] of Object.entries(pin.files)) {
12496
12735
  totalBytes += expected.bytes;
12497
- const file = path29.join(dir, name);
12736
+ const file = path30.join(dir, name);
12498
12737
  const info = existsSync22(file) ? await stat3(file).catch(() => null) : null;
12499
12738
  if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
12500
12739
  installedBytes += expected.bytes;
@@ -12524,7 +12763,7 @@ async function installEditor(opts = {}) {
12524
12763
  );
12525
12764
  }
12526
12765
  if (!baseUrl) throw new EditorInstallError("no download location for the editor build");
12527
- await mkdir9(dir, { recursive: true });
12766
+ await mkdir10(dir, { recursive: true });
12528
12767
  const downloaded = [];
12529
12768
  const skipped = Object.keys(pin.files).filter((name) => !before.missing.includes(name));
12530
12769
  let fetchedBytes = before.installedBytes;
@@ -12552,11 +12791,11 @@ async function installEditor(opts = {}) {
12552
12791
  if (digest2 !== expected.sha256) {
12553
12792
  throw new EditorInstallError(`${name}: sha256 ${digest2.slice(0, 12)}\u2026 does not match the pinned ${expected.sha256.slice(0, 12)}\u2026`, name);
12554
12793
  }
12555
- const final = path29.join(dir, name);
12794
+ const final = path30.join(dir, name);
12556
12795
  const tmp = `${final}.${process.pid}.part`;
12557
12796
  try {
12558
- await writeFile12(tmp, bytes);
12559
- await rename8(tmp, final);
12797
+ await writeFile13(tmp, bytes);
12798
+ await rename9(tmp, final);
12560
12799
  } finally {
12561
12800
  await rm9(tmp, { force: true });
12562
12801
  }
@@ -12598,7 +12837,7 @@ var EDITOR_CONTENT_TYPES = {
12598
12837
  ".json": "application/json; charset=utf-8"
12599
12838
  };
12600
12839
  function editorContentType(name) {
12601
- return EDITOR_CONTENT_TYPES[path29.extname(name).toLowerCase()] ?? null;
12840
+ return EDITOR_CONTENT_TYPES[path30.extname(name).toLowerCase()] ?? null;
12602
12841
  }
12603
12842
 
12604
12843
  // src/gallery/editor.ts
@@ -12674,7 +12913,7 @@ function createGalleryEditorHandlers(opts = {}) {
12674
12913
  if (!status.installed) return null;
12675
12914
  let bytes;
12676
12915
  try {
12677
- bytes = await readFile21(path30.join(dir, name));
12916
+ bytes = await readFile22(path31.join(dir, name));
12678
12917
  } catch {
12679
12918
  verified = null;
12680
12919
  return null;
@@ -12714,13 +12953,13 @@ async function serveUntilStopped(initial, reload, args, access2) {
12714
12953
  };
12715
12954
  const loadProject = async (project) => {
12716
12955
  const ctx = await access2.loadProject(project);
12717
- const dir = path31.dirname(ctx.loaded.path);
12956
+ const dir = path32.dirname(ctx.loaded.path);
12718
12957
  const declared = readEnvFiles(dir);
12719
12958
  const credentialNames = new Set(availableProviders().flatMap((id) => providerCredentialEnvs(providerFactory(id))));
12720
12959
  for (const [name, value] of Object.entries(declared)) {
12721
12960
  if (credentialNames.has(name) && name in process.env && process.env[name] !== value) {
12722
12961
  throw new Error(
12723
- `${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.`
12724
12963
  );
12725
12964
  }
12726
12965
  }
@@ -12765,7 +13004,7 @@ async function serveUntilStopped(initial, reload, args, access2) {
12765
13004
  }
12766
13005
  async function runGallery(args) {
12767
13006
  if (args.workspace) {
12768
- const workspacePath = path31.resolve(args.workspace);
13007
+ const workspacePath = path32.resolve(args.workspace);
12769
13008
  if (!existsSync23(workspacePath)) {
12770
13009
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
12771
13010
  }
@@ -12786,7 +13025,7 @@ async function runGallery(args) {
12786
13025
  const registered = async (projectId) => {
12787
13026
  const project = (await loadWorkspace(workspacePath)).projects.find((candidate) => candidate.id === projectId);
12788
13027
  if (!projectId || !project) throw new Error(`unknown workspace project "${projectId ?? ""}"`);
12789
- return resolveProject(path31.dirname(workspacePath), project);
13028
+ return resolveProject(path32.dirname(workspacePath), project);
12790
13029
  };
12791
13030
  await serveUntilStopped(initial, build2, args, {
12792
13031
  manifestFor: async (projectId) => (await registered(projectId)).manifestPath,
@@ -12811,7 +13050,7 @@ async function runGallery(args) {
12811
13050
  return buildGallerySnapshot({ ...fresh, filter });
12812
13051
  };
12813
13052
  await serveUntilStopped(await build(), reload, args, {
12814
- manifestFor: () => path31.resolve(args.manifest),
13053
+ manifestFor: () => path32.resolve(args.manifest),
12815
13054
  loadProject: () => openProject(args.manifest, { lockPath: args.lock, env: false })
12816
13055
  });
12817
13056
  }
@@ -13054,7 +13293,7 @@ async function runLifecycle(args, wave, spent) {
13054
13293
  log(` Packaging stays blocked until each refined PNG has a current human approval.`);
13055
13294
  }
13056
13295
  }
13057
- 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);
13058
13297
  }
13059
13298
  return false;
13060
13299
  }
@@ -13120,14 +13359,14 @@ import { existsSync as existsSync25 } from "fs";
13120
13359
 
13121
13360
  // src/pipeline/cache-health.ts
13122
13361
  import { existsSync as existsSync24 } from "fs";
13123
- import { readFile as readFile22, readdir as readdir2, rm as rm10 } from "fs/promises";
13124
- import path32 from "path";
13362
+ import { readFile as readFile23, readdir as readdir2, rm as rm10 } from "fs/promises";
13363
+ import path33 from "path";
13125
13364
  async function inspectCaches(lock, lockPath, options = {}) {
13126
13365
  if (options.prune && !existsSync24(lockPath)) {
13127
- 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)}`);
13128
13367
  }
13129
- const contentDir = path32.resolve(path32.dirname(lockPath), ".pixelkiln", "cache");
13130
- const remotePath = path32.resolve(cachePathFor(lockPath));
13368
+ const contentDir = path33.resolve(path33.dirname(lockPath), ".pixelkiln", "cache");
13369
+ const remotePath = path33.resolve(cachePathFor(lockPath));
13131
13370
  const referenced = referencedHashes(lock);
13132
13371
  let content = await inspectContentCache(contentDir, referenced);
13133
13372
  let remoteHashes = await inspectRemoteHashCache(remotePath);
@@ -13139,7 +13378,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
13139
13378
  ]);
13140
13379
  for (const name of names) {
13141
13380
  try {
13142
- await rm10(path32.join(contentDir, name), { force: true });
13381
+ await rm10(path33.join(contentDir, name), { force: true });
13143
13382
  removed.contentFiles++;
13144
13383
  } catch {
13145
13384
  }
@@ -13148,7 +13387,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
13148
13387
  await saveCache(remotePath, { version: 1, hashes: {} });
13149
13388
  removed.resetRemoteHashCache = true;
13150
13389
  } else if (remoteHashes.invalidIds.length) {
13151
- const cache = parseCache(JSON.parse(await readFile22(remotePath, "utf8")));
13390
+ const cache = parseCache(JSON.parse(await readFile23(remotePath, "utf8")));
13152
13391
  for (const id of remoteHashes.invalidIds) delete cache.hashes[id];
13153
13392
  removed.remoteHashEntries = remoteHashes.invalidIds.length;
13154
13393
  await saveCache(remotePath, cache);
@@ -13187,12 +13426,12 @@ async function inspectContentCache(contentDir, referenced) {
13187
13426
  continue;
13188
13427
  }
13189
13428
  report.files++;
13190
- const file = path32.join(contentDir, entry.name);
13429
+ const file = path33.join(contentDir, entry.name);
13191
13430
  const mediaType = mediaTypeFromExtension(entry.name);
13192
13431
  const expected = mediaType ? entry.name.slice(0, -4) : "";
13193
13432
  let bytes;
13194
13433
  try {
13195
- bytes = await readFile22(file);
13434
+ bytes = await readFile23(file);
13196
13435
  report.bytes += bytes.length;
13197
13436
  } catch (err) {
13198
13437
  report.invalid.push({
@@ -13239,7 +13478,7 @@ async function inspectRemoteHashCache(remotePath) {
13239
13478
  if (!report.exists) return report;
13240
13479
  let cache;
13241
13480
  try {
13242
- cache = parseCache(JSON.parse(await readFile22(remotePath, "utf8")));
13481
+ cache = parseCache(JSON.parse(await readFile23(remotePath, "utf8")));
13243
13482
  } catch (err) {
13244
13483
  report.error = err instanceof Error ? err.message : String(err);
13245
13484
  return report;
@@ -13358,42 +13597,42 @@ This drops their provenance from the lockfile. The art on disk is`);
13358
13597
  }
13359
13598
 
13360
13599
  // src/cli/commands/init.ts
13361
- import path33 from "path";
13600
+ import path34 from "path";
13362
13601
  import { existsSync as existsSync26 } from "fs";
13363
13602
  async function runInit(args) {
13364
13603
  if (!args.from) throw new Error("init needs --from <dir> pointing at your existing PNGs.");
13365
- const root = path33.resolve(args.from);
13604
+ const root = path34.resolve(args.from);
13366
13605
  if (!existsSync26(root)) throw new Error(`No directory at ${root}`);
13367
13606
  const generator = args.generator ?? "map";
13368
13607
  if (generator !== "1dir" && generator !== "map") {
13369
13608
  throw new Error(`--generator must be "1dir" or "map", got "${args.generator}".`);
13370
13609
  }
13371
- const target = path33.resolve(args.out ?? "pixelkiln.manifest.json");
13610
+ const target = path34.resolve(args.out ?? "pixelkiln.manifest.json");
13372
13611
  const { assets, skipped } = await scanAssets(root, { exclude: args.exclude });
13373
13612
  if (!assets.length) throw new Error(`No PNGs found under ${root}`);
13374
13613
  const manifest = buildManifest(
13375
- args.name ?? path33.basename(path33.dirname(target)),
13614
+ args.name ?? path34.basename(path34.dirname(target)),
13376
13615
  args.styles[0] ?? "base",
13377
13616
  generator,
13378
- path33.relative(path33.dirname(target), root) || ".",
13617
+ path34.relative(path34.dirname(target), root) || ".",
13379
13618
  assets
13380
13619
  );
13381
13620
  await writeManifestFile(target, manifest);
13382
- 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)}`);
13383
13622
  if (skipped.length) log(` skipped ${skipped.length} unreadable file(s)`);
13384
- log(` wrote ${path33.relative(process.cwd(), target)}`);
13623
+ log(` wrote ${path34.relative(process.cwd(), target)}`);
13385
13624
  log(`
13386
13625
  Prompts are intentionally empty. To recover the real ones from your`);
13387
13626
  log(` PixelLab account instead of inventing them:`);
13388
13627
  log(`
13389
- pixelkiln adopt --manifest ${path33.relative(process.cwd(), target)} --write-prompts
13628
+ pixelkiln adopt --manifest ${path34.relative(process.cwd(), target)} --write-prompts
13390
13629
  `);
13391
13630
  }
13392
13631
 
13393
13632
  // src/pipeline/doctor.ts
13394
13633
  import { constants, existsSync as existsSync27 } from "fs";
13395
13634
  import { access, stat as stat4 } from "fs/promises";
13396
- import path34 from "path";
13635
+ import path35 from "path";
13397
13636
  async function doctor(loaded, specs, lock, lockPath, opts = {}) {
13398
13637
  const checks = [];
13399
13638
  const add = (id, level, message6) => checks.push({ id, level, message: message6 });
@@ -13404,8 +13643,8 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
13404
13643
  );
13405
13644
  add("lock", "ok", `${Object.keys(lock.entries).length} valid v2 lock entr(ies)`);
13406
13645
  const dirs = [...new Set(specs.flatMap((spec) => [
13407
- path34.dirname(spec.outFile),
13408
- ...spec.quality ? [path34.dirname(spec.quality.outFile)] : []
13646
+ path35.dirname(spec.outFile),
13647
+ ...spec.quality ? [path35.dirname(spec.quality.outFile)] : []
13409
13648
  ]))];
13410
13649
  const unwritable = [];
13411
13650
  for (const dir of dirs) {
@@ -13428,7 +13667,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
13428
13667
  for (let index = 0; index < entry.outputs.length; index++) {
13429
13668
  const output = entry.outputs[index];
13430
13669
  const spec = specByKey.get(key);
13431
- 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));
13432
13671
  const owner = outputOwners.get(absolute);
13433
13672
  if (owner && owner !== key) duplicateOutputs.push(`${output.path} (${owner}, ${key})`);
13434
13673
  outputOwners.set(absolute, key);
@@ -13443,7 +13682,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
13443
13682
  } else {
13444
13683
  add("lock-outputs", "ok", `${outputOwners.size} recorded output path(s) are uniquely owned`);
13445
13684
  }
13446
- const cacheDir = path34.join(path34.dirname(lockPath), ".pixelkiln", "cache");
13685
+ const cacheDir = path35.join(path35.dirname(lockPath), ".pixelkiln", "cache");
13447
13686
  const stranded = [];
13448
13687
  const invalidState = [];
13449
13688
  const staleMapUrls = [];
@@ -13459,7 +13698,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
13459
13698
  const hasSource = Boolean(entry.sourceUrl || entry.sourceUrls?.length);
13460
13699
  let hasCache = false;
13461
13700
  for (const output of entry.outputs) {
13462
- const cached = path34.join(
13701
+ const cached = path35.join(
13463
13702
  cacheDir,
13464
13703
  cacheFileName(output.sha256, output.mediaType ?? MediaType.PNG)
13465
13704
  );
@@ -13559,16 +13798,16 @@ async function nearestExistingDirectory(start) {
13559
13798
  if ((await stat4(current)).isDirectory()) return current;
13560
13799
  } catch {
13561
13800
  }
13562
- const parent = path34.dirname(current);
13801
+ const parent = path35.dirname(current);
13563
13802
  if (parent === current) return current;
13564
13803
  current = parent;
13565
13804
  }
13566
13805
  }
13567
13806
 
13568
13807
  // src/pipeline/audit.ts
13569
- import { readFile as readFile23 } from "fs/promises";
13808
+ import { readFile as readFile24 } from "fs/promises";
13570
13809
  import { existsSync as existsSync28 } from "fs";
13571
- import path35 from "path";
13810
+ import path36 from "path";
13572
13811
  function paletteDistance(asset, reference) {
13573
13812
  if (!asset.length || !reference.length) return 0;
13574
13813
  let total = 0;
@@ -13609,7 +13848,7 @@ async function auditStyle(loaded, specs, styleId, lock) {
13609
13848
  continue;
13610
13849
  }
13611
13850
  try {
13612
- const png = decodePng(await readFile23(output.absolutePath));
13851
+ const png = decodePng(await readFile24(output.absolutePath));
13613
13852
  const palette = extractPalette(png, 12);
13614
13853
  assets.push({
13615
13854
  assetId: spec.assetId,
@@ -13633,10 +13872,10 @@ async function auditStyle(loaded, specs, styleId, lock) {
13633
13872
  let referenceFromStyleImages = false;
13634
13873
  const refPalettes = [];
13635
13874
  for (const img of style.styleImages) {
13636
- const abs = path35.resolve(loaded.root, img.path);
13875
+ const abs = path36.resolve(loaded.root, img.path);
13637
13876
  if (!existsSync28(abs)) continue;
13638
13877
  try {
13639
- refPalettes.push(extractPalette(decodePng(await readFile23(abs)), 12));
13878
+ refPalettes.push(extractPalette(decodePng(await readFile24(abs)), 12));
13640
13879
  } catch {
13641
13880
  }
13642
13881
  }
@@ -13856,24 +14095,24 @@ async function runAudit(args) {
13856
14095
  }
13857
14096
 
13858
14097
  // src/cli/commands/pack.ts
13859
- import path37 from "path";
13860
- import { readFile as readFile24 } from "fs/promises";
14098
+ import path38 from "path";
14099
+ import { readFile as readFile25 } from "fs/promises";
13861
14100
 
13862
14101
  // src/pipeline/pack.ts
13863
14102
  import { readFileSync as readFileSync5 } from "fs";
13864
- import path36 from "path";
14103
+ import path37 from "path";
13865
14104
  var DEFAULT_FRAME_SET_FPS = 12;
13866
14105
  function resolvePackInputs(raw, inputsFilePath) {
13867
14106
  if (!Array.isArray(raw) || !raw.length) {
13868
14107
  throw new Error("--inputs must be a non-empty JSON array of { id, path }");
13869
14108
  }
13870
- const baseDir = path36.dirname(path36.resolve(inputsFilePath));
14109
+ const baseDir = path37.dirname(path37.resolve(inputsFilePath));
13871
14110
  return raw.map((entry, i) => {
13872
14111
  const e = entry;
13873
14112
  if (typeof e.id !== "string" || typeof e.path !== "string") {
13874
14113
  throw new Error(`--inputs[${i}] needs string "id" and "path"`);
13875
14114
  }
13876
- return { id: e.id, path: path36.resolve(baseDir, e.path) };
14115
+ return { id: e.id, path: path37.resolve(baseDir, e.path) };
13877
14116
  });
13878
14117
  }
13879
14118
  function packSprites(inputs, options = {}) {
@@ -13945,7 +14184,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
13945
14184
  }
13946
14185
  const inputs2 = Object.entries(options.sourceOverrides).map(([id, source]) => ({
13947
14186
  id,
13948
- path: path36.resolve(source)
14187
+ path: path37.resolve(source)
13949
14188
  }));
13950
14189
  if (!inputs2.length) throw new Error(`No approved quality outputs for style "${styleId}".`);
13951
14190
  const packed2 = packSprites(inputs2, options);
@@ -13969,7 +14208,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
13969
14208
  let outputs = resolveEntryOutputs(entry, id, manifestDir);
13970
14209
  const source = sources[id];
13971
14210
  if (source && outputs.length <= 1) {
13972
- inputs.push({ id, path: path36.resolve(manifestDir, source) });
14211
+ inputs.push({ id, path: path37.resolve(manifestDir, source) });
13973
14212
  continue;
13974
14213
  }
13975
14214
  if (source && sourceIsStem(source, entry, manifestDir)) {
@@ -14003,7 +14242,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
14003
14242
  }
14004
14243
  }
14005
14244
  for (const [id, source] of Object.entries(sources)) {
14006
- 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) });
14007
14246
  }
14008
14247
  if (!inputs.length) {
14009
14248
  throw new Error(`No readable sprites for style "${styleId}"; ${noOutput.length} skipped.`);
@@ -14111,7 +14350,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
14111
14350
  const source = sources[id];
14112
14351
  const entry = lock.entries[`${styleId}/${id}`];
14113
14352
  if (source && !(entry && sourceIsStem(source, entry, manifestDir))) {
14114
- placements.push({ id, path: path36.resolve(manifestDir, source), cell });
14353
+ placements.push({ id, path: path37.resolve(manifestDir, source), cell });
14115
14354
  continue;
14116
14355
  }
14117
14356
  if (source && entry) {
@@ -14144,7 +14383,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
14144
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}`
14145
14384
  );
14146
14385
  }
14147
- 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;
14148
14387
  const mounted = mountSprites(placements, {
14149
14388
  cellWidth: mount.cellWidth,
14150
14389
  cellHeight: mount.cellHeight,
@@ -14154,7 +14393,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
14154
14393
  ...mounted,
14155
14394
  sources: mount.base ? [{
14156
14395
  id: "$base",
14157
- path: path36.resolve(manifestDir, mount.base),
14396
+ path: path37.resolve(manifestDir, mount.base),
14158
14397
  sha256: sha256(basePng),
14159
14398
  included: true
14160
14399
  }, ...mounted.sources] : mounted.sources,
@@ -14521,12 +14760,12 @@ async function runPack(args) {
14521
14760
  if (args.primaryOnly || args.outputRoles.length) {
14522
14761
  throw new Error("--primary-only and --output-role require manifest-driven pack");
14523
14762
  }
14524
- const raw = JSON.parse(await readFile24(path37.resolve(args.inputs), "utf8"));
14763
+ const raw = JSON.parse(await readFile25(path38.resolve(args.inputs), "utf8"));
14525
14764
  const inputs = resolvePackInputs(raw, args.inputs);
14526
14765
  const { png, atlas, skipped, sources } = packSprites(inputs, { columns: args.columns });
14527
14766
  const format = sheetFormat(args);
14528
- const base = path37.resolve(args.out.replace(/\.(?:png|json|tres)$/i, ""));
14529
- 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`) });
14530
14769
  const outputs = [
14531
14770
  { path: `${base}.png`, data: png },
14532
14771
  { path: `${base}${extension}`, data: document }
@@ -14539,7 +14778,7 @@ async function runPack(args) {
14539
14778
  log(
14540
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})`
14541
14780
  );
14542
- log(` ${path37.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
14781
+ log(` ${path38.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
14543
14782
  for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
14544
14783
  return;
14545
14784
  }
@@ -14547,7 +14786,7 @@ async function runPack(args) {
14547
14786
  if (args.primaryOnly && args.outputRoles.length) {
14548
14787
  throw new Error("pack accepts either --primary-only or --output-role, not both");
14549
14788
  }
14550
- const manifestDir = path37.dirname(path37.resolve(args.manifest));
14789
+ const manifestDir = path38.dirname(path38.resolve(args.manifest));
14551
14790
  const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
14552
14791
  for (const styleId of styleIds) {
14553
14792
  const packagingSpecs = await resolveSpecs(loaded, { styles: [styleId] });
@@ -14560,9 +14799,9 @@ async function runPack(args) {
14560
14799
  sources: manifestSources(loaded.manifest, styleId)
14561
14800
  });
14562
14801
  const style = loaded.manifest.styles[styleId];
14563
- 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`);
14564
14803
  const format = sheetFormat(args);
14565
- const { extension, document } = renderSheetDocument(format, atlas, { imageName: path37.basename(`${base}.png`) });
14804
+ const { extension, document } = renderSheetDocument(format, atlas, { imageName: path38.basename(`${base}.png`) });
14566
14805
  const outputs = [
14567
14806
  { path: `${base}.png`, data: png },
14568
14807
  { path: `${base}${extension}`, data: document }
@@ -14593,13 +14832,13 @@ async function runPack(args) {
14593
14832
  log(
14594
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})`
14595
14834
  );
14596
- log(` ${path37.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
14835
+ log(` ${path38.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
14597
14836
  for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
14598
14837
  }
14599
14838
  }
14600
14839
  async function runMount(args) {
14601
14840
  const { loaded, lock } = await openProject2(args);
14602
- const manifestDir = path37.dirname(path37.resolve(args.manifest));
14841
+ const manifestDir = path38.dirname(path38.resolve(args.manifest));
14603
14842
  const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
14604
14843
  for (const styleId of styleIds) {
14605
14844
  const style = loaded.manifest.styles[styleId];
@@ -14638,9 +14877,9 @@ async function runMount(args) {
14638
14877
  sources,
14639
14878
  outputRoles
14640
14879
  );
14641
- const out = path37.resolve(manifestDir, style.mount.out);
14880
+ const out = path38.resolve(manifestDir, style.mount.out);
14642
14881
  const format = sheetFormat(args);
14643
- const { extension, document } = renderSheetDocument(format, atlas, { imageName: path37.basename(out) });
14882
+ const { extension, document } = renderSheetDocument(format, atlas, { imageName: path38.basename(out) });
14644
14883
  const metadata = out.replace(/\.png$/, "") + extension;
14645
14884
  const companion = out.replace(/\.png$/, "") + ".pixelkiln.json";
14646
14885
  const outputs = [
@@ -14660,7 +14899,7 @@ async function runMount(args) {
14660
14899
  await provenanceFile("$lock", args.lock),
14661
14900
  ...qualityRecords,
14662
14901
  ...artifactSources.filter(
14663
- (source) => source.id !== "$base" || path37.resolve(source.path) !== out
14902
+ (source) => source.id !== "$base" || path38.resolve(source.path) !== out
14664
14903
  )
14665
14904
  ],
14666
14905
  options: {
@@ -14674,14 +14913,14 @@ async function runMount(args) {
14674
14913
  log(
14675
14914
  ` ${styleId}: ${atlas.frames.length} cell(s) into ${atlas.sheet.width}x${atlas.sheet.height}` + (overBase ? ` over ${style.mount.base}` : " (new sheet)")
14676
14915
  );
14677
- log(` ${path37.relative(process.cwd(), out)} + atlas/provenance JSON`);
14916
+ log(` ${path38.relative(process.cwd(), out)} + atlas/provenance JSON`);
14678
14917
  for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
14679
14918
  }
14680
14919
  }
14681
14920
  async function runExport(args) {
14682
14921
  const { loaded, specs, lock } = await openProject2(args);
14683
14922
  const format = args.format ?? "generic";
14684
- const manifestDir = path37.dirname(path37.resolve(args.manifest));
14923
+ const manifestDir = path38.dirname(path38.resolve(args.manifest));
14685
14924
  const selected = specs.filter((spec) => {
14686
14925
  if (spec.generator !== "tiles") return false;
14687
14926
  if (args.styles.length && !args.styles.includes(spec.styleId)) return false;
@@ -14697,12 +14936,12 @@ async function runExport(args) {
14697
14936
  for (const spec of selected) {
14698
14937
  const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
14699
14938
  const style = loaded.manifest.styles[spec.styleId];
14700
- const defaultBase = path37.resolve(manifestDir, style.outDir, `${spec.assetId}-tileset`);
14701
- 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;
14702
14941
  const result = exportTileset(entry, spec, {
14703
14942
  format,
14704
14943
  manifestDir,
14705
- imageName: path37.basename(`${base}.png`),
14944
+ imageName: path38.basename(`${base}.png`),
14706
14945
  columns: args.columns
14707
14946
  });
14708
14947
  const outputs = [
@@ -14720,7 +14959,7 @@ async function runExport(args) {
14720
14959
  asset: spec.assetId,
14721
14960
  columns: args.columns ?? null,
14722
14961
  format,
14723
- image: path37.basename(`${base}.png`),
14962
+ image: path38.basename(`${base}.png`),
14724
14963
  providerRules: result.generic.providerRules,
14725
14964
  style: spec.styleId,
14726
14965
  tileType: spec.tileType ?? null
@@ -14730,7 +14969,7 @@ async function runExport(args) {
14730
14969
  ` ${spec.styleId}/${spec.assetId}: ${result.generic.tiles.length} tile(s), ${result.generic.sheet.width}x${result.generic.sheet.height} (${format})`
14731
14970
  );
14732
14971
  log(
14733
- ` ${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`
14734
14973
  );
14735
14974
  }
14736
14975
  }
@@ -14743,19 +14982,19 @@ function sheetFormat(args) {
14743
14982
  }
14744
14983
 
14745
14984
  // src/cli/commands/quality.ts
14746
- import path39 from "path";
14747
- import { readFile as readFile26 } from "fs/promises";
14985
+ import path40 from "path";
14986
+ import { readFile as readFile27 } from "fs/promises";
14748
14987
 
14749
14988
  // src/pipeline/quality-regression.ts
14750
- import path38 from "path";
14989
+ import path39 from "path";
14751
14990
  import { existsSync as existsSync29 } from "fs";
14752
- import { readFile as readFile25 } from "fs/promises";
14991
+ import { readFile as readFile26 } from "fs/promises";
14753
14992
  import { z as z9 } from "zod";
14754
14993
  var SHA256_RE = /^[0-9a-f]{64}$/;
14755
14994
  var HEX_RE = /^#[0-9a-f]{6}$/;
14756
14995
  var MAX_REDMEAN_DISTANCE = 765;
14757
14996
  var QualityPathSchema = z9.string().min(1).refine(
14758
- (value) => !path38.posix.isAbsolute(value) && !value.includes("\\"),
14997
+ (value) => !path39.posix.isAbsolute(value) && !value.includes("\\"),
14759
14998
  "expected a portable relative path"
14760
14999
  );
14761
15000
  var DEFAULT_QUALITY_TOLERANCES = {
@@ -14902,10 +15141,10 @@ function measureDecodedImageQuality(png, digest2) {
14902
15141
  };
14903
15142
  }
14904
15143
  async function measureImageQuality(file) {
14905
- const absolute = path38.resolve(file);
15144
+ const absolute = path39.resolve(file);
14906
15145
  let bytes;
14907
15146
  try {
14908
- bytes = await readFile25(absolute);
15147
+ bytes = await readFile26(absolute);
14909
15148
  } catch (error) {
14910
15149
  throw new Error(`Cannot read quality image ${absolute}: ${message4(error)}`, { cause: error });
14911
15150
  }
@@ -14924,11 +15163,11 @@ function resolveQualityInputs(raw, inputsFilePath) {
14924
15163
  }
14925
15164
  const ids = parsed.data.map((entry) => entry.id);
14926
15165
  if (new Set(ids).size !== ids.length) throw new Error("--inputs contains a duplicate quality case id.");
14927
- const root = path38.dirname(path38.resolve(inputsFilePath));
15166
+ const root = path39.dirname(path39.resolve(inputsFilePath));
14928
15167
  return parsed.data.map((entry) => ({
14929
15168
  id: entry.id,
14930
- file: path38.resolve(root, entry.path),
14931
- ...entry.record ? { record: path38.resolve(root, entry.record) } : {},
15169
+ file: path39.resolve(root, entry.path),
15170
+ ...entry.record ? { record: path39.resolve(root, entry.record) } : {},
14932
15171
  tolerances: QualityToleranceSchema.parse({
14933
15172
  ...DEFAULT_QUALITY_TOLERANCES,
14934
15173
  ...entry.tolerances
@@ -14936,7 +15175,7 @@ function resolveQualityInputs(raw, inputsFilePath) {
14936
15175
  }));
14937
15176
  }
14938
15177
  function portableRelative2(from, to) {
14939
- return path38.relative(from, path38.resolve(to)).split(path38.sep).join("/") || ".";
15178
+ return path39.relative(from, path39.resolve(to)).split(path39.sep).join("/") || ".";
14940
15179
  }
14941
15180
  async function verifyRecordForImage(recordPath, imagePath) {
14942
15181
  const verification = await verifyArtifactBundle(recordPath);
@@ -14951,20 +15190,20 @@ async function verifyRecordForImage(recordPath, imagePath) {
14951
15190
  if (record.kind !== "refine" || options === null || typeof options !== "object" || options.schema !== "pixelkiln-quality") {
14952
15191
  throw new Error(`Quality record ${recordPath} is not a PixelKiln refinement record.`);
14953
15192
  }
14954
- const outputs = record.outputs.map((output) => path38.resolve(path38.dirname(recordPath), output.path));
14955
- 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))) {
14956
15195
  throw new Error(`Quality record ${recordPath} does not own ${imagePath}.`);
14957
15196
  }
14958
15197
  }
14959
15198
  async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
14960
15199
  if (!inputs.length) throw new Error("Cannot snapshot an empty quality baseline.");
14961
- const absolute = path38.resolve(baselinePath);
15200
+ const absolute = path39.resolve(baselinePath);
14962
15201
  for (const input of inputs) {
14963
- 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) {
14964
15203
  throw new Error(`Quality baseline output would overwrite an input: ${absolute}.`);
14965
15204
  }
14966
15205
  }
14967
- const root = path38.dirname(absolute);
15206
+ const root = path39.dirname(absolute);
14968
15207
  const cases = [];
14969
15208
  for (const input of [...inputs].sort((a, b) => a.id.localeCompare(b.id))) {
14970
15209
  const expected = await measureImageQuality(input.file);
@@ -14992,7 +15231,7 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
14992
15231
  });
14993
15232
  const data = Buffer.from(JSON.stringify(baseline, null, 2) + "\n");
14994
15233
  if (existsSync29(absolute) && !options.force) {
14995
- const current = await readFile25(absolute);
15234
+ const current = await readFile26(absolute);
14996
15235
  if (!current.equals(data)) {
14997
15236
  throw new OverwriteRefusedError(
14998
15237
  `Quality baseline already exists with different content: ${absolute}. Pass --force to replace it.`,
@@ -15004,10 +15243,10 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
15004
15243
  return { baseline, path: absolute, changed: result.changed.length > 0 };
15005
15244
  }
15006
15245
  async function readQualityBaseline(baselinePath) {
15007
- const absolute = path38.resolve(baselinePath);
15246
+ const absolute = path39.resolve(baselinePath);
15008
15247
  let raw;
15009
15248
  try {
15010
- raw = JSON.parse(await readFile25(absolute, "utf8"));
15249
+ raw = JSON.parse(await readFile26(absolute, "utf8"));
15011
15250
  } catch (error) {
15012
15251
  throw new Error(`Could not read quality baseline ${absolute}: ${message4(error)}`, { cause: error });
15013
15252
  }
@@ -15077,12 +15316,12 @@ function regressionViolations(expected, actual, tolerances) {
15077
15316
  return { violations, warnings };
15078
15317
  }
15079
15318
  async function checkQualityBaseline(baselinePath) {
15080
- const absolute = path38.resolve(baselinePath);
15319
+ const absolute = path39.resolve(baselinePath);
15081
15320
  const baseline = await readQualityBaseline(absolute);
15082
- const root = path38.dirname(absolute);
15321
+ const root = path39.dirname(absolute);
15083
15322
  const cases = [];
15084
15323
  for (const entry of baseline.cases) {
15085
- const file = path38.resolve(root, entry.file);
15324
+ const file = path39.resolve(root, entry.file);
15086
15325
  let actual = null;
15087
15326
  const violations = [];
15088
15327
  const warnings = [];
@@ -15095,7 +15334,7 @@ async function checkQualityBaseline(baselinePath) {
15095
15334
  violations.push(message4(error));
15096
15335
  }
15097
15336
  if (entry.record) {
15098
- const recordPath = path38.resolve(root, entry.record.path);
15337
+ const recordPath = path39.resolve(root, entry.record.path);
15099
15338
  try {
15100
15339
  const recordHash = await sha256File(recordPath);
15101
15340
  if (recordHash !== entry.record.sha256) violations.push("quality record hash changed");
@@ -15137,15 +15376,15 @@ async function runQuality(args) {
15137
15376
  if (!args.out) throw new Error("quality snapshot needs --out <pixelkiln.quality.json>.");
15138
15377
  let raw;
15139
15378
  try {
15140
- raw = JSON.parse(await readFile26(path39.resolve(args.inputs), "utf8"));
15379
+ raw = JSON.parse(await readFile27(path40.resolve(args.inputs), "utf8"));
15141
15380
  } catch (error) {
15142
15381
  throw new Error(
15143
- `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)}`,
15144
15383
  { cause: error }
15145
15384
  );
15146
15385
  }
15147
15386
  const inputs = resolveQualityInputs(raw, args.inputs);
15148
- if (path39.resolve(args.inputs) === path39.resolve(args.out)) {
15387
+ if (path40.resolve(args.inputs) === path40.resolve(args.out)) {
15149
15388
  throw new Error("quality snapshot --out must not overwrite its --inputs file.");
15150
15389
  }
15151
15390
  const result = await snapshotQualityBaseline(inputs, args.out, { force: args.force });
@@ -15158,7 +15397,7 @@ async function runQuality(args) {
15158
15397
  }, null, 2));
15159
15398
  } else {
15160
15399
  log(
15161
- ` ${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))`
15162
15401
  );
15163
15402
  log(` Review the tolerances, commit the baseline, then run quality check in CI.`);
15164
15403
  }
@@ -15184,12 +15423,12 @@ async function runQuality(args) {
15184
15423
  }
15185
15424
 
15186
15425
  // src/cli/commands/recipe.ts
15187
- import path41 from "path";
15426
+ import path42 from "path";
15188
15427
 
15189
15428
  // src/recipes.ts
15190
- import path40 from "path";
15429
+ import path41 from "path";
15191
15430
  import { existsSync as existsSync30 } from "fs";
15192
- import { readdir as readdir3, readFile as readFile27 } from "fs/promises";
15431
+ import { readdir as readdir3, readFile as readFile28 } from "fs/promises";
15193
15432
  import { fileURLToPath } from "url";
15194
15433
  import { z as z10 } from "zod";
15195
15434
  var SHA256_RE2 = /^[0-9a-f]{64}$/;
@@ -15198,7 +15437,7 @@ var VERSION_RE = /^\d+\.\d+\.\d+$/;
15198
15437
  var RECIPE_FILE = "pixelkiln.recipe.json";
15199
15438
  var RECIPE_DIR_TOKEN = "{{recipeDir}}";
15200
15439
  var PortablePathSchema = z10.string().min(1).refine((value) => {
15201
- if (value.includes("\\") || path40.posix.isAbsolute(value)) return false;
15440
+ if (value.includes("\\") || path41.posix.isAbsolute(value)) return false;
15202
15441
  const parts = value.split("/");
15203
15442
  return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
15204
15443
  }, "expected a portable relative path without '.', '..', or backslashes");
@@ -15374,10 +15613,10 @@ function message5(error) {
15374
15613
  return error instanceof Error ? error.message : String(error);
15375
15614
  }
15376
15615
  async function readRecipeFile(recipePath, bundled) {
15377
- const absolute = path40.resolve(recipePath);
15616
+ const absolute = path41.resolve(recipePath);
15378
15617
  let raw;
15379
15618
  try {
15380
- raw = JSON.parse(await readFile27(absolute, "utf8"));
15619
+ raw = JSON.parse(await readFile28(absolute, "utf8"));
15381
15620
  } catch (error) {
15382
15621
  throw new Error(`Could not read recipe ${absolute}: ${message5(error)}`, { cause: error });
15383
15622
  }
@@ -15388,13 +15627,13 @@ async function readRecipeFile(recipePath, bundled) {
15388
15627
  ` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
15389
15628
  );
15390
15629
  }
15391
- 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 };
15392
15631
  }
15393
15632
  async function walkRecipeFiles(root) {
15394
15633
  if (!existsSync30(root)) return [];
15395
15634
  const out = [];
15396
15635
  for (const entry of await readdir3(root, { withFileTypes: true })) {
15397
- const resolved = path40.join(root, entry.name);
15636
+ const resolved = path41.join(root, entry.name);
15398
15637
  if (entry.isDirectory()) out.push(...await walkRecipeFiles(resolved));
15399
15638
  else if (entry.isFile() && entry.name === RECIPE_FILE) out.push(resolved);
15400
15639
  }
@@ -15405,7 +15644,7 @@ function bundledRecipeRoot() {
15405
15644
  }
15406
15645
  async function listBundledRecipes(root = bundledRecipeRoot()) {
15407
15646
  const recipes = await Promise.all(
15408
- (await walkRecipeFiles(path40.resolve(root))).map((file) => readRecipeFile(file, true))
15647
+ (await walkRecipeFiles(path41.resolve(root))).map((file) => readRecipeFile(file, true))
15409
15648
  );
15410
15649
  return recipes.sort(
15411
15650
  (a, b) => a.recipe.id.localeCompare(b.recipe.id) || compareVersions(b.recipe.version, a.recipe.version)
@@ -15428,9 +15667,9 @@ function parseSelector(selector) {
15428
15667
  return { id: match[1], version: match[2] };
15429
15668
  }
15430
15669
  async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
15431
- const local = path40.resolve(target);
15670
+ const local = path41.resolve(target);
15432
15671
  if (existsSync30(local)) {
15433
- 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);
15434
15673
  return readRecipeFile(recipePath, false);
15435
15674
  }
15436
15675
  if (target.startsWith(".") || target.startsWith("/") || target.includes("\\")) {
@@ -15444,7 +15683,7 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
15444
15683
  return matches[0];
15445
15684
  }
15446
15685
  async function verifyFile(root, file) {
15447
- const absolute = path40.resolve(root, ...file.path.split("/"));
15686
+ const absolute = path41.resolve(root, ...file.path.split("/"));
15448
15687
  if (!existsSync30(absolute)) {
15449
15688
  return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
15450
15689
  }
@@ -15460,7 +15699,7 @@ async function verifyRecipe(target, options = {}) {
15460
15699
  const loaded = await resolveRecipe(target, options.bundledRoot);
15461
15700
  const actualIntegrity = recipeDigest(loaded.authored);
15462
15701
  const files = await Promise.all(loaded.recipe.files.map((file) => verifyFile(loaded.dir, file)));
15463
- const modelRoot = options.modelRoot ? path40.resolve(options.modelRoot) : null;
15702
+ const modelRoot = options.modelRoot ? path41.resolve(options.modelRoot) : null;
15464
15703
  const models = modelRoot ? await Promise.all(loaded.recipe.models.map(async (model) => ({
15465
15704
  ...await verifyFile(modelRoot, model),
15466
15705
  source: model.source,
@@ -15494,7 +15733,7 @@ async function verifyRecipe(target, options = {}) {
15494
15733
  };
15495
15734
  }
15496
15735
  function renderStyle(style, destination, cwd) {
15497
- const portableDir = path40.relative(cwd, destination).split(path40.sep).join("/") || ".";
15736
+ const portableDir = path41.relative(cwd, destination).split(path41.sep).join("/") || ".";
15498
15737
  const replace = (value) => {
15499
15738
  if (typeof value === "string") return value.replaceAll(RECIPE_DIR_TOKEN, portableDir);
15500
15739
  if (Array.isArray(value)) return value.map(replace);
@@ -15506,25 +15745,25 @@ function renderStyle(style, destination, cwd) {
15506
15745
  return replace(style);
15507
15746
  }
15508
15747
  async function installRecipe(target, options = {}) {
15509
- const cwd = path40.resolve(options.cwd ?? process.cwd());
15748
+ const cwd = path41.resolve(options.cwd ?? process.cwd());
15510
15749
  const loaded = await resolveRecipe(target, options.bundledRoot);
15511
15750
  const verification = await verifyRecipe(loaded.path);
15512
15751
  if (!verification.ok) {
15513
15752
  throw new Error(`Recipe ${loaded.recipe.id}@${loaded.recipe.version} failed integrity verification.`);
15514
15753
  }
15515
- 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);
15516
15755
  const sources = [
15517
15756
  { path: RECIPE_FILE, source: loaded.path },
15518
- ...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("/")) }))
15519
15758
  ];
15520
15759
  const files = await Promise.all(sources.map(async (file) => ({
15521
- path: path40.join(destination, ...file.path.split("/")),
15522
- data: await readFile27(file.source)
15760
+ path: path41.join(destination, ...file.path.split("/")),
15761
+ data: await readFile28(file.source)
15523
15762
  })));
15524
15763
  if (!options.force) {
15525
15764
  for (const file of files) {
15526
15765
  if (!existsSync30(file.path)) continue;
15527
- const current = await readFile27(file.path);
15766
+ const current = await readFile28(file.path);
15528
15767
  if (!current.equals(file.data)) {
15529
15768
  throw new OverwriteRefusedError(
15530
15769
  `Recipe destination has local changes: ${file.path}. Choose another --out or pass --force to replace declared recipe files.`,
@@ -15618,14 +15857,14 @@ async function runRecipe(args) {
15618
15857
  }, null, 2));
15619
15858
  } else {
15620
15859
  log(` installed ${result.recipe.id}@${result.recipe.version}`);
15621
- log(` ${path41.relative(process.cwd(), result.destination) || "."}`);
15860
+ log(` ${path42.relative(process.cwd(), result.destination) || "."}`);
15622
15861
  log(`
15623
15862
  Add this entry under your manifest's styles object:`);
15624
15863
  log(JSON.stringify({ [result.styleId]: result.style }, null, 2));
15625
15864
  if (result.recipe.models.length) {
15626
15865
  log(`
15627
15866
  Models are not downloaded automatically. Verify them with:`);
15628
- 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>`);
15629
15868
  }
15630
15869
  }
15631
15870
  return;
@@ -15633,7 +15872,7 @@ async function runRecipe(args) {
15633
15872
  }
15634
15873
 
15635
15874
  // src/cli/commands/refine.ts
15636
- import path42 from "path";
15875
+ import path43 from "path";
15637
15876
  async function runRefine(args) {
15638
15877
  if (args.from || args.subcommand === "approve") {
15639
15878
  if (!args.from) {
@@ -15663,12 +15902,12 @@ async function runRefine(args) {
15663
15902
  ` recovered ${result3.detection.columns}x${result3.detection.rows} native grid (${result3.detection.confidence}: ${result3.detection.consensus})`
15664
15903
  );
15665
15904
  log(` enforced ${result3.palette.length}-color palette without dithering`);
15666
- log(` wrote ${path42.relative(process.cwd(), result3.output)}`);
15667
- 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)}`);
15668
15907
  log(`
15669
15908
  Automated checks passed. Human 1\xD7 review is still required:`);
15670
15909
  log(
15671
- ` 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"`
15672
15911
  );
15673
15912
  }
15674
15913
  return;
@@ -15696,7 +15935,7 @@ async function runRefine(args) {
15696
15935
  if (args.json) log(JSON.stringify(result3, null, 2));
15697
15936
  else {
15698
15937
  log(
15699
- 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()}`
15700
15939
  );
15701
15940
  }
15702
15941
  return;
@@ -15705,7 +15944,7 @@ async function runRefine(args) {
15705
15944
  if (args.json) {
15706
15945
  log(JSON.stringify(result2, null, 2));
15707
15946
  } else {
15708
- 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)}`);
15709
15948
  for (const reason of result2.reasons) log(` ${reason}`);
15710
15949
  }
15711
15950
  if (!result2.safe) process.exitCode = 1;
@@ -15751,7 +15990,7 @@ async function runRefine(args) {
15751
15990
  log(` ${item.state.padEnd(16)} ${item.key} ${item.reason}`);
15752
15991
  if (item.state === "needs-approval") {
15753
15992
  log(
15754
- ` 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"`
15755
15994
  );
15756
15995
  }
15757
15996
  }
@@ -15803,15 +16042,15 @@ async function runTools(args) {
15803
16042
  }
15804
16043
 
15805
16044
  // src/cli/commands/workspace.ts
15806
- import path43 from "path";
16045
+ import path44 from "path";
15807
16046
  import { existsSync as existsSync31 } from "fs";
15808
16047
  async function runWorkspace(args) {
15809
- const workspacePath = path43.resolve(args.workspace ?? "pixelkiln.workspace.json");
15810
- const dir = path43.dirname(workspacePath);
16048
+ const workspacePath = path44.resolve(args.workspace ?? "pixelkiln.workspace.json");
16049
+ const dir = path44.dirname(workspacePath);
15811
16050
  if (args.subcommand === "add") {
15812
- const manifestPath = path43.resolve(args.target);
16051
+ const manifestPath = path44.resolve(args.target);
15813
16052
  const loadedTarget = await loadManifest(manifestPath);
15814
- 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");
15815
16054
  const ws = await loadWorkspace(workspacePath);
15816
16055
  const id = args.name ?? loadedTarget.manifest.name;
15817
16056
  if (ws.projects.some((p) => p.id === id)) {
@@ -15831,7 +16070,7 @@ async function runWorkspace(args) {
15831
16070
  ...args.account ? { account: args.account } : {}
15832
16071
  };
15833
16072
  await saveWorkspace(workspacePath, { version: 1, projects: [...ws.projects, project] });
15834
- log(` registered "${id}" in ${path43.relative(process.cwd(), workspacePath)}`);
16073
+ log(` registered "${id}" in ${path44.relative(process.cwd(), workspacePath)}`);
15835
16074
  log(` manifest: ${project.manifest}`);
15836
16075
  log(` lock: ${project.lock}`);
15837
16076
  if (!existsSync31(lockPath)) {
@@ -15843,7 +16082,7 @@ async function runWorkspace(args) {
15843
16082
  }
15844
16083
  if (args.subcommand === "remove") {
15845
16084
  const ws = await loadWorkspace(workspacePath);
15846
- const resolvedTarget = path43.resolve(args.target);
16085
+ const resolvedTarget = path44.resolve(args.target);
15847
16086
  const match = ws.projects.find(
15848
16087
  (p) => p.id === args.target || resolveProject(dir, p).manifestPath === resolvedTarget
15849
16088
  );
@@ -15854,7 +16093,7 @@ async function runWorkspace(args) {
15854
16093
  version: 1,
15855
16094
  projects: ws.projects.filter((p) => p !== match)
15856
16095
  });
15857
- log(` removed "${match.id}" from ${path43.relative(process.cwd(), workspacePath)}`);
16096
+ log(` removed "${match.id}" from ${path44.relative(process.cwd(), workspacePath)}`);
15858
16097
  return;
15859
16098
  }
15860
16099
  if (args.subcommand === "list") {
@@ -15866,9 +16105,9 @@ async function runWorkspace(args) {
15866
16105
  if (args.json) {
15867
16106
  log(JSON.stringify({ version: 1, workspace: workspacePath, projects: ws.projects, diagnostics }, null, 2));
15868
16107
  } else if (!ws.projects.length) {
15869
- log(` no projects registered in ${path43.relative(process.cwd(), workspacePath)}`);
16108
+ log(` no projects registered in ${path44.relative(process.cwd(), workspacePath)}`);
15870
16109
  } else {
15871
- 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)}:`);
15872
16111
  for (const p of ws.projects) {
15873
16112
  log(` ${p.id.padEnd(24)} ${p.manifest.padEnd(40)} (${p.provider}${p.account ? `, ${p.account}` : ""})`);
15874
16113
  }
@@ -15886,7 +16125,7 @@ async function runWorkspace(args) {
15886
16125
  if (args.json) {
15887
16126
  log(JSON.stringify({ ...report, workspace: workspacePath }, null, 2));
15888
16127
  } else {
15889
- log(` workspace: ${path43.relative(process.cwd(), workspacePath)}`);
16128
+ log(` workspace: ${path44.relative(process.cwd(), workspacePath)}`);
15890
16129
  for (const p of report.projects) {
15891
16130
  if (p.error) {
15892
16131
  log(`
@@ -15941,7 +16180,7 @@ async function runVersion() {
15941
16180
  for (const up of ["../package.json", "../../package.json"]) {
15942
16181
  const url = new URL(up, import.meta.url);
15943
16182
  if (!existsSync32(url)) continue;
15944
- const pkg = JSON.parse(await readFile28(url, "utf8"));
16183
+ const pkg = JSON.parse(await readFile29(url, "utf8"));
15945
16184
  if (pkg.name === "pixelkiln") {
15946
16185
  log(`${pkg.name} ${pkg.version}`);
15947
16186
  return;