pixelkiln 0.41.0 → 0.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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")));
@@ -665,8 +665,16 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
665
665
  parent: spec.character.parentAssetId,
666
666
  parentSha256: spec.character.parentSha256 ?? null,
667
667
  state: spec.character.state,
668
- animation: spec.character.animation
668
+ animation: spec.character.animation,
669
+ // Absent keys keep the hashes of manifests that never set these.
670
+ proportions: spec.character.proportions,
671
+ textGuidanceScale: spec.character.textGuidanceScale,
672
+ isometric: spec.character.isometric,
673
+ reference: spec.character.reference ? Object.fromEntries(Object.entries(spec.character.reference).map(([direction, image]) => [direction, image.sha256])) : void 0
669
674
  } : void 0,
675
+ // A mirror's bytes come from its source's recorded outputs; the plan
676
+ // compares those directly, so only the choice of source is identity.
677
+ mirror: spec.mirror ? { of: spec.mirror.sourceAssetId } : void 0,
670
678
  revision: spec.revision ? {
671
679
  mode: spec.revision.mode,
672
680
  from: spec.revision.sourceAssetId,
@@ -678,9 +686,9 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
678
686
  );
679
687
  }
680
688
 
681
- // src/outputs.ts
682
- import { existsSync as existsSync2 } from "fs";
683
- import path3 from "path";
689
+ // src/pipeline/mirror.ts
690
+ import { mkdir as mkdir2, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "fs/promises";
691
+ import path4 from "path";
684
692
 
685
693
  // src/lock.ts
686
694
  import { readFile, writeFile, rename, mkdir, rmdir, rm, stat } from "fs/promises";
@@ -769,7 +777,28 @@ var CHARACTER_DIRECTIONS_8 = [
769
777
  "south-west"
770
778
  ];
771
779
  var CHARACTER_DIRECTIONS_4 = ["south", "west", "east", "north"];
780
+ var MIRRORED_DIRECTION = {
781
+ south: "south",
782
+ "south-east": "south-west",
783
+ east: "west",
784
+ "north-east": "north-west",
785
+ north: "north",
786
+ "north-west": "north-east",
787
+ west: "east",
788
+ "south-west": "south-east"
789
+ };
772
790
  var CharacterModeSchema = z.enum(["standard", "v3", "pro"]);
791
+ var CHARACTER_PROPORTION_PRESETS = ["default", "chibi", "cartoon", "stylized", "realistic_male", "realistic_female", "heroic"];
792
+ var CharacterProportionsSchema = z.union([
793
+ z.enum(CHARACTER_PROPORTION_PRESETS),
794
+ z.object({
795
+ headSize: z.number().min(0.5).max(2).optional(),
796
+ armsLength: z.number().min(0.5).max(2).optional(),
797
+ legsLength: z.number().min(0.5).max(2).optional(),
798
+ shoulderWidth: z.number().min(0.5).max(2).optional(),
799
+ hipWidth: z.number().min(0.5).max(2).optional()
800
+ }).strict()
801
+ ]);
773
802
  var CharacterAnimationModeSchema = z.enum(["template", "v3", "pro"]);
774
803
  var CharacterStateSchema = z.object({
775
804
  /** The base character, or another state, in the same style. */
@@ -928,6 +957,16 @@ var StyleObjectSchema = z.object({
928
957
  directions: z.union([z.literal(4), z.literal(8)]).optional(),
929
958
  /** `character` only. Body template: `mannequin` (default) or a quadruped (`bear`, `cat`, `dog`, `horse`, `lion`). */
930
959
  template: z.string().min(1).optional(),
960
+ /** `character` only, `standard` humanoid bases. A preset or multipliers; an asset may override it. */
961
+ proportions: CharacterProportionsSchema.optional(),
962
+ /**
963
+ * `character` only. How closely a `standard` base and a template loop
964
+ * follow their text, 1 to 20; PixelLab's default is 8. The v3 and pro
965
+ * engines do not take it.
966
+ */
967
+ textGuidanceScale: z.number().min(1).max(20).optional(),
968
+ /** `character` only. Draw `standard` bases and every loop in isometric view. */
969
+ isometric: z.boolean().optional(),
931
970
  /** Fixed seed for reproducibility where the endpoint supports it. */
932
971
  seed: z.number().int().optional(),
933
972
  /**
@@ -1028,8 +1067,8 @@ var InheritedStyleSchema = StyleObjectSchema.partial().extend({
1028
1067
  }).strict();
1029
1068
  var StyleInputSchema = z.union([StyleSchema, InheritedStyleSchema]);
1030
1069
  var AssetSchema = z.object({
1031
- /** The subject. Style wrapping comes from the style's prefix/suffix. */
1032
- prompt: z.string(),
1070
+ /** The subject. Style wrapping comes from the style's prefix/suffix. Only a `mirror` may leave it out. */
1071
+ prompt: z.string().optional(),
1033
1072
  /** Subdirectory under the style's outDir. Optional. */
1034
1073
  category: z.string().optional(),
1035
1074
  /** Overrides the style default. `map` generator only. */
@@ -1087,6 +1126,32 @@ var AssetSchema = z.object({
1087
1126
  state: CharacterStateSchema.optional(),
1088
1127
  /** `character` styles: this asset is a loop of another character asset in one direction. */
1089
1128
  animation: CharacterAnimationSchema.optional(),
1129
+ /** `character` styles, `standard` humanoid bases: this character's proportions, over the style's. */
1130
+ proportions: CharacterProportionsSchema.optional(),
1131
+ /**
1132
+ * `character` styles, bases only: the character's own sprite, which
1133
+ * PixelLab rotates into the other directions instead of drawing from
1134
+ * the prompt. A manifest-relative PNG or JPEG of the south-facing
1135
+ * sprite, or an object keyed by direction (`{ "south": ..., "east":
1136
+ * ... }`); quadrupeds in `standard` mode need south and east, and only
1137
+ * `standard` takes more than south. `standard` wants each image at the
1138
+ * style's size; v3 accepts up to 256px, pro up to 168px. The prompt
1139
+ * still guides the result.
1140
+ */
1141
+ reference: z.union([z.string().min(1), z.record(CharacterDirectionSchema, z.string().min(1))]).optional(),
1142
+ /**
1143
+ * Another asset of the same style flipped left to right, made locally
1144
+ * from that asset's downloaded files at no generation cost.
1145
+ *
1146
+ * The saving is in character loops, where each direction is its own
1147
+ * generation: a walk facing west mirrored is the walk facing east, and a
1148
+ * diagonal mirrors to the other diagonal. South and north mirror onto
1149
+ * themselves and are refused. A single image or a frame set from any
1150
+ * generator can be mirrored too; a tile set cannot, since its edges
1151
+ * carry meaning. Mirroring swaps handedness, so an asymmetric character
1152
+ * (a sword hand, a parted fringe) comes out the other way round.
1153
+ */
1154
+ mirror: z.string().min(1).optional(),
1090
1155
  /**
1091
1156
  * Generated output role to place in `cell` when this asset expands to
1092
1157
  * several files. Omit for ordinary single-output assets. A structural set
@@ -1126,7 +1191,7 @@ var AssetSchema = z.object({
1126
1191
  path: ["revision"]
1127
1192
  });
1128
1193
  }
1129
- const shapes = [asset.revision && "revision", asset.state && "state", asset.animation && "animation"].filter(Boolean);
1194
+ const shapes = [asset.revision && "revision", asset.state && "state", asset.animation && "animation", asset.mirror && "mirror"].filter(Boolean);
1130
1195
  if (shapes.length > 1) {
1131
1196
  context.addIssue({
1132
1197
  code: z.ZodIssueCode.custom,
@@ -1134,6 +1199,30 @@ var AssetSchema = z.object({
1134
1199
  path: [shapes[1]]
1135
1200
  });
1136
1201
  }
1202
+ if (asset.mirror && (asset.source || Object.keys(asset.sourceByStyle).length)) {
1203
+ context.addIssue({
1204
+ code: z.ZodIssueCode.custom,
1205
+ message: "a mirror is made from another asset's files; it cannot also declare a source",
1206
+ path: ["mirror"]
1207
+ });
1208
+ }
1209
+ if (asset.prompt === void 0 && !asset.mirror) {
1210
+ context.addIssue({
1211
+ code: z.ZodIssueCode.custom,
1212
+ message: "Required",
1213
+ path: ["prompt"]
1214
+ });
1215
+ }
1216
+ if (asset.reference && (asset.state || asset.animation || asset.mirror)) {
1217
+ context.addIssue({
1218
+ code: z.ZodIssueCode.custom,
1219
+ message: "a reference sprite belongs on a base; a state, animation, or mirror takes its look from its parent",
1220
+ path: ["reference"]
1221
+ });
1222
+ }
1223
+ if (typeof asset.reference === "object" && !Object.keys(asset.reference).length) {
1224
+ context.addIssue({ code: z.ZodIssueCode.custom, message: "a reference needs at least a south image", path: ["reference"] });
1225
+ }
1137
1226
  });
1138
1227
  var DEFAULT_HISTORY_LIMIT = 5;
1139
1228
  var HistoryLimitSchema = z.number().int().min(0).max(100);
@@ -1223,6 +1312,11 @@ var LockEntrySchema = z.object({
1223
1312
  maskSha256: z.string().regex(/^[0-9a-f]{64}$/).optional(),
1224
1313
  strength: z.number().min(0).max(1).optional()
1225
1314
  }).strict().nullable().default(null),
1315
+ /** For a mirror: the asset it flips and a hash over that asset's output hashes when this was made. */
1316
+ mirror: z.object({
1317
+ sourceAssetId: z.string().min(1),
1318
+ sourceSha256: z.string().regex(/^[0-9a-f]{64}$/)
1319
+ }).strict().nullable().default(null),
1226
1320
  /**
1227
1321
  * Output hashes owned by the previous generation while its replacement is
1228
1322
  * pending. They authorize replacing only unchanged PixelKiln-owned files.
@@ -1922,6 +2016,8 @@ function cacheFileName(hash, mediaType = MediaType.PNG) {
1922
2016
  }
1923
2017
 
1924
2018
  // src/outputs.ts
2019
+ import { existsSync as existsSync2 } from "fs";
2020
+ import path3 from "path";
1925
2021
  function portableOutputPath(file, manifestDir) {
1926
2022
  const root = path3.resolve(manifestDir);
1927
2023
  const absolute = path3.isAbsolute(file) ? path3.normalize(file) : path3.resolve(root, file);
@@ -2047,21 +2143,219 @@ function frameSetFps(entry) {
2047
2143
  return null;
2048
2144
  }
2049
2145
 
2146
+ // src/palette.ts
2147
+ function normalizePalette(values) {
2148
+ const colors = /* @__PURE__ */ new Map();
2149
+ for (const value of values) {
2150
+ const rgb = parseHex(value);
2151
+ const hex2 = `#${rgb.r.toString(16).padStart(2, "0")}${rgb.g.toString(16).padStart(2, "0")}${rgb.b.toString(16).padStart(2, "0")}`;
2152
+ if (!colors.has(hex2)) colors.set(hex2, { hex: hex2, ...rgb });
2153
+ }
2154
+ if (colors.size < 2 || colors.size > 256) {
2155
+ throw new Error(`A palette must contain 2\u2013256 unique colors; got ${colors.size}.`);
2156
+ }
2157
+ return [...colors.values()];
2158
+ }
2159
+ function colorDistance(a, b) {
2160
+ const rmean = (a.r + b.r) / 2;
2161
+ const dr = a.r - b.r;
2162
+ const dg = a.g - b.g;
2163
+ const db = a.b - b.b;
2164
+ return Math.sqrt(
2165
+ (512 + rmean) * dr * dr / 256 + 4 * dg * dg + (767 - rmean) * db * db / 256
2166
+ );
2167
+ }
2168
+ function quantize(png, palette) {
2169
+ const colors = normalizePalette(palette);
2170
+ const pixels = Buffer.from(png.pixels);
2171
+ const nearestOf = /* @__PURE__ */ new Map();
2172
+ let changed = 0;
2173
+ for (let i = 0; i < pixels.length; i += 4) {
2174
+ const alpha = pixels[i + 3];
2175
+ if (alpha === 0) {
2176
+ if (pixels[i] || pixels[i + 1] || pixels[i + 2]) changed++;
2177
+ pixels[i] = 0;
2178
+ pixels[i + 1] = 0;
2179
+ pixels[i + 2] = 0;
2180
+ continue;
2181
+ }
2182
+ const key = pixels[i] << 16 | pixels[i + 1] << 8 | pixels[i + 2];
2183
+ let nearest = nearestOf.get(key);
2184
+ if (!nearest) {
2185
+ const pixel = { r: pixels[i], g: pixels[i + 1], b: pixels[i + 2] };
2186
+ nearest = colors[0];
2187
+ let distance = colorDistance(pixel, nearest);
2188
+ for (let j = 1; j < colors.length; j++) {
2189
+ const candidate = colors[j];
2190
+ const candidateDistance = colorDistance(pixel, candidate);
2191
+ if (candidateDistance < distance) {
2192
+ nearest = candidate;
2193
+ distance = candidateDistance;
2194
+ }
2195
+ }
2196
+ nearestOf.set(key, nearest);
2197
+ }
2198
+ if (pixels[i] !== nearest.r || pixels[i + 1] !== nearest.g || pixels[i + 2] !== nearest.b) changed++;
2199
+ pixels[i] = nearest.r;
2200
+ pixels[i + 1] = nearest.g;
2201
+ pixels[i + 2] = nearest.b;
2202
+ }
2203
+ return { png: { width: png.width, height: png.height, pixels }, changed };
2204
+ }
2205
+ function quantizeToPalette(png, palette) {
2206
+ return quantize(png, palette).png;
2207
+ }
2208
+
2209
+ // src/pipeline/postprocess.ts
2210
+ function postprocessFor(spec) {
2211
+ if (!spec.enforcePalette || spec.palette.length < 2) return void 0;
2212
+ return {
2213
+ palette: {
2214
+ colors: normalizePalette(spec.palette).map((color) => color.hex),
2215
+ dither: "none",
2216
+ distance: "redmean"
2217
+ }
2218
+ };
2219
+ }
2220
+ function samePostprocess(a, b) {
2221
+ const pa = a?.palette;
2222
+ const pb = b?.palette;
2223
+ if (!pa && !pb) return true;
2224
+ if (!pa || !pb) return false;
2225
+ return pa.dither === pb.dither && pa.distance === pb.distance && pa.colors.length === pb.colors.length && pa.colors.every((color, i) => color === pb.colors[i]);
2226
+ }
2227
+ function postprocessCurrent(entry, spec) {
2228
+ return samePostprocess(entry.postprocess, postprocessFor(spec));
2229
+ }
2230
+ function applyPostprocess(bytes, mediaType, record) {
2231
+ if (!record?.palette || mediaType !== MediaType.PNG) return { bytes, changed: false };
2232
+ const png = decodePng(bytes);
2233
+ const { png: snapped, changed } = quantize(png, record.palette.colors);
2234
+ if (!changed) return { bytes, changed: false };
2235
+ return { bytes: encodeRgbaPng(snapped.width, snapped.height, snapped.pixels), changed: true };
2236
+ }
2237
+
2238
+ // src/pipeline/mirror.ts
2239
+ function mirrorSourceHash(source) {
2240
+ return sha256(JSON.stringify(source.outputs.map((output) => output.sha256)));
2241
+ }
2242
+ function mirrorCurrent(entry, source) {
2243
+ return Boolean(entry.mirror && source && entry.mirror.sourceSha256 === mirrorSourceHash(source));
2244
+ }
2245
+ function mirroredRole(role) {
2246
+ if (role && role in MIRRORED_DIRECTION) return MIRRORED_DIRECTION[role];
2247
+ return role;
2248
+ }
2249
+ function flipHorizontal(png) {
2250
+ const { width, height, pixels } = decodePng(png);
2251
+ const flipped = Buffer.alloc(pixels.length);
2252
+ const stride = width * 4;
2253
+ for (let y = 0; y < height; y++) {
2254
+ const row = y * stride;
2255
+ for (let x = 0; x < width; x++) {
2256
+ pixels.copy(flipped, row + (width - 1 - x) * 4, row + x * 4, row + x * 4 + 4);
2257
+ }
2258
+ }
2259
+ return encodeRgbaPng(width, height, flipped);
2260
+ }
2261
+ async function deriveMirror(spec, lock, lockPath, opts = {}) {
2262
+ const log2 = opts.onProgress ?? (() => {
2263
+ });
2264
+ const mirror = spec.mirror;
2265
+ const key = lockKey(spec.styleId, spec.assetId);
2266
+ const sourceKey = lockKey(spec.styleId, mirror.sourceAssetId);
2267
+ const source = lock.entries[sourceKey];
2268
+ if (!source || source.status !== "downloaded" || !source.outputs.length) {
2269
+ throw new Error(`mirror source ${sourceKey} has no downloaded outputs`);
2270
+ }
2271
+ const members = source.outputs.map((output, index) => ({
2272
+ file: currentEntryOutputPath(source, mirror.sourceSpec, index),
2273
+ role: mirroredRole(output.role)
2274
+ }));
2275
+ if (members.every((member) => member.role && CHARACTER_DIRECTIONS_8.includes(member.role))) {
2276
+ members.sort((a, b) => CHARACTER_DIRECTIONS_8.indexOf(a.role) - CHARACTER_DIRECTIONS_8.indexOf(b.role));
2277
+ }
2278
+ const outputs = [];
2279
+ const written = [];
2280
+ for (const [index, member] of members.entries()) {
2281
+ const bytes = flipHorizontal(await readFile2(member.file));
2282
+ const target = expectedOutputPath(spec, member.role, index, members.length, MediaType.PNG);
2283
+ await mkdir2(path4.dirname(target), { recursive: true });
2284
+ const tmp = `${target}.pixelkiln.tmp`;
2285
+ await writeFile2(tmp, bytes);
2286
+ await rename2(tmp, target);
2287
+ written.push(target);
2288
+ outputs.push({
2289
+ path: portableOutputPath(target, spec.root),
2290
+ sha256: sha256(bytes),
2291
+ ...member.role ? { role: member.role } : {},
2292
+ mediaType: MediaType.PNG
2293
+ });
2294
+ }
2295
+ const sourceMetadata = source.providerMetadata?.[source.provider] ?? {};
2296
+ const character = spec.character;
2297
+ const entry = upsert(lock, key, {
2298
+ styleId: spec.styleId,
2299
+ assetId: spec.assetId,
2300
+ specHash: spec.specHash,
2301
+ generator: spec.generator,
2302
+ tileFeature: null,
2303
+ prompt: "",
2304
+ width: spec.width,
2305
+ height: spec.height,
2306
+ revision: null,
2307
+ mirror: { sourceAssetId: mirror.sourceAssetId, sourceSha256: mirrorSourceHash(source) },
2308
+ supersededOutputs: [],
2309
+ jobId: null,
2310
+ submissionComplete: void 0,
2311
+ reviewObjectId: null,
2312
+ objectId: null,
2313
+ candidateIndex: null,
2314
+ status: "downloaded",
2315
+ error: null,
2316
+ outputs,
2317
+ providerMetadata: {
2318
+ [source.provider]: {
2319
+ ...sourceMetadata.frameSet ? { frameSet: sourceMetadata.frameSet } : {},
2320
+ ...character ? {
2321
+ character: {
2322
+ kind: character.kind,
2323
+ mirrorOf: mirror.sourceAssetId,
2324
+ ...character.animation ? { direction: character.animation.direction } : {}
2325
+ }
2326
+ } : {}
2327
+ }
2328
+ },
2329
+ sourceUrl: null,
2330
+ sourceUrls: [],
2331
+ submittedAt: (/* @__PURE__ */ new Date()).toISOString(),
2332
+ downloadedAt: (/* @__PURE__ */ new Date()).toISOString(),
2333
+ history: [],
2334
+ cost: 0,
2335
+ costUnit: spec.costUnit,
2336
+ provider: source.provider,
2337
+ postprocess: postprocessFor(spec)
2338
+ });
2339
+ await saveLock(lockPath, lock);
2340
+ log2(` mirror ${key} \u2190 ${sourceKey} (${written.length} file${written.length === 1 ? "" : "s"} flipped, no generation cost)`);
2341
+ return entry;
2342
+ }
2343
+
2050
2344
  // src/pipeline/quality-profile.ts
2051
2345
  import { existsSync as existsSync3 } from "fs";
2052
- import path6 from "path";
2346
+ import path7 from "path";
2053
2347
 
2054
2348
  // src/pipeline/refine.ts
2055
- import path5 from "path";
2349
+ import path6 from "path";
2056
2350
  import { execFile } from "child_process";
2057
- import { mkdtemp, readFile as readFile3, rm as rm3 } from "fs/promises";
2351
+ import { mkdtemp, readFile as readFile4, rm as rm3 } from "fs/promises";
2058
2352
  import { tmpdir } from "os";
2059
2353
  import { promisify } from "util";
2060
2354
 
2061
2355
  // src/artifacts.ts
2062
- import path4 from "path";
2356
+ import path5 from "path";
2063
2357
  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";
2358
+ import { mkdir as mkdir3, readFile as readFile3, rename as rename3, rm as rm2, writeFile as writeFile3 } from "fs/promises";
2065
2359
  var activeTransactions = /* @__PURE__ */ new Set();
2066
2360
  function message(error) {
2067
2361
  return error instanceof Error ? error.message : String(error);
@@ -2070,7 +2364,7 @@ function digest(data) {
2070
2364
  return createHash2("sha256").update(data).digest("hex");
2071
2365
  }
2072
2366
  function portableRelative(from, to) {
2073
- return path4.relative(from, path4.resolve(to)).split(path4.sep).join("/") || ".";
2367
+ return path5.relative(from, path5.resolve(to)).split(path5.sep).join("/") || ".";
2074
2368
  }
2075
2369
  function canonical(value) {
2076
2370
  if (value === null || typeof value === "string" || typeof value === "boolean") return value;
@@ -2120,12 +2414,12 @@ function parseArtifactManifest(absolute, data) {
2120
2414
  return raw;
2121
2415
  }
2122
2416
  async function readArtifactBundleManifest(manifestPath) {
2123
- const absolute = path4.resolve(manifestPath);
2124
- return parseArtifactManifest(absolute, await readFile2(absolute, "utf8"));
2417
+ const absolute = path5.resolve(manifestPath);
2418
+ return parseArtifactManifest(absolute, await readFile3(absolute, "utf8"));
2125
2419
  }
2126
2420
  async function readOptional(file) {
2127
2421
  try {
2128
- return await readFile2(file);
2422
+ return await readFile3(file);
2129
2423
  } catch (error) {
2130
2424
  if (isCode(error, "ENOENT")) return null;
2131
2425
  throw error;
@@ -2144,8 +2438,8 @@ function processIsAlive(pid) {
2144
2438
  }
2145
2439
  }
2146
2440
  function validTemporaryPath(candidate, destination, type) {
2147
- return path4.dirname(candidate) === path4.dirname(destination) && path4.basename(candidate).startsWith(
2148
- `.${path4.basename(destination)}.pixelkiln-${type}-`
2441
+ return path5.dirname(candidate) === path5.dirname(destination) && path5.basename(candidate).startsWith(
2442
+ `.${path5.basename(destination)}.pixelkiln-${type}-`
2149
2443
  );
2150
2444
  }
2151
2445
  function parseTransaction(journal, data) {
@@ -2165,7 +2459,7 @@ async function removeTransactionFiles(journal) {
2165
2459
  await rm2(transactionMarker(journal), { force: true });
2166
2460
  }
2167
2461
  async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
2168
- const journal = path4.resolve(recoveryFile);
2462
+ const journal = path5.resolve(recoveryFile);
2169
2463
  const bytes = await readOptional(journal);
2170
2464
  if (!bytes) {
2171
2465
  await rm2(transactionMarker(journal), { force: true });
@@ -2181,7 +2475,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
2181
2475
  );
2182
2476
  }
2183
2477
  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")) {
2478
+ 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
2479
  throw new Error(
2186
2480
  `Refusing unsafe artifact recovery from ${journal}; its destinations or temporary paths do not match the current bundle.`
2187
2481
  );
@@ -2207,7 +2501,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
2207
2501
  }
2208
2502
  await remove2(entry.destination, errors);
2209
2503
  try {
2210
- await rename2(entry.backup, entry.destination);
2504
+ await rename3(entry.backup, entry.destination);
2211
2505
  } catch (error) {
2212
2506
  errors.push(
2213
2507
  `could not restore ${entry.destination}; previous file remains at ${entry.backup}: ${message(error)}`
@@ -2227,7 +2521,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
2227
2521
  await removeTransactionFiles(journal);
2228
2522
  }
2229
2523
  function createArtifactBundleManifest(manifestPath, outputs, provenance) {
2230
- const root = path4.dirname(path4.resolve(manifestPath));
2524
+ const root = path5.dirname(path5.resolve(manifestPath));
2231
2525
  const sources = provenance.sources.map((source) => ({
2232
2526
  ...source,
2233
2527
  path: portableRelative(root, source.path)
@@ -2257,11 +2551,11 @@ function withArtifactManifest(manifestPath, outputs, provenance) {
2257
2551
  ];
2258
2552
  }
2259
2553
  async function writeManagedArtifactBundle(manifestPath, outputs, provenance, options = {}) {
2260
- const absoluteManifest = path4.resolve(manifestPath);
2554
+ const absoluteManifest = path5.resolve(manifestPath);
2261
2555
  const recoveryFile = `${absoluteManifest}.transaction`;
2262
2556
  const allowedDestinations = /* @__PURE__ */ new Set([
2263
2557
  absoluteManifest,
2264
- ...outputs.map((output) => path4.resolve(output.path))
2558
+ ...outputs.map((output) => path5.resolve(output.path))
2265
2559
  ]);
2266
2560
  await recoverArtifactTransaction(recoveryFile, allowedDestinations);
2267
2561
  const existingManifestBytes = await readOptional(absoluteManifest);
@@ -2279,13 +2573,13 @@ async function writeManagedArtifactBundle(manifestPath, outputs, provenance, opt
2279
2573
  }
2280
2574
  }
2281
2575
  if (!options.force) {
2282
- const root = path4.dirname(absoluteManifest);
2576
+ const root = path5.dirname(absoluteManifest);
2283
2577
  const recorded = new Map(
2284
- previous?.outputs.map((output) => [path4.resolve(root, output.path), output.sha256]) ?? []
2578
+ previous?.outputs.map((output) => [path5.resolve(root, output.path), output.sha256]) ?? []
2285
2579
  );
2286
2580
  const conflicts = [];
2287
2581
  for (const output of outputs) {
2288
- const destination = path4.resolve(output.path);
2582
+ const destination = path5.resolve(output.path);
2289
2583
  const current = await readOptional(destination);
2290
2584
  if (!current || digest(current) === digest(output.data)) continue;
2291
2585
  const expected = recorded.get(destination);
@@ -2307,15 +2601,15 @@ Pass { force: true } (CLI: --force) to take ownership and replace the complete b
2307
2601
  );
2308
2602
  }
2309
2603
  async function verifyArtifactBundle(manifestPath) {
2310
- const absolute = path4.resolve(manifestPath);
2604
+ const absolute = path5.resolve(manifestPath);
2311
2605
  const raw = await readArtifactBundleManifest(absolute);
2312
- const root = path4.dirname(absolute);
2606
+ const root = path5.dirname(absolute);
2313
2607
  const changedSources = [];
2314
2608
  const changedOutputs = [];
2315
2609
  for (const source of raw.sources) {
2316
2610
  let actual = null;
2317
2611
  try {
2318
- actual = digest(await readFile2(path4.resolve(root, source.path)));
2612
+ actual = digest(await readFile3(path5.resolve(root, source.path)));
2319
2613
  } catch (error) {
2320
2614
  if (!isCode(error, "ENOENT")) throw error;
2321
2615
  }
@@ -2323,7 +2617,7 @@ async function verifyArtifactBundle(manifestPath) {
2323
2617
  }
2324
2618
  for (const output of raw.outputs) {
2325
2619
  try {
2326
- if (digest(await readFile2(path4.resolve(root, output.path))) !== output.sha256) {
2620
+ if (digest(await readFile3(path5.resolve(root, output.path))) !== output.sha256) {
2327
2621
  changedOutputs.push(output.path);
2328
2622
  }
2329
2623
  } catch (error) {
@@ -2362,7 +2656,7 @@ async function rollback(prepared) {
2362
2656
  for (const artifact of [...prepared].reverse()) {
2363
2657
  if (!artifact.backup || !artifact.backedUp) continue;
2364
2658
  try {
2365
- await rename2(artifact.backup, artifact.destination);
2659
+ await rename3(artifact.backup, artifact.destination);
2366
2660
  artifact.backedUp = false;
2367
2661
  } catch (error) {
2368
2662
  errors.push(
@@ -2379,7 +2673,7 @@ async function writeArtifactBundle(files, options = {}) {
2379
2673
  if (!files.length) throw new Error("An artifact bundle must contain at least one file.");
2380
2674
  const normalized = files.map((file) => {
2381
2675
  if (!file.path.trim()) throw new Error("Artifact paths cannot be empty.");
2382
- return { destination: path4.resolve(file.path), data: Buffer.from(file.data) };
2676
+ return { destination: path5.resolve(file.path), data: Buffer.from(file.data) };
2383
2677
  });
2384
2678
  const destinations = /* @__PURE__ */ new Set();
2385
2679
  for (const artifact of normalized) {
@@ -2395,7 +2689,7 @@ async function writeArtifactBundle(files, options = {}) {
2395
2689
  const unchanged = [];
2396
2690
  for (const artifact of normalized) {
2397
2691
  try {
2398
- const current = await readFile2(artifact.destination);
2692
+ const current = await readFile3(artifact.destination);
2399
2693
  if (current.equals(artifact.data)) {
2400
2694
  unchanged.push(artifact.destination);
2401
2695
  } else {
@@ -2409,21 +2703,21 @@ async function writeArtifactBundle(files, options = {}) {
2409
2703
  if (!changed.length) return { changed: [], unchanged };
2410
2704
  const token = randomUUID();
2411
2705
  for (const [index, artifact] of changed.entries()) {
2412
- const basename = path4.basename(artifact.destination);
2413
- artifact.stage = path4.join(
2414
- path4.dirname(artifact.destination),
2706
+ const basename = path5.basename(artifact.destination);
2707
+ artifact.stage = path5.join(
2708
+ path5.dirname(artifact.destination),
2415
2709
  `.${basename}.pixelkiln-stage-${token}-${index}`
2416
2710
  );
2417
2711
  if (artifact.existed) {
2418
- artifact.backup = path4.join(
2419
- path4.dirname(artifact.destination),
2712
+ artifact.backup = path5.join(
2713
+ path5.dirname(artifact.destination),
2420
2714
  `.${basename}.pixelkiln-backup-${token}-${index}`
2421
2715
  );
2422
2716
  }
2423
2717
  }
2424
- const journal = options.recoveryFile ? path4.resolve(options.recoveryFile) : null;
2718
+ const journal = options.recoveryFile ? path5.resolve(options.recoveryFile) : null;
2425
2719
  if (journal) {
2426
- await mkdir2(path4.dirname(journal), { recursive: true });
2720
+ await mkdir3(path5.dirname(journal), { recursive: true });
2427
2721
  const transaction = {
2428
2722
  format: "pixelkiln-artifact-transaction",
2429
2723
  version: 1,
@@ -2437,7 +2731,7 @@ async function writeArtifactBundle(files, options = {}) {
2437
2731
  }))
2438
2732
  };
2439
2733
  try {
2440
- await writeFile2(journal, JSON.stringify(transaction, null, 2) + "\n", { flag: "wx" });
2734
+ await writeFile3(journal, JSON.stringify(transaction, null, 2) + "\n", { flag: "wx" });
2441
2735
  activeTransactions.add(journal);
2442
2736
  } catch (error) {
2443
2737
  throw new Error(
@@ -2448,9 +2742,9 @@ async function writeArtifactBundle(files, options = {}) {
2448
2742
  }
2449
2743
  try {
2450
2744
  for (const [index, artifact] of changed.entries()) {
2451
- await mkdir2(path4.dirname(artifact.destination), { recursive: true });
2745
+ await mkdir3(path5.dirname(artifact.destination), { recursive: true });
2452
2746
  await options.beforeStage?.(artifact.destination, index);
2453
- await writeFile2(artifact.stage, artifact.data, { flag: "wx" });
2747
+ await writeFile3(artifact.stage, artifact.data, { flag: "wx" });
2454
2748
  }
2455
2749
  } catch (error) {
2456
2750
  const cleanupErrors2 = await rollback(changed);
@@ -2463,17 +2757,17 @@ async function writeArtifactBundle(files, options = {}) {
2463
2757
  try {
2464
2758
  for (const artifact of changed) {
2465
2759
  if (!artifact.existed) continue;
2466
- await rename2(artifact.destination, artifact.backup);
2760
+ await rename3(artifact.destination, artifact.backup);
2467
2761
  artifact.backedUp = true;
2468
2762
  }
2469
2763
  for (const [index, artifact] of changed.entries()) {
2470
2764
  await options.beforePromote?.(artifact.destination, index);
2471
- await rename2(artifact.stage, artifact.destination);
2765
+ await rename3(artifact.stage, artifact.destination);
2472
2766
  artifact.stage = void 0;
2473
2767
  artifact.promoted = true;
2474
2768
  }
2475
2769
  if (journal) {
2476
- await writeFile2(transactionMarker(journal), `${token}
2770
+ await writeFile3(transactionMarker(journal), `${token}
2477
2771
  `, { flag: "wx" });
2478
2772
  durableCommit = true;
2479
2773
  await options.afterCommit?.();
@@ -2515,69 +2809,6 @@ async function writeArtifactBundle(files, options = {}) {
2515
2809
  };
2516
2810
  }
2517
2811
 
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
2812
  // src/pipeline/refine.ts
2582
2813
  var execFileAsync = promisify(execFile);
2583
2814
  var PIXEL_ART_FIXER_MIN_SIDE = 16;
@@ -2777,16 +3008,16 @@ function sameFrameGrid(first, next) {
2777
3008
  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
3009
  }
2779
3010
  async function refineAsset(options) {
2780
- const source = path5.resolve(options.source);
2781
- const output = path5.resolve(options.output);
3011
+ const source = path6.resolve(options.source);
3012
+ const output = path6.resolve(options.output);
2782
3013
  if (source === output) throw new Error("Refinement source and output must be different files.");
2783
- const record = path5.resolve(qualityRecordPath(output));
3014
+ const record = path6.resolve(qualityRecordPath(output));
2784
3015
  const palette = normalizePalette(options.palette).map((color) => color.hex);
2785
3016
  const minGridConfidence = requireConfidence(options.minGridConfidence ?? "high");
2786
3017
  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
3018
  let sourcePng;
2788
3019
  try {
2789
- sourcePng = decodePng(await readFile3(source));
3020
+ sourcePng = decodePng(await readFile4(source));
2790
3021
  } catch (error) {
2791
3022
  throw new Error(`Cannot read refinement source ${source}: ${message2(error)}`, { cause: error });
2792
3023
  }
@@ -2803,8 +3034,8 @@ async function refineAsset(options) {
2803
3034
  );
2804
3035
  }
2805
3036
  }
2806
- const temp = await mkdtemp(path5.join(tmpdir(), "pixelkiln-refine-"));
2807
- const recovered = path5.join(temp, "native.png");
3037
+ const temp = await mkdtemp(path6.join(tmpdir(), "pixelkiln-refine-"));
3038
+ const recovered = path6.join(temp, "native.png");
2808
3039
  const fixerCommand = compatibilityCommand ?? options.fixerPython ?? process.env.PIXELKILN_PIXEL_FIXER_PYTHON ?? "python3";
2809
3040
  const args = compatibilityCommand ? [...options.fixerArgsPrefix ?? [], source, "--extract", recovered] : ["-c", PYTHON_FIXER_BRIDGE, source, recovered];
2810
3041
  try {
@@ -2825,7 +3056,7 @@ async function refineAsset(options) {
2825
3056
  );
2826
3057
  }
2827
3058
  const detection = parseDetection(firstJsonObject(stdout));
2828
- const native = decodePng(await readFile3(recovered));
3059
+ const native = decodePng(await readFile4(recovered));
2829
3060
  const quantized = quantizeToPalette(native, palette);
2830
3061
  const png = encodeRgbaPng(quantized.width, quantized.height, quantized.pixels);
2831
3062
  const audit = auditRefinement(
@@ -2889,18 +3120,18 @@ async function refineFrameSet(options) {
2889
3120
  if (roles.some((role) => !role.trim()) || new Set(roles).size !== roles.length) {
2890
3121
  throw new Error("Frame-set roles must be non-empty and unique.");
2891
3122
  }
2892
- const output = path5.resolve(options.output);
2893
- const record = path5.resolve(qualityRecordPath(output));
3123
+ const output = path6.resolve(options.output);
3124
+ const record = path6.resolve(qualityRecordPath(output));
2894
3125
  const palette = normalizeRefinementPalette(options.palette);
2895
3126
  const finalOutputs = options.sources.map((source, index) => qualityFrameOutputPath(output, source.role, index));
2896
3127
  if (new Set(finalOutputs).size !== finalOutputs.length) {
2897
3128
  throw new Error("Frame-set roles resolve to duplicate output filenames.");
2898
3129
  }
2899
- const sourceSet = new Set(options.sources.map((source) => path5.resolve(source.path)));
3130
+ const sourceSet = new Set(options.sources.map((source) => path6.resolve(source.path)));
2900
3131
  if (finalOutputs.some((file) => sourceSet.has(file))) {
2901
3132
  throw new Error("Frame-set refinement sources and outputs must be different files.");
2902
3133
  }
2903
- const temp = await mkdtemp(path5.join(tmpdir(), "pixelkiln-refine-frames-"));
3134
+ const temp = await mkdtemp(path6.join(tmpdir(), "pixelkiln-refine-frames-"));
2904
3135
  try {
2905
3136
  const frames = [];
2906
3137
  const outputs = [];
@@ -2908,8 +3139,8 @@ async function refineFrameSet(options) {
2908
3139
  const sourcePaths = [];
2909
3140
  let firstOptions = null;
2910
3141
  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`);
3142
+ const source = path6.resolve(sourceEntry.path);
3143
+ const temporaryOutput = path6.join(temp, `${String(index).padStart(2, "0")}.png`);
2913
3144
  const refined = await refineAsset({
2914
3145
  source,
2915
3146
  output: temporaryOutput,
@@ -2941,7 +3172,7 @@ async function refineFrameSet(options) {
2941
3172
  });
2942
3173
  outputs.push({
2943
3174
  path: finalOutputs[index],
2944
- data: await readFile3(temporaryOutput)
3175
+ data: await readFile4(temporaryOutput)
2945
3176
  });
2946
3177
  }
2947
3178
  const first = frames[0];
@@ -2984,7 +3215,7 @@ async function refineFrameSet(options) {
2984
3215
  );
2985
3216
  return {
2986
3217
  sources: sourcePaths,
2987
- outputs: outputs.map((item) => path5.resolve(item.path)),
3218
+ outputs: outputs.map((item) => path6.resolve(item.path)),
2988
3219
  record,
2989
3220
  frames,
2990
3221
  palette,
@@ -2995,12 +3226,12 @@ async function refineFrameSet(options) {
2995
3226
  }
2996
3227
  }
2997
3228
  async function checkQualityRecord(recordPath) {
2998
- const record = path5.resolve(recordPath);
3229
+ const record = path6.resolve(recordPath);
2999
3230
  const manifest = await readArtifactBundleManifest(record);
3000
3231
  const options = parseRecordOptions(manifest, record);
3001
3232
  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));
3233
+ const sources = manifest.sources.map((source2) => path6.resolve(path6.dirname(record), source2.path));
3234
+ const outputs = manifest.outputs.map((output2) => path6.resolve(path6.dirname(record), output2.path));
3004
3235
  const source = sources[0] ?? "";
3005
3236
  const output = outputs[0] ?? "";
3006
3237
  const expectedCount = options.frameSet?.count ?? 1;
@@ -3033,7 +3264,7 @@ async function checkQualityRecord(recordPath) {
3033
3264
  async function approveQualityRecord(recordPath, approval) {
3034
3265
  const reviewer = approval.reviewer.trim();
3035
3266
  if (!reviewer) throw new Error("A human reviewer name is required.");
3036
- const record = path5.resolve(recordPath);
3267
+ const record = path6.resolve(recordPath);
3037
3268
  const manifest = await readArtifactBundleManifest(record);
3038
3269
  const options = parseRecordOptions(manifest, record);
3039
3270
  const verification = await verifyArtifactBundle(record);
@@ -3054,14 +3285,14 @@ async function approveQualityRecord(recordPath, approval) {
3054
3285
  `A refinement record must contain exactly ${expectedCount} source(s) and output(s).`
3055
3286
  );
3056
3287
  }
3057
- const root = path5.dirname(record);
3288
+ const root = path6.dirname(record);
3058
3289
  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) };
3290
+ const outputPath = path6.resolve(root, entry.path);
3291
+ return { path: outputPath, data: await readFile4(outputPath) };
3061
3292
  }));
3062
3293
  const sources = manifest.sources.map((source) => ({
3063
3294
  ...source,
3064
- path: path5.resolve(root, source.path)
3295
+ path: path6.resolve(root, source.path)
3065
3296
  }));
3066
3297
  const approved = {
3067
3298
  ...options,
@@ -3093,7 +3324,7 @@ async function qualitySource(spec, lock) {
3093
3324
  if (spec.generator === "frames") {
3094
3325
  return { items: null, reason: "frame-set quality requires ordered downloaded provider outputs" };
3095
3326
  }
3096
- const source = path6.resolve(spec.root, spec.source);
3327
+ const source = path7.resolve(spec.root, spec.source);
3097
3328
  return existsSync3(source) ? { items: [{ role: "source", path: source }] } : { items: null, reason: `declared source is missing: ${spec.source}` };
3098
3329
  }
3099
3330
  const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
@@ -3160,7 +3391,7 @@ async function inspectQualityProfile(spec, lock) {
3160
3391
  const expectedRevision = spec.quality.fixerRevision ?? PIXEL_ART_FIXER_REVISION;
3161
3392
  const expectedTransparency = spec.quality.minTransparency ?? null;
3162
3393
  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);
3394
+ 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
3395
  if (contractChanged) {
3165
3396
  return {
3166
3397
  key,
@@ -3322,43 +3553,21 @@ async function requireApprovedQualitySources(specs, lock, assetIds, outputRoles)
3322
3553
  return sources;
3323
3554
  }
3324
3555
 
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
3556
  // src/pipeline/revision.ts
3355
3557
  import { existsSync as existsSync4 } from "fs";
3356
- import path7 from "path";
3558
+ import path8 from "path";
3357
3559
  async function inspectRevisionReadiness(spec, lock) {
3560
+ if (spec.mirror) return inspectMirror(spec, lock, /* @__PURE__ */ new Set());
3358
3561
  if (spec.character?.parentSpec) return inspectCharacterParent(spec, lock, /* @__PURE__ */ new Set());
3359
3562
  if (!spec.revision) return null;
3360
3563
  return inspectRevision(spec, lock, /* @__PURE__ */ new Set());
3361
3564
  }
3565
+ async function inspectMirror(spec, lock, seen) {
3566
+ const source = spec.mirror.sourceSpec;
3567
+ const label = `mirror source ${spec.styleId}/${spec.mirror.sourceAssetId}`;
3568
+ const dependency = await inspectParent(source, lock, seen);
3569
+ return dependency.ready ? { ready: true, reason: `${label} is current` } : { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
3570
+ }
3362
3571
  async function inspectCharacterParent(spec, lock, seen) {
3363
3572
  const character = spec.character;
3364
3573
  const parentSpec = character.parentSpec;
@@ -3412,6 +3621,10 @@ async function inspectParent(spec, lock, seen) {
3412
3621
  if (seen.has(key)) return { ready: false, reason: `revision dependency cycle at ${key}` };
3413
3622
  const nextSeen = new Set(seen);
3414
3623
  nextSeen.add(key);
3624
+ if (spec.mirror) {
3625
+ const inputs = await inspectMirror(spec, lock, nextSeen);
3626
+ if (!inputs.ready) return inputs;
3627
+ }
3415
3628
  if (spec.revision) {
3416
3629
  const inputs = await inspectRevision(spec, lock, nextSeen);
3417
3630
  if (!inputs.ready) return inputs;
@@ -3428,7 +3641,7 @@ async function inspectParent(spec, lock, seen) {
3428
3641
  };
3429
3642
  }
3430
3643
  if (spec.source) {
3431
- const source = path7.resolve(spec.root, spec.source);
3644
+ const source = path8.resolve(spec.root, spec.source);
3432
3645
  return existsSync4(source) ? { ready: true, reason: "declared source is present" } : { ready: false, reason: `declared source is missing: ${spec.source}` };
3433
3646
  }
3434
3647
  const entry = lock.entries[key];
@@ -3498,7 +3711,7 @@ async function buildPlan(specs, lock, opts = {}) {
3498
3711
  state = "missing";
3499
3712
  reason = "--force";
3500
3713
  } else if (!entry && spec.source && spec.generator !== "frames") {
3501
- if (existsSync5(path8.resolve(spec.root, spec.source))) {
3714
+ if (existsSync5(path9.resolve(spec.root, spec.source))) {
3502
3715
  state = "ok";
3503
3716
  reason = `placed from ${spec.source}; not generated`;
3504
3717
  } else {
@@ -3511,11 +3724,17 @@ async function buildPlan(specs, lock, opts = {}) {
3511
3724
  reason = "file on disk, no provenance recorded";
3512
3725
  } else {
3513
3726
  state = "missing";
3514
- reason = "not in lockfile";
3727
+ reason = spec.mirror ? `not made yet; flipped from ${spec.mirror.sourceAssetId} (no generation cost)` : "not in lockfile";
3515
3728
  }
3516
3729
  } else if (entry.specHash !== spec.specHash) {
3517
3730
  state = "stale";
3518
3731
  reason = "prompt, size, or style changed";
3732
+ } else if (spec.mirror && !mirrorCurrent(entry, lock.entries[lockKey(spec.styleId, spec.mirror.sourceAssetId)])) {
3733
+ state = "stale";
3734
+ reason = `mirror source ${spec.mirror.sourceAssetId} changed; flip it again (no generation cost)`;
3735
+ } else if (spec.mirror && entry.status === "downloaded" && entry.outputs.some((_output, index) => !existsSync5(currentEntryOutputPath(entry, spec, index)))) {
3736
+ state = "stale";
3737
+ reason = "mirror output missing on disk; flip it again (no generation cost)";
3519
3738
  } else if (entry.status === "download-failed") {
3520
3739
  state = "recoverable";
3521
3740
  const force = entry.error?.includes("pass --force") ? " --force" : "";
@@ -3544,6 +3763,9 @@ async function buildPlan(specs, lock, opts = {}) {
3544
3763
  } else if (await anyOutputModified(entry, spec)) {
3545
3764
  state = "orphaned";
3546
3765
  reason = "output modified since download";
3766
+ } else if (spec.mirror && !postprocessCurrent(entry, spec)) {
3767
+ state = "stale";
3768
+ reason = "palette enforcement changed; flip the source again once it is refreshed (no generation cost)";
3547
3769
  } else if (!postprocessCurrent(entry, spec)) {
3548
3770
  state = "recoverable";
3549
3771
  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 +3777,19 @@ async function buildPlan(specs, lock, opts = {}) {
3555
3777
  items.push({ spec, key, state, reason, ...quality ? { quality } : {} });
3556
3778
  }
3557
3779
  const isActionable = (i) => i.state === "missing" || i.state === "stale" || i.state === "failed";
3558
- const actionableKeys = new Set(items.filter(isActionable).map((i) => i.key));
3780
+ const stateOf = new Map(items.map((i) => [i.key, i.state]));
3559
3781
  for (const item of items) {
3560
3782
  if (!isActionable(item)) continue;
3561
- const parent = item.spec.revision?.sourceSpec ?? item.spec.character?.parentSpec;
3783
+ const parent = item.spec.mirror?.sourceSpec ?? item.spec.revision?.sourceSpec ?? item.spec.character?.parentSpec;
3562
3784
  if (!parent) continue;
3563
3785
  const parentKey = lockKey(parent.styleId, parent.assetId);
3564
- if (actionableKeys.has(parentKey)) {
3786
+ const parentState = stateOf.get(parentKey);
3787
+ if (parentState && isActionable({ state: parentState })) {
3565
3788
  item.state = "blocked";
3566
3789
  item.reason = `parent ${parentKey} is being generated in this run; this follows in the next wave`;
3790
+ } else if (item.spec.mirror && (parentState === "recoverable" || parentState === "in-flight")) {
3791
+ item.state = "blocked";
3792
+ item.reason = `mirror source ${parentKey} is ${parentState}; this follows in the next wave`;
3567
3793
  }
3568
3794
  }
3569
3795
  const actionable = items.filter(isActionable);
@@ -3718,12 +3944,12 @@ async function confirm(question, auto) {
3718
3944
  }
3719
3945
 
3720
3946
  // src/cli/commands/account.ts
3721
- import path21 from "path";
3947
+ import path22 from "path";
3722
3948
  import { existsSync as existsSync16 } from "fs";
3723
3949
 
3724
3950
  // src/env.ts
3725
3951
  import { readFileSync, existsSync as existsSync6 } from "fs";
3726
- import path9 from "path";
3952
+ import path10 from "path";
3727
3953
  function loadEnvFiles(dir) {
3728
3954
  const loaded = [];
3729
3955
  for (const [file, vars] of envFiles(dir)) {
@@ -3746,7 +3972,7 @@ function readEnvFiles(dir) {
3746
3972
  function envFiles(dir) {
3747
3973
  const out = [];
3748
3974
  for (const name of [".env.local", ".env"]) {
3749
- const file = path9.join(dir, name);
3975
+ const file = path10.join(dir, name);
3750
3976
  if (!existsSync6(file)) continue;
3751
3977
  try {
3752
3978
  out.push([file, parseEnv(readFileSync(file, "utf8"))]);
@@ -3776,8 +4002,8 @@ function parseEnv(contents) {
3776
4002
 
3777
4003
  // src/providers/comfyui.ts
3778
4004
  import { randomUUID as randomUUID2 } from "crypto";
3779
- import { readFile as readFile4 } from "fs/promises";
3780
- import path10 from "path";
4005
+ import { readFile as readFile5 } from "fs/promises";
4006
+ import path11 from "path";
3781
4007
 
3782
4008
  // src/http.ts
3783
4009
  var MAX_RETRIES = 4;
@@ -3964,10 +4190,10 @@ var ComfyUIProvider = class _ComfyUIProvider {
3964
4190
  }
3965
4191
  async resolveOptions(value, context) {
3966
4192
  const options = parseOptions(value);
3967
- const workflowPath = path10.resolve(context.root, options.workflowFile);
4193
+ const workflowPath = path11.resolve(context.root, options.workflowFile);
3968
4194
  let raw;
3969
4195
  try {
3970
- raw = await readFile4(workflowPath, "utf8");
4196
+ raw = await readFile5(workflowPath, "utf8");
3971
4197
  } catch (err) {
3972
4198
  throw new Error(
3973
4199
  `ComfyUI workflow for style "${context.styleId}" could not be read at ${workflowPath}: ${err instanceof Error ? err.message : String(err)}`
@@ -4019,10 +4245,10 @@ var ComfyUIProvider = class _ComfyUIProvider {
4019
4245
  if (typeof item !== "string") {
4020
4246
  throw new Error(`ComfyUI ${label} must be a manifest-relative PNG or JPEG path`);
4021
4247
  }
4022
- const file = path10.resolve(context.root, item);
4248
+ const file = path11.resolve(context.root, item);
4023
4249
  let bytes;
4024
4250
  try {
4025
- bytes = await readFile4(file);
4251
+ bytes = await readFile5(file);
4026
4252
  } catch (error) {
4027
4253
  throw new Error(
4028
4254
  `ComfyUI ${label} could not be read at ${file}: ${error instanceof Error ? error.message : String(error)}`
@@ -4154,7 +4380,7 @@ var ComfyUIProvider = class _ComfyUIProvider {
4154
4380
  setBinding(workflow2, binding, input);
4155
4381
  return;
4156
4382
  }
4157
- const bytes = await readFile4(input.path);
4383
+ const bytes = await readFile5(input.path);
4158
4384
  if (sha256(bytes) !== input.sha256) {
4159
4385
  throw new Error(`ComfyUI provider input "${name}" changed before upload`);
4160
4386
  }
@@ -4221,7 +4447,7 @@ var ComfyUIProvider = class _ComfyUIProvider {
4221
4447
  if (!spec.revision.sourceSha256 || !spec.revision.sourceFormat) {
4222
4448
  throw new Error("ComfyUI revision source is not ready");
4223
4449
  }
4224
- const sourceBytes = await readFile4(spec.revision.sourceFile);
4450
+ const sourceBytes = await readFile5(spec.revision.sourceFile);
4225
4451
  if (sha256(sourceBytes) !== spec.revision.sourceSha256) {
4226
4452
  throw new Error("ComfyUI revision source changed before upload");
4227
4453
  }
@@ -4235,7 +4461,7 @@ var ComfyUIProvider = class _ComfyUIProvider {
4235
4461
  if (!spec.revision.maskSha256 || !spec.revision.maskFormat) {
4236
4462
  throw new Error("ComfyUI revision mask is not ready");
4237
4463
  }
4238
- const maskBytes = await readFile4(spec.revision.maskFile);
4464
+ const maskBytes = await readFile5(spec.revision.maskFile);
4239
4465
  if (sha256(maskBytes) !== spec.revision.maskSha256) {
4240
4466
  throw new Error("ComfyUI revision mask changed before upload");
4241
4467
  }
@@ -4709,7 +4935,7 @@ function canonical2(value) {
4709
4935
  import { mkdirSync, existsSync as existsSync7, readFileSync as readFileSync2, writeFileSync } from "fs";
4710
4936
  import { randomUUID as randomUUID3 } from "crypto";
4711
4937
  import os from "os";
4712
- import path11 from "path";
4938
+ import path12 from "path";
4713
4939
 
4714
4940
  // src/client.ts
4715
4941
  import { z as z2 } from "zod";
@@ -4839,6 +5065,17 @@ var PixelLabError = class extends ProviderError {
4839
5065
  }
4840
5066
  body;
4841
5067
  };
5068
+ function proportionsPayload(proportions) {
5069
+ if (typeof proportions === "string") return { type: "preset", name: proportions };
5070
+ return {
5071
+ type: "custom",
5072
+ ...proportions.headSize !== void 0 ? { head_size: proportions.headSize } : {},
5073
+ ...proportions.armsLength !== void 0 ? { arms_length: proportions.armsLength } : {},
5074
+ ...proportions.legsLength !== void 0 ? { legs_length: proportions.legsLength } : {},
5075
+ ...proportions.shoulderWidth !== void 0 ? { shoulder_width: proportions.shoulderWidth } : {},
5076
+ ...proportions.hipWidth !== void 0 ? { hip_width: proportions.hipWidth } : {}
5077
+ };
5078
+ }
4842
5079
  var PixelLabClient = class {
4843
5080
  constructor(apiKey, timeoutMs = 12e4) {
4844
5081
  this.apiKey = apiKey;
@@ -4848,9 +5085,9 @@ var PixelLabClient = class {
4848
5085
  apiKey;
4849
5086
  http;
4850
5087
  /** Retries transport failures, 429, and 5xx; see http.ts for the policy. */
4851
- async request(path44, init) {
5088
+ async request(path45, init) {
4852
5089
  const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
4853
- const res = await this.http(`${BASE}${path44}`, {
5090
+ const res = await this.http(`${BASE}${path45}`, {
4854
5091
  ...init,
4855
5092
  headers: {
4856
5093
  Authorization: auth,
@@ -4860,13 +5097,13 @@ var PixelLabClient = class {
4860
5097
  });
4861
5098
  const text = await res.text();
4862
5099
  if (!res.ok) {
4863
- throw new PixelLabError(`${init?.method ?? "GET"} ${path44} \u2192 ${res.status}`, res.status, text);
5100
+ throw new PixelLabError(`${init?.method ?? "GET"} ${path45} \u2192 ${res.status}`, res.status, text);
4864
5101
  }
4865
5102
  if (!text) return {};
4866
5103
  try {
4867
5104
  return JSON.parse(text);
4868
5105
  } catch {
4869
- throw new Error(`${init?.method ?? "GET"} ${path44} returned invalid JSON`);
5106
+ throw new Error(`${init?.method ?? "GET"} ${path45} returned invalid JSON`);
4870
5107
  }
4871
5108
  }
4872
5109
  async balance() {
@@ -5047,14 +5284,21 @@ var PixelLabClient = class {
5047
5284
  * A base character. Standard mode picks the 4- or 8-direction template
5048
5285
  * endpoint; v3 and pro have their own and always draw 8. Every one answers
5049
5286
  * at once with the character id and a background job.
5287
+ *
5288
+ * A `reference` is the author's own sprite: standard takes one per
5289
+ * direction and draws the rest, v3 rotates the south one, and pro
5290
+ * switches to its rotate method for it. A `styleReference` is pro's style
5291
+ * anchor; the other engines have no such input.
5050
5292
  */
5051
5293
  async createCharacter(args) {
5052
5294
  const imageSize = { width: args.size, height: args.size };
5053
5295
  const palette = args.paletteSwatchBase64 ? { color_image: { type: "base64", base64: args.paletteSwatchBase64, format: "png" }, force_colors: true } : {};
5054
- let path44;
5296
+ const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
5297
+ const south = args.reference?.south;
5298
+ let path45;
5055
5299
  let body;
5056
5300
  if (args.mode === "v3") {
5057
- path44 = "/create-character-v3";
5301
+ path45 = "/create-character-v3";
5058
5302
  body = {
5059
5303
  description: args.description,
5060
5304
  image_size: imageSize,
@@ -5063,20 +5307,23 @@ var PixelLabClient = class {
5063
5307
  ...args.view ? { view: args.view } : {},
5064
5308
  ...args.outline ? { outline: args.outline } : {},
5065
5309
  ...args.detail ? { detail: args.detail } : {},
5066
- ...args.seed != null ? { seed: args.seed } : {}
5310
+ ...args.seed != null ? { seed: args.seed } : {},
5311
+ ...south ? { reference_image: encode(south) } : {}
5067
5312
  };
5068
5313
  } else if (args.mode === "pro") {
5069
- path44 = "/create-character-pro";
5314
+ path45 = "/create-character-pro";
5070
5315
  body = {
5071
5316
  description: args.description,
5072
5317
  image_size: imageSize,
5073
5318
  template_id: args.template,
5074
5319
  no_background: args.noBackground ?? true,
5320
+ method: south ? "rotate_character" : "create_with_style",
5075
5321
  ...args.view ? { view: args.view } : {},
5076
- ...args.seed != null ? { seed: args.seed } : {}
5322
+ ...args.seed != null ? { seed: args.seed } : {},
5323
+ ...south ? { reference_image: encode(south) } : args.styleReference ? { reference_image: encode(args.styleReference) } : {}
5077
5324
  };
5078
5325
  } else {
5079
- path44 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
5326
+ path45 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
5080
5327
  body = {
5081
5328
  description: args.description,
5082
5329
  image_size: imageSize,
@@ -5086,11 +5333,15 @@ var PixelLabClient = class {
5086
5333
  ...args.shading ? { shading: args.shading } : {},
5087
5334
  ...args.detail ? { detail: args.detail } : {},
5088
5335
  ...args.seed != null ? { seed: args.seed } : {},
5336
+ ...args.proportions !== void 0 ? { proportions: proportionsPayload(args.proportions) } : {},
5337
+ ...args.textGuidanceScale !== void 0 ? { text_guidance_scale: args.textGuidanceScale } : {},
5338
+ ...args.isometric !== void 0 ? { isometric: args.isometric } : {},
5339
+ ...args.reference && Object.keys(args.reference).length ? { directions: Object.fromEntries(Object.entries(args.reference).flatMap(([direction, image]) => image ? [[direction, encode(image)]] : [])) } : {},
5089
5340
  ...palette
5090
5341
  };
5091
5342
  }
5092
- const raw = await this.request(path44, { method: "POST", body: JSON.stringify(body) });
5093
- return validateResponse(CharacterSubmitSchema, raw, path44.slice(1));
5343
+ const raw = await this.request(path45, { method: "POST", body: JSON.stringify(body) });
5344
+ return validateResponse(CharacterSubmitSchema, raw, path45.slice(1));
5094
5345
  }
5095
5346
  /** A text edit of an existing character, applied to every direction and stored as a sibling. */
5096
5347
  async createCharacterState(args) {
@@ -5125,6 +5376,8 @@ var PixelLabClient = class {
5125
5376
  ...args.actionDescription ? { action_description: args.actionDescription } : {},
5126
5377
  ...args.mode === "v3" && args.frameCount ? { frame_count: args.frameCount } : {},
5127
5378
  ...args.mode === "v3" && args.keepFirstFrame === false ? { keep_first_frame: false } : {},
5379
+ ...args.mode === "template" && args.textGuidanceScale !== void 0 ? { text_guidance_scale: args.textGuidanceScale } : {},
5380
+ ...args.isometric !== void 0 ? { isometric: args.isometric } : {},
5128
5381
  ...args.seed != null ? { seed: args.seed } : {}
5129
5382
  })
5130
5383
  });
@@ -5278,7 +5531,7 @@ var PixelLabProvider = class _PixelLabProvider {
5278
5531
  * filename, polling is an existence check, and downloading is a file read.
5279
5532
  */
5280
5533
  static cacheDir() {
5281
- const dir = path11.join(os.tmpdir(), "pixelkiln-pixflux");
5534
+ const dir = path12.join(os.tmpdir(), "pixelkiln-pixflux");
5282
5535
  mkdirSync(dir, { recursive: true });
5283
5536
  return dir;
5284
5537
  }
@@ -5321,8 +5574,8 @@ var PixelLabProvider = class _PixelLabProvider {
5321
5574
  "high detail"
5322
5575
  ]);
5323
5576
  }
5324
- if (spec.generator === "character") this.validateCharacter(spec);
5325
- if ((spec.generator === "map" || spec.generator === "pixflux" || spec.generator === "character") && styleImages.length) {
5577
+ if (spec.generator === "character") this.validateCharacter(spec, styleImages);
5578
+ if ((spec.generator === "map" || spec.generator === "pixflux") && styleImages.length) {
5326
5579
  throw new Error(`PixelLab ${spec.generator} does not support style images`);
5327
5580
  }
5328
5581
  for (const image of styleImages) {
@@ -5338,9 +5591,61 @@ var PixelLabProvider = class _PixelLabProvider {
5338
5591
  );
5339
5592
  }
5340
5593
  }
5341
- validateCharacter(spec) {
5594
+ validateCharacter(spec, styleImages = []) {
5342
5595
  const character = spec.character;
5343
5596
  if (!character) throw new Error(`${spec.styleId}/${spec.assetId} has no character shape`);
5597
+ const label = `${spec.styleId}/${spec.assetId}`;
5598
+ if (character.kind === "base" && character.proportions !== void 0) {
5599
+ if (character.mode !== "standard") {
5600
+ throw new Error(`${label}: proportions apply to standard bases; the ${character.mode} engine has none to set`);
5601
+ }
5602
+ if (character.template !== "mannequin") {
5603
+ throw new Error(`${label}: proportions apply to the mannequin template; ${character.template} is a quadruped`);
5604
+ }
5605
+ }
5606
+ if (character.reference) {
5607
+ const given = Object.keys(character.reference);
5608
+ if (!character.reference.south) throw new Error(`${label}: a reference needs a south-facing sprite`);
5609
+ if (character.mode !== "standard") {
5610
+ if (given.length > 1) throw new Error(`${label}: PixelLab ${character.mode} rotates one south-facing reference; drop the other directions`);
5611
+ const limit = character.mode === "v3" ? 256 : 168;
5612
+ const south = character.reference.south;
5613
+ if (south.width > limit || south.height > limit) {
5614
+ throw new Error(`${label}: reference is ${south.width}x${south.height}; PixelLab ${character.mode} takes up to ${limit}px`);
5615
+ }
5616
+ } else {
5617
+ const allowed = character.directions === 4 ? CHARACTER_DIRECTIONS_4 : CHARACTER_DIRECTIONS_8;
5618
+ for (const direction of given) {
5619
+ if (!allowed.includes(direction)) {
5620
+ throw new Error(`${label}: reference "${direction}" is not one of this character's ${character.directions} directions`);
5621
+ }
5622
+ const image = character.reference[direction];
5623
+ if (image.width !== spec.width || image.height !== spec.height) {
5624
+ throw new Error(
5625
+ `${label}: reference (${direction}) is ${image.width}x${image.height}; standard mode wants each image at the style's size, ${spec.width}x${spec.height}`
5626
+ );
5627
+ }
5628
+ }
5629
+ if (character.template !== "mannequin" && !character.reference.east) {
5630
+ throw new Error(`${label}: a ${character.template} reference needs south and east images`);
5631
+ }
5632
+ }
5633
+ }
5634
+ if (styleImages.length) {
5635
+ if (character.mode !== "pro") {
5636
+ throw new Error(
5637
+ `PixelLab ${character.mode} characters take no style images; give a base its own sprite with \`reference\`, or use mode pro for a style anchor`
5638
+ );
5639
+ }
5640
+ if (styleImages.length > 1) throw new Error("PixelLab pro characters take one style image");
5641
+ if (character.kind === "base" && character.reference) {
5642
+ throw new Error(`${label}: a pro base rotating its own reference takes no style image`);
5643
+ }
5644
+ const image = styleImages[0];
5645
+ if (image.width > 168 || image.height > 168) {
5646
+ throw new Error(`PixelLab pro character style image is ${image.width}x${image.height}; the limit is 168px`);
5647
+ }
5648
+ }
5344
5649
  const maxSize = character.mode === "v3" ? 256 : 128;
5345
5650
  if (spec.width < 16 || spec.height < 16 || spec.width > maxSize || spec.height > maxSize) {
5346
5651
  throw new Error(`PixelLab ${character.mode} characters must be between 16 and ${maxSize} pixels`);
@@ -5376,10 +5681,11 @@ var PixelLabProvider = class _PixelLabProvider {
5376
5681
  * asset so a later run can find it, and clears its own earlier take for
5377
5682
  * that direction first, since PixelLab skips a direction that exists.
5378
5683
  */
5379
- async submitCharacter(spec, context) {
5684
+ async submitCharacter(spec, styleImages, context) {
5380
5685
  const character = spec.character;
5381
5686
  if (character.kind === "base") {
5382
5687
  const swatch = spec.palette.length && character.mode === "standard" ? paletteSwatch(spec.palette).toString("base64") : void 0;
5688
+ const styleImage = styleImages[0];
5383
5689
  const res2 = await this.client.createCharacter({
5384
5690
  mode: character.mode,
5385
5691
  description: spec.prompt,
@@ -5392,7 +5698,12 @@ var PixelLabProvider = class _PixelLabProvider {
5392
5698
  detail: spec.detail,
5393
5699
  seed: spec.seed,
5394
5700
  noBackground: spec.noBackground,
5395
- paletteSwatchBase64: swatch
5701
+ paletteSwatchBase64: swatch,
5702
+ proportions: character.proportions,
5703
+ textGuidanceScale: character.textGuidanceScale,
5704
+ isometric: character.isometric,
5705
+ reference: character.reference ? readReference(spec, character.reference) : void 0,
5706
+ styleReference: styleImage ? { base64: styleImage.base64, format: styleImage.format } : void 0
5396
5707
  });
5397
5708
  return { jobId: res2.character_id, metadata: { character: { kind: "base", characterId: res2.character_id, mode: character.mode, directions: character.directions, backgroundJobId: res2.background_job_id } } };
5398
5709
  }
@@ -5434,7 +5745,9 @@ var PixelLabProvider = class _PixelLabProvider {
5434
5745
  frameCount: animation.frames,
5435
5746
  keepFirstFrame: animation.keepFirstFrame,
5436
5747
  directions: [animation.direction],
5437
- seed: spec.seed
5748
+ seed: spec.seed,
5749
+ textGuidanceScale: character.textGuidanceScale,
5750
+ isometric: character.isometric
5438
5751
  });
5439
5752
  const job = { characterId: parentId, name, direction: animation.direction, jobIds: res.background_job_ids };
5440
5753
  return {
@@ -5524,7 +5837,7 @@ var PixelLabProvider = class _PixelLabProvider {
5524
5837
  }
5525
5838
  async submit(spec, styleImages, context) {
5526
5839
  this.validate(spec, styleImages);
5527
- if (spec.generator === "character") return this.submitCharacter(spec, context);
5840
+ if (spec.generator === "character") return this.submitCharacter(spec, styleImages, context);
5528
5841
  if (spec.generator === "pixflux") {
5529
5842
  const swatch = spec.palette.length ? paletteSwatch(spec.palette).toString("base64") : void 0;
5530
5843
  const { png } = await this.client.createImagePixflux({
@@ -5536,7 +5849,7 @@ var PixelLabProvider = class _PixelLabProvider {
5536
5849
  seed: spec.seed
5537
5850
  });
5538
5851
  const jobId = randomUUID3();
5539
- writeFileSync(path11.join(_PixelLabProvider.cacheDir(), `${jobId}.png`), png);
5852
+ writeFileSync(path12.join(_PixelLabProvider.cacheDir(), `${jobId}.png`), png);
5540
5853
  return { jobId };
5541
5854
  }
5542
5855
  if (spec.generator === "tiles") {
@@ -5577,7 +5890,7 @@ var PixelLabProvider = class _PixelLabProvider {
5577
5890
  }
5578
5891
  async poll(jobId, generator, context) {
5579
5892
  if (generator === "pixflux") {
5580
- const file = path11.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
5893
+ const file = path12.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
5581
5894
  if (existsSync7(file)) {
5582
5895
  const sourceUrl = `file://${file}`;
5583
5896
  return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }] };
@@ -5791,6 +6104,17 @@ function firstUrl(urls) {
5791
6104
  if (!urls) return null;
5792
6105
  return Object.values(urls).find((u) => typeof u === "string") ?? null;
5793
6106
  }
6107
+ function readReference(spec, reference) {
6108
+ const images = {};
6109
+ for (const [direction, image] of Object.entries(reference)) {
6110
+ const bytes = readFileSync2(image.path);
6111
+ if (sha256(bytes) !== image.sha256) {
6112
+ throw new Error(`${spec.styleId}/${spec.assetId}: reference (${direction}) changed after the manifest was resolved: ${image.path}`);
6113
+ }
6114
+ images[direction] = { base64: bytes.toString("base64"), format: image.format };
6115
+ }
6116
+ return images;
6117
+ }
5794
6118
  function rotationSources(character) {
5795
6119
  const order = character.directions === 4 ? CHARACTER_DIRECTIONS_4 : CHARACTER_DIRECTIONS_8;
5796
6120
  const sources = [];
@@ -5861,9 +6185,9 @@ var RetroDiffusionClient = class {
5861
6185
  if (!response.ok) throw new ProviderError("retrodiffusion", `Retro Diffusion download failed (${response.status})`, { status: response.status });
5862
6186
  return Buffer.from(await response.arrayBuffer());
5863
6187
  }
5864
- async call(path44, init = {}) {
6188
+ async call(path45, init = {}) {
5865
6189
  if (!this.token) throw new Error("RD_API_KEY is not set");
5866
- const response = await this.request(`${this.baseUrl}${path44}`, {
6190
+ const response = await this.request(`${this.baseUrl}${path45}`, {
5867
6191
  ...init,
5868
6192
  headers: {
5869
6193
  "Content-Type": "application/json",
@@ -6720,13 +7044,13 @@ registerProvider({
6720
7044
  });
6721
7045
 
6722
7046
  // src/manifest.ts
6723
- import { readFile as readFile5 } from "fs/promises";
7047
+ import { readFile as readFile6 } from "fs/promises";
6724
7048
  import { existsSync as existsSync8 } from "fs";
6725
- import path12 from "path";
7049
+ import path13 from "path";
6726
7050
  async function loadManifest(manifestPath) {
6727
- const abs = path12.resolve(manifestPath);
7051
+ const abs = path13.resolve(manifestPath);
6728
7052
  if (!existsSync8(abs)) throw new ProjectError(`No manifest at ${abs}`);
6729
- const input = ManifestInputSchema.safeParse(JSON.parse(await readFile5(abs, "utf8")));
7053
+ const input = ManifestInputSchema.safeParse(JSON.parse(await readFile6(abs, "utf8")));
6730
7054
  if (!input.success) {
6731
7055
  const issues = formatManifestIssues(input.error.issues);
6732
7056
  throw new ProjectError(`Manifest at ${abs} is invalid:
@@ -6811,7 +7135,7 @@ ${issues}`);
6811
7135
  throw new ProjectError(`Manifest at ${abs} is invalid:
6812
7136
  ${unknownReferences.map((i) => ` ${i}`).join("\n")}`);
6813
7137
  }
6814
- return { manifest: parsed.data, root: path12.dirname(abs), path: abs };
7138
+ return { manifest: parsed.data, root: path13.dirname(abs), path: abs };
6815
7139
  }
6816
7140
  function parentAssetId(asset) {
6817
7141
  return asset?.revision?.from ?? asset?.state?.of ?? asset?.animation?.of;
@@ -6910,16 +7234,16 @@ async function resolveSpecs(loaded, filter) {
6910
7234
  }
6911
7235
  }
6912
7236
  const styleImageCache = /* @__PURE__ */ new Map();
6913
- async function loadStyleImage(rel) {
6914
- const abs = path12.resolve(root, rel);
7237
+ async function loadStyleImage(rel, what = "Style image") {
7238
+ const abs = path13.resolve(root, rel);
6915
7239
  let hit = styleImageCache.get(abs);
6916
7240
  if (!hit) {
6917
- if (!existsSync8(abs)) throw new Error(`Style image not found: ${abs}`);
6918
- const buf = await readFile5(abs);
7241
+ if (!existsSync8(abs)) throw new Error(`${what} not found: ${abs}`);
7242
+ const buf = await readFile6(abs);
6919
7243
  const metadata = imageMetadata(buf);
6920
- if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${abs}`);
7244
+ if (!metadata) throw new Error(`${what} is not a readable PNG or JPEG: ${abs}`);
6921
7245
  if (metadata.width < 1 || metadata.height < 1) {
6922
- throw new Error(`Style image has invalid dimensions: ${abs}`);
7246
+ throw new Error(`${what} has invalid dimensions: ${abs}`);
6923
7247
  }
6924
7248
  hit = { base64: buf.toString("base64"), hash: sha256(buf), ...metadata };
6925
7249
  styleImageCache.set(abs, hit);
@@ -6941,7 +7265,7 @@ async function resolveSpecs(loaded, filter) {
6941
7265
  styleImageDimensions.push(loadedImage);
6942
7266
  }
6943
7267
  const resolvedImages = style.styleImages.map((image) => {
6944
- const hit = styleImageCache.get(path12.resolve(root, image.path));
7268
+ const hit = styleImageCache.get(path13.resolve(root, image.path));
6945
7269
  return { base64: hit.base64, width: hit.width, height: hit.height, format: hit.format };
6946
7270
  });
6947
7271
  const styleSpecs = /* @__PURE__ */ new Map();
@@ -6994,11 +7318,11 @@ async function resolveSpecs(loaded, filter) {
6994
7318
  );
6995
7319
  }
6996
7320
  const characterKind = asset.animation ? "animation" : asset.state ? "state" : "base";
6997
- const subject = asset.promptByStyle[styleId] ?? asset.prompt;
7321
+ const subject = asset.promptByStyle[styleId] ?? asset.prompt ?? "";
6998
7322
  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;
7323
+ const relFile = asset.file ?? path13.join(asset.category ?? "", `${assetId}.png`);
7324
+ const outFile = path13.resolve(root, style.outDir, relFile);
7325
+ const qualityOutFile = style.quality ? path13.resolve(root, style.quality.outDir, pngPath(relFile)) : void 0;
7002
7326
  const tileSize = generator === "tiles" ? size : style.tileSize ?? 32;
7003
7327
  const tileVariations = tileFeatureOutputCount(generator === "tiles" ? style.tileFeature : void 0) ?? tileVariationCount(countNumberedDescriptions(prompt));
7004
7328
  const base = {
@@ -7026,7 +7350,7 @@ async function resolveSpecs(loaded, filter) {
7026
7350
  tileView: generator === "tiles" ? style.tileView : void 0,
7027
7351
  tileFeature: generator === "tiles" ? style.tileFeature : void 0,
7028
7352
  outlineMode: generator === "tiles" ? style.outlineMode : void 0,
7029
- ...generator === "character" ? { character: resolveCharacterShape(asset, style, characterKind) } : {},
7353
+ ...generator === "character" ? { character: await resolveCharacterShape(asset, style, characterKind, { root, load: loadStyleImage }) } : {},
7030
7354
  cost: generator === "tiles" ? tilesCost(tileSize, tileVariations) : generationCost(width, height, generator),
7031
7355
  costUnit: "generations",
7032
7356
  candidates: generator === "tiles" ? tileVariations : generator === "1dir" ? candidateCount(size) : 1
@@ -7051,7 +7375,7 @@ async function resolveSpecs(loaded, filter) {
7051
7375
  minGridConfidence: style.quality.minGridConfidence,
7052
7376
  ...style.quality.minTransparency == null ? {} : { minTransparency: style.quality.minTransparency },
7053
7377
  ...style.quality.fixerRevision ? { fixerRevision: style.quality.fixerRevision } : {},
7054
- ...style.quality.fixerPython ? { fixerPython: path12.resolve(root, style.quality.fixerPython) } : {},
7378
+ ...style.quality.fixerPython ? { fixerPython: path13.resolve(root, style.quality.fixerPython) } : {},
7055
7379
  ...style.quality.fps == null ? {} : { fps: style.quality.fps }
7056
7380
  }
7057
7381
  } : {},
@@ -7069,11 +7393,42 @@ async function resolveSpecs(loaded, filter) {
7069
7393
  const resolved = styleSpecs.get(assetId);
7070
7394
  if (!resolved) {
7071
7395
  throw new Error(
7072
- `Asset "${assetId}" is not available in style "${styleId}"; revision parents must participate in the same style.`
7396
+ `Asset "${assetId}" is not available in style "${styleId}"; revision parents, character parents, and mirror sources must be in the same style.`
7073
7397
  );
7074
7398
  }
7075
7399
  if (finalized.has(assetId)) return resolved;
7076
7400
  const asset = manifest.assets[assetId];
7401
+ if (asset.mirror) {
7402
+ if (asset.mirror === assetId) throw new Error(`assets.${assetId}: an asset cannot mirror itself`);
7403
+ const sourceSpec = await finalize(asset.mirror);
7404
+ if (sourceSpec.generator === "tiles") {
7405
+ throw new Error(`assets.${assetId}: a tile set cannot be mirrored; its edges carry meaning`);
7406
+ }
7407
+ if (sourceSpec.character?.kind === "animation") {
7408
+ const facing = sourceSpec.character.animation.direction;
7409
+ const mirrored = MIRRORED_DIRECTION[facing];
7410
+ if (mirrored === facing) {
7411
+ throw new Error(
7412
+ `assets.${assetId}: mirroring ${asset.mirror} gives another ${facing}-facing loop; mirror a loop facing east, west, or a diagonal`
7413
+ );
7414
+ }
7415
+ resolved.character = { ...sourceSpec.character, animation: { ...sourceSpec.character.animation, direction: mirrored } };
7416
+ } else if (sourceSpec.character) {
7417
+ resolved.character = { ...sourceSpec.character };
7418
+ }
7419
+ resolved.mirror = { sourceAssetId: asset.mirror, sourceSpec };
7420
+ resolved.generator = sourceSpec.generator;
7421
+ resolved.width = sourceSpec.width;
7422
+ resolved.height = sourceSpec.height;
7423
+ resolved.size = sourceSpec.size;
7424
+ resolved.prompt = "";
7425
+ resolved.specHash = specHash(resolved, styleImageHashes, providerOptionIdentity, providerInputIdentities.get(assetId));
7426
+ resolved.cost = 0;
7427
+ resolved.costUnit = sourceSpec.costUnit;
7428
+ resolved.candidates = 1;
7429
+ finalized.add(assetId);
7430
+ return resolved;
7431
+ }
7077
7432
  const characterParent = asset.state?.of ?? asset.animation?.of;
7078
7433
  if (resolved.character && characterParent) {
7079
7434
  const parentSpec = await finalize(characterParent);
@@ -7097,9 +7452,9 @@ async function resolveSpecs(loaded, filter) {
7097
7452
  );
7098
7453
  }
7099
7454
  const sourceSpec = await finalize(asset.revision.from);
7100
- const sourceFile = sourceSpec.quality?.outFile ?? (sourceSpec.source ? path12.resolve(root, sourceSpec.source) : sourceSpec.outFile);
7455
+ const sourceFile = sourceSpec.quality?.outFile ?? (sourceSpec.source ? path13.resolve(root, sourceSpec.source) : sourceSpec.outFile);
7101
7456
  const sourceImage = await optionalRevisionImage(sourceFile, "revision source");
7102
- const maskFile = asset.revision.mask ? path12.resolve(root, asset.revision.mask) : void 0;
7457
+ const maskFile = asset.revision.mask ? path13.resolve(root, asset.revision.mask) : void 0;
7103
7458
  const maskImage = maskFile ? await optionalRevisionImage(maskFile, "revision mask", "png") : null;
7104
7459
  if (sourceImage && maskImage && (sourceImage.width !== maskImage.width || sourceImage.height !== maskImage.height)) {
7105
7460
  throw new Error(
@@ -7180,12 +7535,12 @@ async function resolveSpecs(loaded, filter) {
7180
7535
  return specs;
7181
7536
  }
7182
7537
  function pngPath(file) {
7183
- const extension = path12.extname(file);
7538
+ const extension = path13.extname(file);
7184
7539
  return extension ? `${file.slice(0, -extension.length)}.png` : `${file}.png`;
7185
7540
  }
7186
7541
  async function optionalRevisionImage(file, label, requiredFormat) {
7187
7542
  if (!existsSync8(file)) return null;
7188
- const bytes = await readFile5(file);
7543
+ const bytes = await readFile6(file);
7189
7544
  const metadata = imageMetadata(bytes);
7190
7545
  if (!metadata) throw new Error(`${label} is not a readable PNG or JPEG: ${file}`);
7191
7546
  if (requiredFormat && metadata.format !== requiredFormat) {
@@ -7198,8 +7553,8 @@ async function resolveStyleImages(loaded, styleId) {
7198
7553
  if (!style) throw new Error(`Unknown style "${styleId}"`);
7199
7554
  const out = [];
7200
7555
  for (const img of style.styleImages) {
7201
- const file = path12.resolve(loaded.root, img.path);
7202
- const buf = await readFile5(file);
7556
+ const file = path13.resolve(loaded.root, img.path);
7557
+ const buf = await readFile6(file);
7203
7558
  const metadata = imageMetadata(buf);
7204
7559
  if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${file}`);
7205
7560
  if (metadata.width < 1 || metadata.height < 1) {
@@ -7237,15 +7592,33 @@ function imageMetadata(buf) {
7237
7592
  }
7238
7593
  return null;
7239
7594
  }
7240
- function resolveCharacterShape(asset, style, kind) {
7595
+ async function resolveCharacterShape(asset, style, kind, files) {
7241
7596
  const mode2 = style.mode ?? "standard";
7242
7597
  const directions = mode2 === "standard" ? style.directions ?? 8 : 8;
7598
+ const proportions = asset.proportions ?? style.proportions;
7243
7599
  const shape = {
7244
7600
  kind,
7245
7601
  mode: mode2,
7246
7602
  directions,
7247
- template: style.template ?? "mannequin"
7603
+ template: style.template ?? "mannequin",
7604
+ ...proportions !== void 0 ? { proportions } : {},
7605
+ ...style.textGuidanceScale !== void 0 ? { textGuidanceScale: style.textGuidanceScale } : {},
7606
+ ...style.isometric !== void 0 ? { isometric: style.isometric } : {}
7248
7607
  };
7608
+ if (asset.reference) {
7609
+ const byDirection = typeof asset.reference === "string" ? { south: asset.reference } : asset.reference;
7610
+ shape.reference = {};
7611
+ for (const [direction, rel] of Object.entries(byDirection)) {
7612
+ const image = await files.load(rel, `Reference sprite (${direction})`);
7613
+ shape.reference[direction] = {
7614
+ path: path13.resolve(files.root, rel),
7615
+ sha256: image.hash,
7616
+ width: image.width,
7617
+ height: image.height,
7618
+ format: image.format
7619
+ };
7620
+ }
7621
+ }
7249
7622
  if (asset.state) {
7250
7623
  shape.state = {
7251
7624
  paletteFromReference: asset.state.paletteFromReference,
@@ -7268,8 +7641,8 @@ function resolveCharacterShape(asset, style, kind) {
7268
7641
 
7269
7642
  // src/pipeline/fetch.ts
7270
7643
  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";
7644
+ import { mkdir as mkdir4, readFile as readFile7, rename as rename4, rm as rm4, writeFile as writeFile4 } from "fs/promises";
7645
+ import path14 from "path";
7273
7646
 
7274
7647
  // src/source-url.ts
7275
7648
  var SENSITIVE_QUERY_KEYS = /* @__PURE__ */ new Set([
@@ -7315,9 +7688,10 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
7315
7688
  const result = { downloaded: 0, skipped: 0, failed: 0, ...opts.refresh ? { unchanged: 0 } : {} };
7316
7689
  const specByKey = new Map(specs.map((s) => [lockKey(s.styleId, s.assetId), s]));
7317
7690
  normalizeLockOutputPaths(lock, specs);
7318
- const cacheDir = opts.cacheDir === false ? null : path13.resolve(opts.cacheDir ?? path13.join(path13.dirname(lockPath), ".pixelkiln", "cache"));
7691
+ const cacheDir = opts.cacheDir === false ? null : path14.resolve(opts.cacheDir ?? path14.join(path14.dirname(lockPath), ".pixelkiln", "cache"));
7319
7692
  const pending = Object.entries(lock.entries).filter(([key, e]) => {
7320
7693
  if (e.provider !== provider.id || !specByKey.has(key)) return false;
7694
+ if (specByKey.get(key)?.mirror) return false;
7321
7695
  if (e.status === "selected" || e.status === "download-failed") return true;
7322
7696
  if (e.status !== "downloaded") return false;
7323
7697
  if (opts.refresh) return e.outputs.length > 0 && Boolean(e.sourceUrls?.length || e.sourceUrl);
@@ -7416,7 +7790,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
7416
7790
  if (!source.url) throw new Error(`no durable source URL to refresh ${source.role ?? "asset"} from`);
7417
7791
  fresh = await provider.download(source.url);
7418
7792
  if (recorded && sha256(fresh) === recordedRaw && !reapply) {
7419
- log2(` current ${path13.relative(process.cwd(), target)}`);
7793
+ log2(` current ${path14.relative(process.cwd(), target)}`);
7420
7794
  outputs.push({ ...recorded, path: portableOutputPath(target, spec.root) });
7421
7795
  continue;
7422
7796
  }
@@ -7430,7 +7804,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
7430
7804
  if (cacheDir) {
7431
7805
  await cacheMedia(
7432
7806
  cacheDir,
7433
- await readFile6(target),
7807
+ await readFile7(target),
7434
7808
  recorded.mediaType ?? MediaType.PNG,
7435
7809
  recorded.sha256
7436
7810
  );
@@ -7439,10 +7813,10 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
7439
7813
  continue;
7440
7814
  }
7441
7815
  if (reapply && !fresh) {
7442
- if (!recorded.raw) raw = await readFile6(target);
7443
- log2(` reapply ${path13.relative(process.cwd(), target)}`);
7816
+ if (!recorded.raw) raw = await readFile7(target);
7817
+ log2(` reapply ${path14.relative(process.cwd(), target)}`);
7444
7818
  } else {
7445
- log2(` changed ${path13.relative(process.cwd(), target)} (upstream)`);
7819
+ log2(` changed ${path14.relative(process.cwd(), target)} (upstream)`);
7446
7820
  }
7447
7821
  } else {
7448
7822
  const superseded = (entry.supersededOutputs ?? []).find(
@@ -7461,7 +7835,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
7461
7835
  }
7462
7836
  }
7463
7837
  log2(
7464
- ` replace ${path13.relative(process.cwd(), target)}` + (opts.force ? " (--force)" : " (previous tracked generation)")
7838
+ ` replace ${path14.relative(process.cwd(), target)}` + (opts.force ? " (--force)" : " (previous tracked generation)")
7465
7839
  );
7466
7840
  }
7467
7841
  }
@@ -7470,7 +7844,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
7470
7844
  } else if (!raw && recorded && cacheDir) {
7471
7845
  if (!reapply) final = await readCachedMedia(cacheDir, recorded.sha256, expectedMediaType);
7472
7846
  if (!final && recordedRaw) raw = await readCachedMedia(cacheDir, recordedRaw, expectedMediaType);
7473
- if (final || raw) log2(` cached ${path13.relative(process.cwd(), target)}`);
7847
+ if (final || raw) log2(` cached ${path14.relative(process.cwd(), target)}`);
7474
7848
  }
7475
7849
  if (!final && !raw) {
7476
7850
  if (!source.url) {
@@ -7497,13 +7871,13 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
7497
7871
  buf = processed.bytes;
7498
7872
  if (processed.changed) {
7499
7873
  rawHash = sha256(raw);
7500
- log2(` snapped ${path13.relative(process.cwd(), target)} to ${wanted.palette.colors.length} colours`);
7874
+ log2(` snapped ${path14.relative(process.cwd(), target)} to ${wanted.palette.colors.length} colours`);
7501
7875
  }
7502
7876
  }
7503
7877
  if (cacheDir) await cacheMedia(cacheDir, buf, mediaType);
7504
- await mkdir3(path13.dirname(target), { recursive: true });
7878
+ await mkdir4(path14.dirname(target), { recursive: true });
7505
7879
  const tmp = `${target}.pixelkiln.tmp`;
7506
- await writeFile3(tmp, buf);
7880
+ await writeFile4(tmp, buf);
7507
7881
  outputs.push({
7508
7882
  path: portableOutputPath(target, spec.root),
7509
7883
  sha256: sha256(buf),
@@ -7513,9 +7887,9 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
7513
7887
  });
7514
7888
  upsert(lock, key, { outputs: mergeOutputs(entry.outputs, outputs) });
7515
7889
  await saveLock(lockPath, lock);
7516
- await rename3(tmp, target);
7890
+ await rename4(tmp, target);
7517
7891
  wrote = true;
7518
- log2(` wrote ${path13.relative(process.cwd(), target)}`);
7892
+ log2(` wrote ${path14.relative(process.cwd(), target)}`);
7519
7893
  }
7520
7894
  if (opts.refresh && !wrote) {
7521
7895
  if (rotated) {
@@ -7559,10 +7933,10 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
7559
7933
  return result;
7560
7934
  }
7561
7935
  async function readCachedMedia(cacheDir, hash, mediaType) {
7562
- const file = path13.join(cacheDir, cacheFileName(hash, mediaType));
7936
+ const file = path14.join(cacheDir, cacheFileName(hash, mediaType));
7563
7937
  if (!existsSync9(file)) return null;
7564
7938
  try {
7565
- const buf = await readFile6(file);
7939
+ const buf = await readFile7(file);
7566
7940
  if (sha256(buf) !== hash) return null;
7567
7941
  validateMedia(buf, mediaType);
7568
7942
  return buf;
@@ -7573,13 +7947,13 @@ async function readCachedMedia(cacheDir, hash, mediaType) {
7573
7947
  async function cacheMedia(cacheDir, buf, mediaType, knownHash) {
7574
7948
  const hash = knownHash ?? sha256(buf);
7575
7949
  validateMedia(buf, mediaType);
7576
- const file = path13.join(cacheDir, cacheFileName(hash, mediaType));
7950
+ const file = path14.join(cacheDir, cacheFileName(hash, mediaType));
7577
7951
  if (await readCachedMedia(cacheDir, hash, mediaType)) return;
7578
- await mkdir3(cacheDir, { recursive: true });
7952
+ await mkdir4(cacheDir, { recursive: true });
7579
7953
  const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
7580
- await writeFile3(tmp, buf);
7954
+ await writeFile4(tmp, buf);
7581
7955
  try {
7582
- await rename3(tmp, file);
7956
+ await rename4(tmp, file);
7583
7957
  } catch (err) {
7584
7958
  if (!existsSync9(file)) throw err;
7585
7959
  } finally {
@@ -7615,11 +7989,11 @@ async function pushTags(provider, specs, lock, opts = {}) {
7615
7989
 
7616
7990
  // src/pipeline/adopt.ts
7617
7991
  import { existsSync as existsSync11 } from "fs";
7618
- import { readFile as readFile8, writeFile as writeFile5 } from "fs/promises";
7619
- import path14 from "path";
7992
+ import { readFile as readFile9, writeFile as writeFile6 } from "fs/promises";
7993
+ import path15 from "path";
7620
7994
 
7621
7995
  // src/cache.ts
7622
- import { mkdir as mkdir4, readFile as readFile7, rename as rename4, rm as rm5, writeFile as writeFile4 } from "fs/promises";
7996
+ import { mkdir as mkdir5, readFile as readFile8, rename as rename5, rm as rm5, writeFile as writeFile5 } from "fs/promises";
7623
7997
  import { existsSync as existsSync10 } from "fs";
7624
7998
  import pathModule from "path";
7625
7999
  import { z as z3 } from "zod";
@@ -7635,10 +8009,10 @@ function isSha256Hash(value) {
7635
8009
  function parseCache(value) {
7636
8010
  return HashCacheSchema.parse(value);
7637
8011
  }
7638
- async function loadCache(path44) {
7639
- if (!existsSync10(path44)) return { version: 1, hashes: {} };
8012
+ async function loadCache(path45) {
8013
+ if (!existsSync10(path45)) return { version: 1, hashes: {} };
7640
8014
  try {
7641
- const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile7(path44, "utf8")));
8015
+ const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile8(path45, "utf8")));
7642
8016
  if (!parsed.success) return { version: 1, hashes: {} };
7643
8017
  return {
7644
8018
  version: 1,
@@ -7650,18 +8024,18 @@ async function loadCache(path44) {
7650
8024
  return { version: 1, hashes: {} };
7651
8025
  }
7652
8026
  }
7653
- async function saveCache(path44, cache) {
8027
+ async function saveCache(path45, cache) {
7654
8028
  const sorted = {};
7655
8029
  for (const key of Object.keys(cache.hashes).sort()) {
7656
8030
  const hash = cache.hashes[key];
7657
8031
  if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
7658
8032
  sorted[key] = hash;
7659
8033
  }
7660
- await mkdir4(pathModule.dirname(pathModule.resolve(path44)), { recursive: true });
7661
- const tmp = `${path44}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
8034
+ await mkdir5(pathModule.dirname(pathModule.resolve(path45)), { recursive: true });
8035
+ const tmp = `${path45}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
7662
8036
  try {
7663
- await writeFile4(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
7664
- await rename4(tmp, path44);
8037
+ await writeFile5(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
8038
+ await rename5(tmp, path45);
7665
8039
  } finally {
7666
8040
  await rm5(tmp, { force: true });
7667
8041
  }
@@ -7699,7 +8073,7 @@ async function adopt(provider, specs, lock, lockPath, opts = {}) {
7699
8073
  unmatchedLocal.push(`${spec.styleId}/${spec.assetId} (no file at ${spec.outFile})`);
7700
8074
  continue;
7701
8075
  }
7702
- const bytes = await readFile8(spec.outFile);
8076
+ const bytes = await readFile9(spec.outFile);
7703
8077
  try {
7704
8078
  decodePng(bytes);
7705
8079
  } catch (err) {
@@ -7819,7 +8193,7 @@ async function writePromptsBack(manifestPath, lock, opts = {}) {
7819
8193
  const log2 = opts.onProgress ?? (() => {
7820
8194
  });
7821
8195
  const selectedAssets = opts.assetIds ? new Set(opts.assetIds) : null;
7822
- const raw = JSON.parse(await readFile8(manifestPath, "utf8"));
8196
+ const raw = JSON.parse(await readFile9(manifestPath, "utf8"));
7823
8197
  const best = /* @__PURE__ */ new Map();
7824
8198
  for (const entry of Object.values(lock.entries)) {
7825
8199
  if (opts.provider && entry.provider !== opts.provider) continue;
@@ -7841,7 +8215,7 @@ async function writePromptsBack(manifestPath, lock, opts = {}) {
7841
8215
  stillEmpty.push(id);
7842
8216
  }
7843
8217
  }
7844
- await writeFile5(manifestPath, JSON.stringify(raw, null, 2) + "\n");
8218
+ await writeFile6(manifestPath, JSON.stringify(raw, null, 2) + "\n");
7845
8219
  log2(` filled ${filled} prompt(s) from adopted objects`);
7846
8220
  return { filled, stillEmpty };
7847
8221
  }
@@ -7853,14 +8227,14 @@ function formatUnmatchedRemote(objects, limit = 15) {
7853
8227
 
7854
8228
  // src/pipeline/adopt-characters.ts
7855
8229
  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";
8230
+ import { mkdir as mkdir6, readFile as readFile10, writeFile as writeFile7 } from "fs/promises";
8231
+ import path16 from "path";
7858
8232
  async function adoptCharacters(provider, specs, lock, lockPath, opts) {
7859
8233
  const log2 = opts.onProgress ?? (() => {
7860
8234
  });
7861
8235
  const result = { scanned: 0, matched: 0, unmatched: [], remaining: [], written: [], differing: [] };
7862
8236
  if (!provider.listCharacters || !provider.getCharacter) return result;
7863
- const wanted = specs.filter((spec) => spec.character);
8237
+ const wanted = specs.filter((spec) => spec.character && !spec.mirror);
7864
8238
  if (!wanted.length) return result;
7865
8239
  const remote = [];
7866
8240
  for await (const character of provider.listCharacters()) remote.push(character);
@@ -7906,11 +8280,11 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
7906
8280
  const hash = sha256(bytes);
7907
8281
  const file = expectedOutputPath(spec, source.role, index, sources.length);
7908
8282
  if (existsSync12(file)) {
7909
- if (sha256(await readFile9(file)) !== hash) result.differing.push(path15.relative(process.cwd(), file));
8283
+ if (sha256(await readFile10(file)) !== hash) result.differing.push(path16.relative(process.cwd(), file));
7910
8284
  } else {
7911
- await mkdir5(path15.dirname(file), { recursive: true });
7912
- await writeFile6(file, bytes);
7913
- result.written.push(path15.relative(process.cwd(), file));
8285
+ await mkdir6(path16.dirname(file), { recursive: true });
8286
+ await writeFile7(file, bytes);
8287
+ result.written.push(path16.relative(process.cwd(), file));
7914
8288
  }
7915
8289
  outputs.push({ path: portableOutputPath(file, spec.root), sha256: hash, ...source.role ? { role: source.role } : {} });
7916
8290
  }
@@ -7949,10 +8323,10 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
7949
8323
  } else {
7950
8324
  const south = expectedOutputPath(spec, "south", 0, spec.character.directions);
7951
8325
  if (!existsSync12(south)) {
7952
- result.unmatched.push(`${key}: no remoteId and no ${path15.relative(process.cwd(), south)} to match by`);
8326
+ result.unmatched.push(`${key}: no remoteId and no ${path16.relative(process.cwd(), south)} to match by`);
7953
8327
  return false;
7954
8328
  }
7955
- const localHash = sha256(await readFile9(south));
8329
+ const localHash = sha256(await readFile10(south));
7956
8330
  for (const candidate of remote) {
7957
8331
  if (claimed.has(candidate.id)) continue;
7958
8332
  if (await southHash(candidate) === localHash) {
@@ -7961,7 +8335,7 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
7961
8335
  }
7962
8336
  }
7963
8337
  if (!match) {
7964
- result.unmatched.push(`${key}: no character's south rotation has the bytes of ${path15.relative(process.cwd(), south)}`);
8338
+ result.unmatched.push(`${key}: no character's south rotation has the bytes of ${path16.relative(process.cwd(), south)}`);
7965
8339
  return false;
7966
8340
  }
7967
8341
  }
@@ -8031,12 +8405,12 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
8031
8405
  if (!existsSync12(first)) {
8032
8406
  const byName = candidates.find((a) => a.name === characterAnimationName(spec));
8033
8407
  if (!byName) {
8034
- result.unmatched.push(`${key}: no remoteId and no ${path15.relative(process.cwd(), first)} to match by`);
8408
+ result.unmatched.push(`${key}: no remoteId and no ${path16.relative(process.cwd(), first)} to match by`);
8035
8409
  return false;
8036
8410
  }
8037
8411
  match = byName;
8038
8412
  } else {
8039
- const localHash = sha256(await readFile9(first));
8413
+ const localHash = sha256(await readFile10(first));
8040
8414
  for (const candidate of candidates) {
8041
8415
  if (sha256(await download(candidate.frames[0])) === localHash) {
8042
8416
  match = candidate;
@@ -8044,7 +8418,7 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
8044
8418
  }
8045
8419
  }
8046
8420
  if (!match) {
8047
- result.unmatched.push(`${key}: no ${animation.direction} animation of character ${parentId} starts with the bytes of ${path15.relative(process.cwd(), first)}`);
8421
+ result.unmatched.push(`${key}: no ${animation.direction} animation of character ${parentId} starts with the bytes of ${path16.relative(process.cwd(), first)}`);
8048
8422
  return false;
8049
8423
  }
8050
8424
  }
@@ -8104,9 +8478,9 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
8104
8478
  }
8105
8479
 
8106
8480
  // src/pipeline/salvage.ts
8107
- import { readFile as readFile10 } from "fs/promises";
8481
+ import { readFile as readFile11 } from "fs/promises";
8108
8482
  import { existsSync as existsSync13 } from "fs";
8109
- import path16 from "path";
8483
+ import path17 from "path";
8110
8484
  function providerClaimId(provider, objectId) {
8111
8485
  return `${encodeURIComponent(provider)}:${encodeURIComponent(objectId)}`;
8112
8486
  }
@@ -8116,7 +8490,7 @@ async function loadClaims(lockPaths, opts = {}) {
8116
8490
  if (!existsSync13(p)) throw new Error(`Claim lockfile not found: ${p}`);
8117
8491
  let parsed;
8118
8492
  try {
8119
- parsed = parseLock(JSON.parse(await readFile10(p, "utf8")));
8493
+ parsed = parseLock(JSON.parse(await readFile11(p, "utf8")));
8120
8494
  } catch {
8121
8495
  throw new Error(`Claim lockfile is malformed: ${p}`);
8122
8496
  }
@@ -8164,20 +8538,20 @@ function matchStyleByPattern(prompt, manifest) {
8164
8538
  return null;
8165
8539
  }
8166
8540
  async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, claimPaths) {
8167
- const own = path16.resolve(ownManifestPath);
8541
+ const own = path17.resolve(ownManifestPath);
8168
8542
  const siblingManifestPaths = [
8169
8543
  .../* @__PURE__ */ new Set([
8170
8544
  ...workspaceManifestPaths,
8171
- ...claimPaths.map((c) => path16.join(path16.dirname(path16.resolve(c)), "pixelkiln.manifest.json"))
8545
+ ...claimPaths.map((c) => path17.join(path17.dirname(path17.resolve(c)), "pixelkiln.manifest.json"))
8172
8546
  ])
8173
8547
  ];
8174
8548
  const siblings = [];
8175
8549
  for (const siblingManifestPath of siblingManifestPaths) {
8176
- if (path16.resolve(siblingManifestPath) === own) continue;
8550
+ if (path17.resolve(siblingManifestPath) === own) continue;
8177
8551
  if (!existsSync13(siblingManifestPath)) continue;
8178
8552
  try {
8179
8553
  const { manifest } = await loadManifest(siblingManifestPath);
8180
- siblings.push({ label: path16.basename(path16.dirname(siblingManifestPath)), manifest });
8554
+ siblings.push({ label: path17.basename(path17.dirname(siblingManifestPath)), manifest });
8181
8555
  } catch {
8182
8556
  }
8183
8557
  }
@@ -8293,9 +8667,9 @@ async function applyTags(provider, decisions, existing, opts = {}) {
8293
8667
  }
8294
8668
 
8295
8669
  // src/workspace.ts
8296
- import { mkdir as mkdir6, readFile as readFile11, rename as rename5, rm as rm6, writeFile as writeFile7 } from "fs/promises";
8670
+ import { mkdir as mkdir7, readFile as readFile12, rename as rename6, rm as rm6, writeFile as writeFile8 } from "fs/promises";
8297
8671
  import { existsSync as existsSync14 } from "fs";
8298
- import path17 from "path";
8672
+ import path18 from "path";
8299
8673
  import { z as z4 } from "zod";
8300
8674
  var WorkspaceProjectSchema = z4.object({
8301
8675
  id: z4.string().min(1),
@@ -8323,7 +8697,7 @@ async function loadWorkspace(workspacePath) {
8323
8697
  if (!existsSync14(workspacePath)) return { version: 1, projects: [] };
8324
8698
  let raw;
8325
8699
  try {
8326
- raw = JSON.parse(await readFile11(workspacePath, "utf8"));
8700
+ raw = JSON.parse(await readFile12(workspacePath, "utf8"));
8327
8701
  } catch (err) {
8328
8702
  throw new ProjectError(
8329
8703
  `Workspace catalog at ${workspacePath} is malformed:
@@ -8337,22 +8711,22 @@ async function saveWorkspace(workspacePath, ws) {
8337
8711
  version: 1,
8338
8712
  projects: [...ws.projects].sort((a, b) => a.id.localeCompare(b.id))
8339
8713
  };
8340
- await mkdir6(path17.dirname(path17.resolve(workspacePath)), { recursive: true });
8714
+ await mkdir7(path18.dirname(path18.resolve(workspacePath)), { recursive: true });
8341
8715
  const tmp = `${workspacePath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
8342
8716
  try {
8343
- await writeFile7(tmp, JSON.stringify(sorted, null, 2) + "\n");
8344
- await rename5(tmp, workspacePath);
8717
+ await writeFile8(tmp, JSON.stringify(sorted, null, 2) + "\n");
8718
+ await rename6(tmp, workspacePath);
8345
8719
  } finally {
8346
8720
  await rm6(tmp, { force: true });
8347
8721
  }
8348
8722
  }
8349
8723
  function toPortablePath(dir, absolute) {
8350
- return path17.relative(dir, absolute).split(path17.sep).join("/");
8724
+ return path18.relative(dir, absolute).split(path18.sep).join("/");
8351
8725
  }
8352
8726
  function resolveProject(dir, project) {
8353
8727
  return {
8354
- manifestPath: path17.resolve(dir, project.manifest.split("/").join(path17.sep)),
8355
- lockPath: path17.resolve(dir, project.lock.split("/").join(path17.sep))
8728
+ manifestPath: path18.resolve(dir, project.manifest.split("/").join(path18.sep)),
8729
+ lockPath: path18.resolve(dir, project.lock.split("/").join(path18.sep))
8356
8730
  };
8357
8731
  }
8358
8732
  function validateWorkspace(ws, dir) {
@@ -8365,7 +8739,7 @@ function validateWorkspace(ws, dir) {
8365
8739
  const { manifestPath, lockPath } = resolveProject(dir, project);
8366
8740
  lockOwners.set(lockPath, [...lockOwners.get(lockPath) ?? [], project.id]);
8367
8741
  manifestOwners.set(manifestPath, [...manifestOwners.get(manifestPath) ?? [], project.id]);
8368
- if (path17.isAbsolute(project.manifest) || path17.isAbsolute(project.lock)) {
8742
+ if (path18.isAbsolute(project.manifest) || path18.isAbsolute(project.lock)) {
8369
8743
  diagnostics.push({
8370
8744
  id: "absolute-path",
8371
8745
  level: "warning",
@@ -8426,8 +8800,8 @@ function validateWorkspace(ws, dir) {
8426
8800
  }
8427
8801
 
8428
8802
  // src/pick/salvage-server.ts
8429
- import { mkdir as mkdir7, writeFile as writeFile8, readFile as readFile13 } from "fs/promises";
8430
- import path18 from "path";
8803
+ import { mkdir as mkdir8, writeFile as writeFile9, readFile as readFile14 } from "fs/promises";
8804
+ import path19 from "path";
8431
8805
 
8432
8806
  // src/pick/salvage-sheet.ts
8433
8807
  var escapeHtml = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
@@ -8587,7 +8961,7 @@ refresh();
8587
8961
 
8588
8962
  // src/pick/review-server.ts
8589
8963
  import { createServer } from "http";
8590
- import { readFile as readFile12 } from "fs/promises";
8964
+ import { readFile as readFile13 } from "fs/promises";
8591
8965
 
8592
8966
  // src/open.ts
8593
8967
  import { spawn } from "child_process";
@@ -8614,7 +8988,7 @@ function serveReviewPage(opts) {
8614
8988
  if (req.method === "GET" && req.url && opts.assets?.has(req.url)) {
8615
8989
  try {
8616
8990
  const asset = opts.assets.get(req.url);
8617
- const bytes = await readFile12(asset.path);
8991
+ const bytes = await readFile13(asset.path);
8618
8992
  res.writeHead(200, {
8619
8993
  "Content-Type": asset.contentType,
8620
8994
  "Cache-Control": "no-store",
@@ -8691,7 +9065,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
8691
9065
  });
8692
9066
  const html = renderSalvageSheet(orphans, {
8693
9067
  styleId: ctx.styleId,
8694
- importDir: path18.relative(process.cwd(), ctx.importDir) || "."
9068
+ importDir: path19.relative(process.cwd(), ctx.importDir) || "."
8695
9069
  });
8696
9070
  const byId = new Map(orphans.map((o) => [o.id, o]));
8697
9071
  const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
@@ -8720,10 +9094,10 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
8720
9094
  if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
8721
9095
  decodePng(buf);
8722
9096
  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);
9097
+ const rel = path19.join("_salvaged", `${assetId}.png`);
9098
+ const outFile = path19.resolve(ctx.importDir, rel);
9099
+ await mkdir8(path19.dirname(outFile), { recursive: true });
9100
+ await writeFile9(outFile, buf);
8727
9101
  ctx.manifest.assets[assetId] = {
8728
9102
  prompt: orphan.prompt,
8729
9103
  promptByStyle: {},
@@ -8755,7 +9129,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
8755
9129
  sourceUrl: durableSource,
8756
9130
  sourceUrls: durableSource ? [{ url: durableSource }] : [],
8757
9131
  outputs: [{
8758
- path: portableOutputPath(outFile, path18.dirname(ctx.manifestPath)),
9132
+ path: portableOutputPath(outFile, path19.dirname(ctx.manifestPath)),
8759
9133
  sha256: sha256(buf)
8760
9134
  }],
8761
9135
  submittedAt: orphan.createdAt,
@@ -8779,9 +9153,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
8779
9153
  }
8780
9154
  }
8781
9155
  await applyTags(provider, decisions, existingTags, { onProgress: log2 });
8782
- const raw = JSON.parse(await readFile13(ctx.manifestPath, "utf8"));
9156
+ const raw = JSON.parse(await readFile14(ctx.manifestPath, "utf8"));
8783
9157
  for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
8784
- await writeFile8(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
9158
+ await writeFile9(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
8785
9159
  await saveLock(ctx.lockPath, ctx.lock);
8786
9160
  return result;
8787
9161
  }
@@ -8789,19 +9163,19 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
8789
9163
  }
8790
9164
 
8791
9165
  // src/cli/project.ts
8792
- import path20 from "path";
9166
+ import path21 from "path";
8793
9167
  import { existsSync as existsSync15 } from "fs";
8794
9168
 
8795
9169
  // src/project.ts
8796
- import path19 from "path";
9170
+ import path20 from "path";
8797
9171
  async function openProject(manifestPath = "pixelkiln.manifest.json", opts = {}) {
8798
- const absolute = path19.resolve(manifestPath);
8799
- const root = path19.dirname(absolute);
9172
+ const absolute = path20.resolve(manifestPath);
9173
+ const root = path20.dirname(absolute);
8800
9174
  if (opts.env !== false) {
8801
9175
  loadEnvFiles(root);
8802
- if (path19.resolve(process.cwd()) !== root) loadEnvFiles(process.cwd());
9176
+ if (path20.resolve(process.cwd()) !== root) loadEnvFiles(process.cwd());
8803
9177
  }
8804
- const lockPath = path19.resolve(opts.lockPath ?? path19.join(root, "pixelkiln.lock.json"));
9178
+ const lockPath = path20.resolve(opts.lockPath ?? path20.join(root, "pixelkiln.lock.json"));
8805
9179
  const loaded = await loadManifest(absolute);
8806
9180
  const specs = await resolveSpecs(loaded, { styles: opts.styles ?? [], assets: opts.assets ?? [] });
8807
9181
  const lock = await loadLock(lockPath);
@@ -8932,7 +9306,7 @@ async function workspaceStatus(ws, dir) {
8932
9306
 
8933
9307
  // src/cli/project.ts
8934
9308
  async function provenanceFile(id, file) {
8935
- const absolute = path20.resolve(file);
9309
+ const absolute = path21.resolve(file);
8936
9310
  return {
8937
9311
  id,
8938
9312
  path: absolute,
@@ -9011,7 +9385,7 @@ async function requireCompleteWorkspaceClaims(workspacePath) {
9011
9385
  if (!existsSync15(workspacePath)) {
9012
9386
  throw new ProjectError(`Workspace catalog not found: ${workspacePath}`);
9013
9387
  }
9014
- const dir = path20.dirname(path20.resolve(workspacePath));
9388
+ const dir = path21.dirname(path21.resolve(workspacePath));
9015
9389
  const ws = await loadWorkspace(workspacePath);
9016
9390
  const diagnostics = validateWorkspace(ws, dir);
9017
9391
  const errors = diagnostics.filter((d) => d.level === "error");
@@ -9034,9 +9408,9 @@ async function openAccountProject(args) {
9034
9408
  return project;
9035
9409
  }
9036
9410
  async function open(args) {
9037
- if (!existsSync15(path20.resolve(args.manifest))) {
9411
+ if (!existsSync15(path21.resolve(args.manifest))) {
9038
9412
  throw new ProjectError(
9039
- `No manifest at ${path20.resolve(args.manifest)}. Pass --manifest, or run \`pixelkiln init --from <dir>\`.`
9413
+ `No manifest at ${path21.resolve(args.manifest)}. Pass --manifest, or run \`pixelkiln init --from <dir>\`.`
9040
9414
  );
9041
9415
  }
9042
9416
  const project = await openProject(args.manifest, { lockPath: args.lock, styles: args.styles, assets: args.assets });
@@ -9078,7 +9452,7 @@ function providerCache(mode2) {
9078
9452
 
9079
9453
  // src/cli/commands/account.ts
9080
9454
  async function runBalance(args) {
9081
- loadEnvFiles(path21.dirname(path21.resolve(args.manifest)));
9455
+ loadEnvFiles(path22.dirname(path22.resolve(args.manifest)));
9082
9456
  loadEnvFiles(process.cwd());
9083
9457
  const loaded = await loadManifest(args.manifest);
9084
9458
  const selectedProvider = accountProviderId(loaded.manifest, args.provider, "balance");
@@ -9151,12 +9525,12 @@ async function runAdopt(args) {
9151
9525
  tagged ${n} object(s) upstream`);
9152
9526
  }
9153
9527
  if (args.writePrompts) {
9154
- const { filled, stillEmpty } = await writePromptsBack(path21.resolve(args.manifest), lock, {
9528
+ const { filled, stillEmpty } = await writePromptsBack(path22.resolve(args.manifest), lock, {
9155
9529
  onProgress: log,
9156
9530
  provider: provider.id,
9157
9531
  assetIds: specs.map((spec) => spec.assetId)
9158
9532
  });
9159
- log(` recovered ${filled} prompt(s) into ${path21.relative(process.cwd(), args.manifest)}`);
9533
+ log(` recovered ${filled} prompt(s) into ${path22.relative(process.cwd(), args.manifest)}`);
9160
9534
  const reloaded = await loadManifest(args.manifest);
9161
9535
  const rebased = (await resolveSpecs(reloaded, {
9162
9536
  styles: args.styles,
@@ -9186,12 +9560,12 @@ async function runSalvage2(args) {
9186
9560
  const provider = providerFor(accountProvider);
9187
9561
  const jsonMode = args.dryRun && args.json;
9188
9562
  const diag = jsonMode ? (msg = "") => console.error(msg) : log;
9189
- const ownLock = path21.resolve(args.lock);
9563
+ const ownLock = path22.resolve(args.lock);
9190
9564
  let workspaceProjects = [];
9191
9565
  let workspaceDir = "";
9192
9566
  if (args.workspace) {
9193
- const workspacePath = path21.resolve(args.workspace);
9194
- workspaceDir = path21.dirname(workspacePath);
9567
+ const workspacePath = path22.resolve(args.workspace);
9568
+ workspaceDir = path22.dirname(workspacePath);
9195
9569
  const complete = await requireCompleteWorkspaceClaims(workspacePath);
9196
9570
  workspaceProjects = complete.ws.projects;
9197
9571
  for (const d of complete.diagnostics) diag(` WARN ${d.id}: ${d.message}`);
@@ -9201,11 +9575,11 @@ async function runSalvage2(args) {
9201
9575
  .../* @__PURE__ */ new Set([
9202
9576
  ...workspaceLockPaths,
9203
9577
  ...existsSync16(ownLock) ? [ownLock] : [],
9204
- ...args.claims.map((c) => path21.resolve(c))
9578
+ ...args.claims.map((c) => path22.resolve(c))
9205
9579
  ])
9206
9580
  ];
9207
9581
  diag(` claim set (${lockPaths.length} lockfile(s)):`);
9208
- for (const p of lockPaths) diag(` ${path21.relative(process.cwd(), p)}`);
9582
+ for (const p of lockPaths) diag(` ${path22.relative(process.cwd(), p)}`);
9209
9583
  if (!args.claims.length && !args.workspace) {
9210
9584
  diag(
9211
9585
  `
@@ -9213,7 +9587,7 @@ async function runSalvage2(args) {
9213
9587
  pass every other project's lockfile via --claims a.json,b.json, or
9214
9588
  register every project in a workspace catalog and pass --workspace.`
9215
9589
  );
9216
- } else if (args.workspace && !workspaceProjects.some((p) => resolveProject(workspaceDir, p).manifestPath === path21.resolve(args.manifest))) {
9590
+ } else if (args.workspace && !workspaceProjects.some((p) => resolveProject(workspaceDir, p).manifestPath === path22.resolve(args.manifest))) {
9217
9591
  diag(
9218
9592
  `
9219
9593
  This project's manifest is not registered in the workspace catalog. Its own
@@ -9297,7 +9671,7 @@ async function runSalvage2(args) {
9297
9671
  manifestPath: loaded.path,
9298
9672
  manifest: accountManifest,
9299
9673
  styleId,
9300
- importDir: path21.resolve(loaded.root, style.outDir),
9674
+ importDir: path22.resolve(loaded.root, style.outDir),
9301
9675
  lock,
9302
9676
  lockPath: args.lock
9303
9677
  },
@@ -9398,17 +9772,17 @@ async function runTag(args) {
9398
9772
  }
9399
9773
 
9400
9774
  // src/cli/commands/edit.ts
9401
- import path24 from "path";
9775
+ import path25 from "path";
9402
9776
 
9403
9777
  // src/pipeline/hand-edit.ts
9404
9778
  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";
9779
+ import { copyFile, mkdir as mkdir9, readFile as readFile16, rename as rename8, rm as rm8, writeFile as writeFile11 } from "fs/promises";
9780
+ import path24 from "path";
9407
9781
  import { z as z6 } from "zod";
9408
9782
 
9409
9783
  // 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";
9784
+ import { readFile as readFile15, rename as rename7, rm as rm7, writeFile as writeFile10 } from "fs/promises";
9785
+ import path23 from "path";
9412
9786
  import { z as z5 } from "zod";
9413
9787
  var HexSha = z5.string().regex(/^[0-9a-f]{64}$/);
9414
9788
  var AssetPatchSchema = z5.object({
@@ -9623,8 +9997,8 @@ function applyEdit(raw, edit) {
9623
9997
  }
9624
9998
  }
9625
9999
  async function applyManifestEdit(manifestPath, edit) {
9626
- const absolute = path22.resolve(manifestPath);
9627
- const original = await readFile14(absolute, "utf8");
10000
+ const absolute = path23.resolve(manifestPath);
10001
+ const original = await readFile15(absolute, "utf8");
9628
10002
  const before = sha256(original);
9629
10003
  if (before !== edit.expectedSha256) throw new ManifestDriftError(absolute);
9630
10004
  let raw;
@@ -9638,12 +10012,12 @@ async function applyManifestEdit(manifestPath, edit) {
9638
10012
  applyEdit(raw, edit);
9639
10013
  const next = serializeLike(original, raw);
9640
10014
  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`
10015
+ const tmp = path23.join(
10016
+ path23.dirname(absolute),
10017
+ `.${path23.basename(absolute)}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`
9644
10018
  );
9645
10019
  try {
9646
- await writeFile9(tmp, next);
10020
+ await writeFile10(tmp, next);
9647
10021
  try {
9648
10022
  const loaded = await loadManifest(tmp);
9649
10023
  await resolveSpecs(loaded);
@@ -9651,7 +10025,7 @@ async function applyManifestEdit(manifestPath, edit) {
9651
10025
  const message6 = error instanceof Error ? error.message : String(error);
9652
10026
  throw new ManifestEditError(message6.replace(tmp, absolute));
9653
10027
  }
9654
- await rename6(tmp, absolute);
10028
+ await rename7(tmp, absolute);
9655
10029
  } finally {
9656
10030
  await rm7(tmp, { force: true });
9657
10031
  }
@@ -9662,11 +10036,11 @@ async function applyManifestEdit(manifestPath, edit) {
9662
10036
  var HAND_EDIT_DIR = "edits";
9663
10037
  function handEditPath(loaded, spec) {
9664
10038
  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"));
10039
+ const outDir = path24.resolve(loaded.root, style?.outDir ?? ".");
10040
+ const relative = path24.relative(outDir, spec.outFile);
10041
+ const inside = relative && !relative.startsWith("..") && !path24.isAbsolute(relative);
10042
+ const file = inside ? relative : path24.basename(spec.outFile);
10043
+ return path24.join(outDir, HAND_EDIT_DIR, file.replace(/\.gif$/i, ".png"));
9670
10044
  }
9671
10045
  function handEditBases(spec, lock) {
9672
10046
  const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
@@ -9708,7 +10082,7 @@ async function startHandEdit(loaded, lock, spec, opts = {}) {
9708
10082
  `${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
10083
  );
9710
10084
  }
9711
- const existing = spec.source ? path23.resolve(loaded.root, spec.source) : null;
10085
+ const existing = spec.source ? path24.resolve(loaded.root, spec.source) : null;
9712
10086
  const editPath = existing ?? handEditPath(loaded, spec);
9713
10087
  const members = handEditMembers(loaded, spec, lock, editPath);
9714
10088
  let created = false;
@@ -9719,7 +10093,7 @@ async function startHandEdit(loaded, lock, spec, opts = {}) {
9719
10093
  `${spec.styleId}/${spec.assetId} has no generated PNG on disk to start from; generate or restore it first`
9720
10094
  );
9721
10095
  }
9722
- await mkdir8(path23.dirname(member.path), { recursive: true });
10096
+ await mkdir9(path24.dirname(member.path), { recursive: true });
9723
10097
  await copyFile(member.base, member.path);
9724
10098
  created = true;
9725
10099
  }
@@ -9798,7 +10172,7 @@ function handEditProjectPath(editPath) {
9798
10172
  async function readHandEditCompanion(editPath) {
9799
10173
  let text;
9800
10174
  try {
9801
- text = await readFile15(handEditCompanionPath(editPath), "utf8");
10175
+ text = await readFile16(handEditCompanionPath(editPath), "utf8");
9802
10176
  } catch {
9803
10177
  return null;
9804
10178
  }
@@ -9841,7 +10215,7 @@ async function saveHandEdit(loaded, lock, spec, input) {
9841
10215
  }
9842
10216
  });
9843
10217
  const bases = handEditBases(spec, lock);
9844
- const expectedSize = bases?.[0] ? decodePng(await readFile15(bases[0].path)) : { width: spec.width, height: spec.height };
10218
+ const expectedSize = bases?.[0] ? decodePng(await readFile16(bases[0].path)) : { width: spec.width, height: spec.height };
9845
10219
  for (const [index, image] of decoded.entries()) {
9846
10220
  if (image.width !== expectedSize.width || image.height !== expectedSize.height) {
9847
10221
  const which = memberSet ? `member ${incoming[index].role ?? index}` : "the edit";
@@ -9857,7 +10231,7 @@ async function saveHandEdit(loaded, lock, spec, input) {
9857
10231
  editor: input.editor,
9858
10232
  protocol: input.protocol,
9859
10233
  savedAt: (input.now ?? /* @__PURE__ */ new Date()).toISOString(),
9860
- project: projectPath ? path23.basename(projectPath) : null,
10234
+ project: projectPath ? path24.basename(projectPath) : null,
9861
10235
  outputs: incoming.map((frame, index) => ({
9862
10236
  role: frame.role,
9863
10237
  basedOn: basedOnHash(entry, index),
@@ -9887,8 +10261,8 @@ function basedOnHash(entry, index) {
9887
10261
  async function replaceFile(file, bytes) {
9888
10262
  const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
9889
10263
  try {
9890
- await writeFile10(tmp, bytes);
9891
- await rename7(tmp, file);
10264
+ await writeFile11(tmp, bytes);
10265
+ await rename8(tmp, file);
9892
10266
  } finally {
9893
10267
  await rm8(tmp, { force: true });
9894
10268
  }
@@ -9917,7 +10291,7 @@ async function runEdit(args) {
9917
10291
  const set = started.members.length > 1;
9918
10292
  const kind = spec.generator === "frames" ? "frames" : "members";
9919
10293
  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)}`);
10294
+ if (set) for (const member of started.members) log(` ${member.role ?? ""} ${path25.relative(process.cwd(), member.path)}`);
9921
10295
  if (args.noOpen) {
9922
10296
  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
10297
  } else {
@@ -9927,17 +10301,17 @@ async function runEdit(args) {
9927
10301
  }
9928
10302
 
9929
10303
  // src/cli/commands/gallery.ts
9930
- import path31 from "path";
10304
+ import path32 from "path";
9931
10305
  import { existsSync as existsSync23 } from "fs";
9932
10306
 
9933
10307
  // src/gallery/snapshot.ts
9934
10308
  import { createHash as createHash3 } from "crypto";
9935
10309
  import { existsSync as existsSync19 } from "fs";
9936
- import { readFile as readFile16, stat as stat2 } from "fs/promises";
9937
- import path27 from "path";
10310
+ import { readFile as readFile17, stat as stat2 } from "fs/promises";
10311
+ import path28 from "path";
9938
10312
 
9939
10313
  // src/gallery/edit.ts
9940
- import path25 from "path";
10314
+ import path26 from "path";
9941
10315
  import { z as z7 } from "zod";
9942
10316
  var Base64 = z7.string().regex(/^[A-Za-z0-9+/]*={0,2}$/).max(Math.ceil(MAX_HAND_EDIT_BYTES / 3) * 4);
9943
10317
  var HandEditRequestSchema = z7.discriminatedUnion("action", [
@@ -10027,7 +10401,7 @@ function createGalleryEditHandler(opts) {
10027
10401
  }
10028
10402
  const result = await applyManifestEdit(manifestPath, parsed.data);
10029
10403
  if (result.changed) {
10030
- const relative = path25.relative(process.cwd(), result.manifestPath);
10404
+ const relative = path26.relative(process.cwd(), result.manifestPath);
10031
10405
  const shown = !relative || relative.startsWith("..") ? result.manifestPath : relative;
10032
10406
  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
10407
  log2(` manifest edited: ${parsed.data.action === "add-asset" ? "added" : "changed"} ${subject} in ${shown}`);
@@ -10038,7 +10412,7 @@ function createGalleryEditHandler(opts) {
10038
10412
 
10039
10413
  // src/pipeline/history.ts
10040
10414
  import { existsSync as existsSync18 } from "fs";
10041
- import path26 from "path";
10415
+ import path27 from "path";
10042
10416
  var HISTORY_ENV = "PIXELKILN_HISTORY";
10043
10417
  function historyLimit(manifest, env = process.env) {
10044
10418
  if (manifest?.history !== void 0) return manifest.history;
@@ -10127,7 +10501,7 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
10127
10501
  if (!existsSync18(file)) continue;
10128
10502
  if (await sha256File(file) !== entry.outputs[i].sha256 && !opts.force) {
10129
10503
  throw new OverwriteRefusedError(
10130
- `${path26.relative(process.cwd(), file)} was changed after download; keep it with pixelkiln edit, or pass --force to replace it`,
10504
+ `${path27.relative(process.cwd(), file)} was changed after download; keep it with pixelkiln edit, or pass --force to replace it`,
10131
10505
  [file]
10132
10506
  );
10133
10507
  }
@@ -10179,7 +10553,7 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
10179
10553
  // src/gallery/snapshot.ts
10180
10554
  var PROJECT_FILE_TYPE = "application/octet-stream";
10181
10555
  function galleryMediaId(absolutePath) {
10182
- return createHash3("sha256").update(path27.resolve(absolutePath)).digest("hex").slice(0, 24);
10556
+ return createHash3("sha256").update(path28.resolve(absolutePath)).digest("hex").slice(0, 24);
10183
10557
  }
10184
10558
  function galleryMediaRoute(id) {
10185
10559
  return `/media/${id}`;
@@ -10198,7 +10572,7 @@ function describeHistory(media, entry, cacheDir) {
10198
10572
  return (entry.history ?? []).map((generation, i) => {
10199
10573
  const outputs = generation.outputs.map((output) => {
10200
10574
  const mediaType = output.mediaType ?? MediaType.PNG;
10201
- const file = path27.join(cacheDir, cacheFileName(output.sha256, mediaType));
10575
+ const file = path28.join(cacheDir, cacheFileName(output.sha256, mediaType));
10202
10576
  let url = null;
10203
10577
  if (existsSync19(file)) {
10204
10578
  const id = galleryMediaId(file);
@@ -10229,7 +10603,7 @@ function describeHistory(media, entry, cacheDir) {
10229
10603
  var MAX_COMPARED_BYTES = 8 * 1024 * 1024;
10230
10604
  async function samePixels(a, b) {
10231
10605
  try {
10232
- const [left, right] = await Promise.all([readFile16(a), readFile16(b)]);
10606
+ const [left, right] = await Promise.all([readFile17(a), readFile17(b)]);
10233
10607
  if (left.length > MAX_COMPARED_BYTES || right.length > MAX_COMPARED_BYTES) return false;
10234
10608
  const x = decodePng(left);
10235
10609
  const y = decodePng(right);
@@ -10299,9 +10673,9 @@ async function describeOutput(media, root, absolutePath, recorded = {}) {
10299
10673
  async function describeQuality(media, root, inspection) {
10300
10674
  const outputPaths = inspection.outputs ?? [inspection.output];
10301
10675
  const outputs = await Promise.all(
10302
- outputPaths.map((file) => describeOutput(media, root, path27.resolve(root, file)))
10676
+ outputPaths.map((file) => describeOutput(media, root, path28.resolve(root, file)))
10303
10677
  );
10304
- const recordPath = path27.resolve(root, inspection.record);
10678
+ const recordPath = path28.resolve(root, inspection.record);
10305
10679
  const recordExists = existsSync19(recordPath);
10306
10680
  let options = null;
10307
10681
  let check = null;
@@ -10321,7 +10695,7 @@ async function describeQuality(media, root, inspection) {
10321
10695
  return {
10322
10696
  state: inspection.state,
10323
10697
  reason: inspection.reason,
10324
- output: portableOutputPath(path27.resolve(root, inspection.output), root),
10698
+ output: portableOutputPath(path28.resolve(root, inspection.output), root),
10325
10699
  record: portableOutputPath(recordPath, root),
10326
10700
  recordExists,
10327
10701
  outputs,
@@ -10358,9 +10732,9 @@ async function buildGallerySnapshot(opts) {
10358
10732
  const media = /* @__PURE__ */ new Map();
10359
10733
  const plan = await buildPlan(specs, lock);
10360
10734
  const items = [];
10361
- const manifestText = await readFile16(loaded.path, "utf8");
10735
+ const manifestText = await readFile17(loaded.path, "utf8");
10362
10736
  const manifestSha256 = sha256(manifestText);
10363
- const cacheDir = path27.resolve(path27.dirname(opts.lockPath), ".pixelkiln", "cache");
10737
+ const cacheDir = path28.resolve(path28.dirname(opts.lockPath), ".pixelkiln", "cache");
10364
10738
  let rawStyles = {};
10365
10739
  try {
10366
10740
  const raw = JSON.parse(manifestText);
@@ -10384,7 +10758,7 @@ async function buildGallerySnapshot(opts) {
10384
10758
  )
10385
10759
  );
10386
10760
  if (spec.source) {
10387
- const editPath = path27.resolve(root, spec.source);
10761
+ const editPath = path28.resolve(root, spec.source);
10388
10762
  const memberCount = sourceIsStem(spec.source, entry, root) ? entry.outputs.length : 1;
10389
10763
  const editShas = await Promise.all(
10390
10764
  Array.from({ length: memberCount }, async (_, index) => {
@@ -10433,7 +10807,7 @@ async function buildGallerySnapshot(opts) {
10433
10807
  editStatus = edits.some((member) => !member.exists) ? "missing" : editChanged.every((changed) => !changed) ? "same" : regenerated ? "regenerated-since" : "edited";
10434
10808
  }
10435
10809
  } else if (spec.source) {
10436
- outputs = [await describeOutput(media, root, path27.resolve(root, spec.source))];
10810
+ outputs = [await describeOutput(media, root, path28.resolve(root, spec.source))];
10437
10811
  } else if (!entry && existsSync19(spec.outFile)) {
10438
10812
  outputs = [await describeOutput(media, root, spec.outFile)];
10439
10813
  } else {
@@ -10473,6 +10847,7 @@ async function buildGallerySnapshot(opts) {
10473
10847
  currentSpecHash: spec.specHash,
10474
10848
  revision: entry?.revision ?? null,
10475
10849
  revisionParentKey: spec.revision ? lockKey(spec.styleId, spec.revision.sourceAssetId) : entry?.revision ? lockKey(spec.styleId, entry.revision.sourceAssetId) : null,
10850
+ mirrorOfKey: spec.mirror ? lockKey(spec.styleId, spec.mirror.sourceAssetId) : entry?.mirror ? lockKey(spec.styleId, entry.mirror.sourceAssetId) : null,
10476
10851
  character: describeCharacter(spec, entry),
10477
10852
  outputs,
10478
10853
  quality,
@@ -10535,6 +10910,7 @@ async function buildGallerySnapshot(opts) {
10535
10910
  currentSpecHash: null,
10536
10911
  revision: entry.revision,
10537
10912
  revisionParentKey: entry.revision ? lockKey(entry.styleId, entry.revision.sourceAssetId) : null,
10913
+ mirrorOfKey: entry.mirror ? lockKey(entry.styleId, entry.mirror.sourceAssetId) : null,
10538
10914
  character: describeCharacter(void 0, entry),
10539
10915
  outputs,
10540
10916
  quality: null,
@@ -10623,7 +10999,7 @@ async function buildGallerySnapshot(opts) {
10623
10999
  id: loaded.manifest.name,
10624
11000
  name: loaded.manifest.name,
10625
11001
  manifest: loaded.path,
10626
- lock: path27.resolve(opts.lockPath),
11002
+ lock: path28.resolve(opts.lockPath),
10627
11003
  root,
10628
11004
  provider: loaded.manifest.provider,
10629
11005
  account: null,
@@ -10649,7 +11025,7 @@ async function buildGallerySnapshot(opts) {
10649
11025
  return { snapshot, media };
10650
11026
  }
10651
11027
  async function buildWorkspaceGallerySnapshot(opts) {
10652
- const dir = path27.dirname(path27.resolve(opts.workspacePath));
11028
+ const dir = path28.dirname(path28.resolve(opts.workspacePath));
10653
11029
  const media = /* @__PURE__ */ new Map();
10654
11030
  const projects = [];
10655
11031
  const items = [];
@@ -10692,11 +11068,11 @@ async function buildWorkspaceGallerySnapshot(opts) {
10692
11068
  id: project.id,
10693
11069
  name: loaded.manifest.name,
10694
11070
  manifest: manifestPath,
10695
- lock: path27.resolve(lockPath),
11071
+ lock: path28.resolve(lockPath),
10696
11072
  root: loaded.root,
10697
11073
  provider: loaded.manifest.provider,
10698
11074
  account: project.account ?? null,
10699
- manifestSha256: manifestSha256 ?? sha256(await readFile16(manifestPath, "utf8")),
11075
+ manifestSha256: manifestSha256 ?? sha256(await readFile17(manifestPath, "utf8")),
10700
11076
  historyLimit: historyLimit(loaded.manifest),
10701
11077
  entries: Object.keys(lock.entries).length,
10702
11078
  items: projectItems.length,
@@ -10708,8 +11084,8 @@ async function buildWorkspaceGallerySnapshot(opts) {
10708
11084
  id: project.id,
10709
11085
  name: project.id,
10710
11086
  manifest: manifestPath,
10711
- lock: path27.resolve(lockPath),
10712
- root: path27.dirname(manifestPath),
11087
+ lock: path28.resolve(lockPath),
11088
+ root: path28.dirname(manifestPath),
10713
11089
  provider: project.provider,
10714
11090
  account: project.account ?? null,
10715
11091
  manifestSha256: null,
@@ -10736,7 +11112,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
10736
11112
  version: 1,
10737
11113
  generatedAt: (opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
10738
11114
  project: null,
10739
- workspace: { path: path27.resolve(opts.workspacePath), dir, projects },
11115
+ workspace: { path: path28.resolve(opts.workspacePath), dir, projects },
10740
11116
  filter,
10741
11117
  totals: {
10742
11118
  entries: projects.reduce((sum, project) => sum + project.entries, 0),
@@ -10755,7 +11131,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
10755
11131
  // src/gallery/server.ts
10756
11132
  import { randomBytes } from "crypto";
10757
11133
  import { createServer as createServer2 } from "http";
10758
- import { readFile as readFile17 } from "fs/promises";
11134
+ import { readFile as readFile18 } from "fs/promises";
10759
11135
 
10760
11136
  // src/gallery/page.ts
10761
11137
  import { readFileSync as readFileSync4 } from "fs";
@@ -10996,7 +11372,7 @@ async function serveGallery(opts) {
10996
11372
  const asset = reviewAssets.get(jobId)?.get(url2.pathname);
10997
11373
  if (!asset) return fail(404, "no such review asset");
10998
11374
  try {
10999
- const bytes = await readFile17(asset.path);
11375
+ const bytes = await readFile18(asset.path);
11000
11376
  res.writeHead(200, {
11001
11377
  "Content-Type": asset.contentType,
11002
11378
  "Cache-Control": "no-store",
@@ -11072,7 +11448,7 @@ async function serveGallery(opts) {
11072
11448
  if (!asset) return fail(404, "no such gallery media");
11073
11449
  let bytes;
11074
11450
  try {
11075
- bytes = await readFile17(asset.path);
11451
+ bytes = await readFile18(asset.path);
11076
11452
  } catch {
11077
11453
  return fail(404, "gallery media is no longer on disk; refresh the page");
11078
11454
  }
@@ -11216,7 +11592,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
11216
11592
  });
11217
11593
  const estimates = new Map(items.map((item) => [
11218
11594
  item.key,
11219
- validateCostEstimate(provider.id, provider.estimate(item.spec))
11595
+ item.spec.mirror ? { amount: 0, unit: item.spec.costUnit, candidates: 1 } : validateCostEstimate(provider.id, provider.estimate(item.spec))
11220
11596
  ]));
11221
11597
  const units = new Set([...estimates.values()].map((estimate) => estimate.unit));
11222
11598
  if (units.size > 1) {
@@ -11266,6 +11642,34 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
11266
11642
  }
11267
11643
  for (const { spec, key } of items) {
11268
11644
  const estimate = estimates.get(key);
11645
+ if (spec.mirror) {
11646
+ try {
11647
+ await requireRevisionReady(spec, lock);
11648
+ await deriveMirror(spec, lock, lockPath, { onProgress: log2 });
11649
+ submitted++;
11650
+ } catch (err) {
11651
+ const message6 = err instanceof Error ? err.message : String(err);
11652
+ failed++;
11653
+ upsert(lock, key, {
11654
+ styleId: spec.styleId,
11655
+ assetId: spec.assetId,
11656
+ specHash: spec.specHash,
11657
+ generator: spec.generator,
11658
+ prompt: "",
11659
+ width: spec.width,
11660
+ height: spec.height,
11661
+ status: "failed",
11662
+ error: message6,
11663
+ outputs: lock.entries[key]?.outputs ?? [],
11664
+ cost: 0,
11665
+ costUnit: spec.costUnit,
11666
+ provider: provider.id
11667
+ });
11668
+ await saveLock(lockPath, lock);
11669
+ log2(` FAILED ${key}: ${message6}`);
11670
+ }
11671
+ continue;
11672
+ }
11269
11673
  await waitForSlot();
11270
11674
  const since = Date.now() - lastSubmitAt;
11271
11675
  if (since < spacing) await sleep2(spacing - since);
@@ -11391,12 +11795,12 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
11391
11795
 
11392
11796
  // src/pick/server.ts
11393
11797
  import { existsSync as existsSync21 } from "fs";
11394
- import { readFile as readFile19 } from "fs/promises";
11798
+ import { readFile as readFile20 } from "fs/promises";
11395
11799
 
11396
11800
  // src/pipeline/init.ts
11397
- import { readdir, readFile as readFile18, writeFile as writeFile11 } from "fs/promises";
11801
+ import { readdir, readFile as readFile19, writeFile as writeFile12 } from "fs/promises";
11398
11802
  import { existsSync as existsSync20 } from "fs";
11399
- import path28 from "path";
11803
+ import path29 from "path";
11400
11804
  function pngSize(buf) {
11401
11805
  if (buf.length < 24) return null;
11402
11806
  const isPng = buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834;
@@ -11410,7 +11814,7 @@ function slugify(value) {
11410
11814
  async function walk(dir, exclude) {
11411
11815
  const out = [];
11412
11816
  for (const entry of await readdir(dir, { withFileTypes: true })) {
11413
- const full = path28.join(dir, entry.name);
11817
+ const full = path29.join(dir, entry.name);
11414
11818
  if (exclude.some((x) => full.includes(x))) continue;
11415
11819
  if (entry.isDirectory()) out.push(...await walk(full, exclude));
11416
11820
  else if (entry.name.toLowerCase().endsWith(".png")) out.push(full);
@@ -11424,14 +11828,14 @@ async function scanAssets(root, opts = {}) {
11424
11828
  const skipped = [];
11425
11829
  const seen = /* @__PURE__ */ new Map();
11426
11830
  for (const file of files.sort()) {
11427
- const size = pngSize(await readFile18(file));
11831
+ const size = pngSize(await readFile19(file));
11428
11832
  if (!size) {
11429
11833
  skipped.push(`${file} (not a readable PNG)`);
11430
11834
  continue;
11431
11835
  }
11432
- const rel = path28.relative(root, file);
11433
- const category = path28.dirname(rel) === "." ? "" : path28.dirname(rel);
11434
- let id = slugify(path28.basename(rel));
11836
+ const rel = path29.relative(root, file);
11837
+ const category = path29.dirname(rel) === "." ? "" : path29.dirname(rel);
11838
+ let id = slugify(path29.basename(rel));
11435
11839
  if (seen.has(id)) {
11436
11840
  const n = seen.get(id) + 1;
11437
11841
  seen.set(id, n);
@@ -11492,7 +11896,7 @@ async function writeManifestFile(target, manifest) {
11492
11896
  if (existsSync20(target)) {
11493
11897
  throw new Error(`${target} already exists; refusing to overwrite an existing manifest.`);
11494
11898
  }
11495
- await writeFile11(target, JSON.stringify(manifest, null, 2) + "\n");
11899
+ await writeFile12(target, JSON.stringify(manifest, null, 2) + "\n");
11496
11900
  }
11497
11901
 
11498
11902
  // src/pick/sheet.ts
@@ -11898,7 +12302,7 @@ async function currentArt(entry, spec) {
11898
12302
  let width = entry.width;
11899
12303
  let height = entry.height;
11900
12304
  if (contentType === "image/png") {
11901
- const size = pngSize(await readFile19(file));
12305
+ const size = pngSize(await readFile20(file));
11902
12306
  if (size) ({ width, height } = size);
11903
12307
  }
11904
12308
  return { path: file, contentType, width, height };
@@ -12391,15 +12795,15 @@ function createGenerateHandlers(opts) {
12391
12795
  }
12392
12796
 
12393
12797
  // src/gallery/editor.ts
12394
- import { readFile as readFile21 } from "fs/promises";
12395
- import path30 from "path";
12798
+ import { readFile as readFile22 } from "fs/promises";
12799
+ import path31 from "path";
12396
12800
 
12397
12801
  // src/editor/install.ts
12398
12802
  import { createHash as createHash4 } from "crypto";
12399
12803
  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";
12804
+ import { mkdir as mkdir10, readFile as readFile21, rename as rename9, rm as rm9, stat as stat3, writeFile as writeFile13 } from "fs/promises";
12401
12805
  import os2 from "os";
12402
- import path29 from "path";
12806
+ import path30 from "path";
12403
12807
 
12404
12808
  // tools/pixelorama-bridge/pin.json
12405
12809
  var pin_default = {
@@ -12463,15 +12867,15 @@ var EditorInstallError = class extends Error {
12463
12867
  };
12464
12868
  function editorToolsRoot() {
12465
12869
  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");
12870
+ if (override) return path30.resolve(override);
12871
+ if (process.platform === "darwin") return path30.join(os2.homedir(), "Library", "Caches", "pixelkiln", "tools");
12468
12872
  if (process.platform === "win32") {
12469
- return path29.join(process.env.LOCALAPPDATA ?? path29.join(os2.homedir(), "AppData", "Local"), "pixelkiln", "tools");
12873
+ return path30.join(process.env.LOCALAPPDATA ?? path30.join(os2.homedir(), "AppData", "Local"), "pixelkiln", "tools");
12470
12874
  }
12471
- return path29.join(process.env.XDG_CACHE_HOME ?? path29.join(os2.homedir(), ".cache"), "pixelkiln", "tools");
12875
+ return path30.join(process.env.XDG_CACHE_HOME ?? path30.join(os2.homedir(), ".cache"), "pixelkiln", "tools");
12472
12876
  }
12473
12877
  function editorDir(pin = EDITOR_PIN, root = editorToolsRoot()) {
12474
- return path29.join(root, "pixelorama", pin.release ?? "unpublished");
12878
+ return path30.join(root, "pixelorama", pin.release ?? "unpublished");
12475
12879
  }
12476
12880
  function editorBaseUrl(pin = EDITOR_PIN) {
12477
12881
  const override = process.env.PIXELKILN_EDITOR_URL?.trim();
@@ -12481,7 +12885,7 @@ function editorBaseUrl(pin = EDITOR_PIN) {
12481
12885
  }
12482
12886
  async function sha256Of(file) {
12483
12887
  try {
12484
- return createHash4("sha256").update(await readFile20(file)).digest("hex");
12888
+ return createHash4("sha256").update(await readFile21(file)).digest("hex");
12485
12889
  } catch {
12486
12890
  return null;
12487
12891
  }
@@ -12494,7 +12898,7 @@ async function editorStatus(opts = {}) {
12494
12898
  let totalBytes = 0;
12495
12899
  for (const [name, expected] of Object.entries(pin.files)) {
12496
12900
  totalBytes += expected.bytes;
12497
- const file = path29.join(dir, name);
12901
+ const file = path30.join(dir, name);
12498
12902
  const info = existsSync22(file) ? await stat3(file).catch(() => null) : null;
12499
12903
  if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
12500
12904
  installedBytes += expected.bytes;
@@ -12524,7 +12928,7 @@ async function installEditor(opts = {}) {
12524
12928
  );
12525
12929
  }
12526
12930
  if (!baseUrl) throw new EditorInstallError("no download location for the editor build");
12527
- await mkdir9(dir, { recursive: true });
12931
+ await mkdir10(dir, { recursive: true });
12528
12932
  const downloaded = [];
12529
12933
  const skipped = Object.keys(pin.files).filter((name) => !before.missing.includes(name));
12530
12934
  let fetchedBytes = before.installedBytes;
@@ -12552,11 +12956,11 @@ async function installEditor(opts = {}) {
12552
12956
  if (digest2 !== expected.sha256) {
12553
12957
  throw new EditorInstallError(`${name}: sha256 ${digest2.slice(0, 12)}\u2026 does not match the pinned ${expected.sha256.slice(0, 12)}\u2026`, name);
12554
12958
  }
12555
- const final = path29.join(dir, name);
12959
+ const final = path30.join(dir, name);
12556
12960
  const tmp = `${final}.${process.pid}.part`;
12557
12961
  try {
12558
- await writeFile12(tmp, bytes);
12559
- await rename8(tmp, final);
12962
+ await writeFile13(tmp, bytes);
12963
+ await rename9(tmp, final);
12560
12964
  } finally {
12561
12965
  await rm9(tmp, { force: true });
12562
12966
  }
@@ -12598,7 +13002,7 @@ var EDITOR_CONTENT_TYPES = {
12598
13002
  ".json": "application/json; charset=utf-8"
12599
13003
  };
12600
13004
  function editorContentType(name) {
12601
- return EDITOR_CONTENT_TYPES[path29.extname(name).toLowerCase()] ?? null;
13005
+ return EDITOR_CONTENT_TYPES[path30.extname(name).toLowerCase()] ?? null;
12602
13006
  }
12603
13007
 
12604
13008
  // src/gallery/editor.ts
@@ -12674,7 +13078,7 @@ function createGalleryEditorHandlers(opts = {}) {
12674
13078
  if (!status.installed) return null;
12675
13079
  let bytes;
12676
13080
  try {
12677
- bytes = await readFile21(path30.join(dir, name));
13081
+ bytes = await readFile22(path31.join(dir, name));
12678
13082
  } catch {
12679
13083
  verified = null;
12680
13084
  return null;
@@ -12714,13 +13118,13 @@ async function serveUntilStopped(initial, reload, args, access2) {
12714
13118
  };
12715
13119
  const loadProject = async (project) => {
12716
13120
  const ctx = await access2.loadProject(project);
12717
- const dir = path31.dirname(ctx.loaded.path);
13121
+ const dir = path32.dirname(ctx.loaded.path);
12718
13122
  const declared = readEnvFiles(dir);
12719
13123
  const credentialNames = new Set(availableProviders().flatMap((id) => providerCredentialEnvs(providerFactory(id))));
12720
13124
  for (const [name, value] of Object.entries(declared)) {
12721
13125
  if (credentialNames.has(name) && name in process.env && process.env[name] !== value) {
12722
13126
  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.`
13127
+ `${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
13128
  );
12725
13129
  }
12726
13130
  }
@@ -12765,7 +13169,7 @@ async function serveUntilStopped(initial, reload, args, access2) {
12765
13169
  }
12766
13170
  async function runGallery(args) {
12767
13171
  if (args.workspace) {
12768
- const workspacePath = path31.resolve(args.workspace);
13172
+ const workspacePath = path32.resolve(args.workspace);
12769
13173
  if (!existsSync23(workspacePath)) {
12770
13174
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
12771
13175
  }
@@ -12786,7 +13190,7 @@ async function runGallery(args) {
12786
13190
  const registered = async (projectId) => {
12787
13191
  const project = (await loadWorkspace(workspacePath)).projects.find((candidate) => candidate.id === projectId);
12788
13192
  if (!projectId || !project) throw new Error(`unknown workspace project "${projectId ?? ""}"`);
12789
- return resolveProject(path31.dirname(workspacePath), project);
13193
+ return resolveProject(path32.dirname(workspacePath), project);
12790
13194
  };
12791
13195
  await serveUntilStopped(initial, build2, args, {
12792
13196
  manifestFor: async (projectId) => (await registered(projectId)).manifestPath,
@@ -12811,7 +13215,7 @@ async function runGallery(args) {
12811
13215
  return buildGallerySnapshot({ ...fresh, filter });
12812
13216
  };
12813
13217
  await serveUntilStopped(await build(), reload, args, {
12814
- manifestFor: () => path31.resolve(args.manifest),
13218
+ manifestFor: () => path32.resolve(args.manifest),
12815
13219
  loadProject: () => openProject(args.manifest, { lockPath: args.lock, env: false })
12816
13220
  });
12817
13221
  }
@@ -13054,7 +13458,7 @@ async function runLifecycle(args, wave, spent) {
13054
13458
  log(` Packaging stays blocked until each refined PNG has a current human approval.`);
13055
13459
  }
13056
13460
  }
13057
- return args.command === "gen" && submitted > 0 && total.downloaded > 0 && !total.failed && specs.some((spec) => spec.character?.parentSpec || spec.revision);
13461
+ return args.command === "gen" && submitted > 0 && total.downloaded > 0 && !total.failed && specs.some((spec) => spec.character?.parentSpec || spec.revision || spec.mirror);
13058
13462
  }
13059
13463
  return false;
13060
13464
  }
@@ -13120,14 +13524,14 @@ import { existsSync as existsSync25 } from "fs";
13120
13524
 
13121
13525
  // src/pipeline/cache-health.ts
13122
13526
  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";
13527
+ import { readFile as readFile23, readdir as readdir2, rm as rm10 } from "fs/promises";
13528
+ import path33 from "path";
13125
13529
  async function inspectCaches(lock, lockPath, options = {}) {
13126
13530
  if (options.prune && !existsSync24(lockPath)) {
13127
- throw new Error(`Refusing to prune without an existing lockfile at ${path32.resolve(lockPath)}`);
13531
+ throw new Error(`Refusing to prune without an existing lockfile at ${path33.resolve(lockPath)}`);
13128
13532
  }
13129
- const contentDir = path32.resolve(path32.dirname(lockPath), ".pixelkiln", "cache");
13130
- const remotePath = path32.resolve(cachePathFor(lockPath));
13533
+ const contentDir = path33.resolve(path33.dirname(lockPath), ".pixelkiln", "cache");
13534
+ const remotePath = path33.resolve(cachePathFor(lockPath));
13131
13535
  const referenced = referencedHashes(lock);
13132
13536
  let content = await inspectContentCache(contentDir, referenced);
13133
13537
  let remoteHashes = await inspectRemoteHashCache(remotePath);
@@ -13139,7 +13543,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
13139
13543
  ]);
13140
13544
  for (const name of names) {
13141
13545
  try {
13142
- await rm10(path32.join(contentDir, name), { force: true });
13546
+ await rm10(path33.join(contentDir, name), { force: true });
13143
13547
  removed.contentFiles++;
13144
13548
  } catch {
13145
13549
  }
@@ -13148,7 +13552,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
13148
13552
  await saveCache(remotePath, { version: 1, hashes: {} });
13149
13553
  removed.resetRemoteHashCache = true;
13150
13554
  } else if (remoteHashes.invalidIds.length) {
13151
- const cache = parseCache(JSON.parse(await readFile22(remotePath, "utf8")));
13555
+ const cache = parseCache(JSON.parse(await readFile23(remotePath, "utf8")));
13152
13556
  for (const id of remoteHashes.invalidIds) delete cache.hashes[id];
13153
13557
  removed.remoteHashEntries = remoteHashes.invalidIds.length;
13154
13558
  await saveCache(remotePath, cache);
@@ -13187,12 +13591,12 @@ async function inspectContentCache(contentDir, referenced) {
13187
13591
  continue;
13188
13592
  }
13189
13593
  report.files++;
13190
- const file = path32.join(contentDir, entry.name);
13594
+ const file = path33.join(contentDir, entry.name);
13191
13595
  const mediaType = mediaTypeFromExtension(entry.name);
13192
13596
  const expected = mediaType ? entry.name.slice(0, -4) : "";
13193
13597
  let bytes;
13194
13598
  try {
13195
- bytes = await readFile22(file);
13599
+ bytes = await readFile23(file);
13196
13600
  report.bytes += bytes.length;
13197
13601
  } catch (err) {
13198
13602
  report.invalid.push({
@@ -13239,7 +13643,7 @@ async function inspectRemoteHashCache(remotePath) {
13239
13643
  if (!report.exists) return report;
13240
13644
  let cache;
13241
13645
  try {
13242
- cache = parseCache(JSON.parse(await readFile22(remotePath, "utf8")));
13646
+ cache = parseCache(JSON.parse(await readFile23(remotePath, "utf8")));
13243
13647
  } catch (err) {
13244
13648
  report.error = err instanceof Error ? err.message : String(err);
13245
13649
  return report;
@@ -13358,42 +13762,42 @@ This drops their provenance from the lockfile. The art on disk is`);
13358
13762
  }
13359
13763
 
13360
13764
  // src/cli/commands/init.ts
13361
- import path33 from "path";
13765
+ import path34 from "path";
13362
13766
  import { existsSync as existsSync26 } from "fs";
13363
13767
  async function runInit(args) {
13364
13768
  if (!args.from) throw new Error("init needs --from <dir> pointing at your existing PNGs.");
13365
- const root = path33.resolve(args.from);
13769
+ const root = path34.resolve(args.from);
13366
13770
  if (!existsSync26(root)) throw new Error(`No directory at ${root}`);
13367
13771
  const generator = args.generator ?? "map";
13368
13772
  if (generator !== "1dir" && generator !== "map") {
13369
13773
  throw new Error(`--generator must be "1dir" or "map", got "${args.generator}".`);
13370
13774
  }
13371
- const target = path33.resolve(args.out ?? "pixelkiln.manifest.json");
13775
+ const target = path34.resolve(args.out ?? "pixelkiln.manifest.json");
13372
13776
  const { assets, skipped } = await scanAssets(root, { exclude: args.exclude });
13373
13777
  if (!assets.length) throw new Error(`No PNGs found under ${root}`);
13374
13778
  const manifest = buildManifest(
13375
- args.name ?? path33.basename(path33.dirname(target)),
13779
+ args.name ?? path34.basename(path34.dirname(target)),
13376
13780
  args.styles[0] ?? "base",
13377
13781
  generator,
13378
- path33.relative(path33.dirname(target), root) || ".",
13782
+ path34.relative(path34.dirname(target), root) || ".",
13379
13783
  assets
13380
13784
  );
13381
13785
  await writeManifestFile(target, manifest);
13382
- log(` scanned ${assets.length} PNG(s) under ${path33.relative(process.cwd(), root)}`);
13786
+ log(` scanned ${assets.length} PNG(s) under ${path34.relative(process.cwd(), root)}`);
13383
13787
  if (skipped.length) log(` skipped ${skipped.length} unreadable file(s)`);
13384
- log(` wrote ${path33.relative(process.cwd(), target)}`);
13788
+ log(` wrote ${path34.relative(process.cwd(), target)}`);
13385
13789
  log(`
13386
13790
  Prompts are intentionally empty. To recover the real ones from your`);
13387
13791
  log(` PixelLab account instead of inventing them:`);
13388
13792
  log(`
13389
- pixelkiln adopt --manifest ${path33.relative(process.cwd(), target)} --write-prompts
13793
+ pixelkiln adopt --manifest ${path34.relative(process.cwd(), target)} --write-prompts
13390
13794
  `);
13391
13795
  }
13392
13796
 
13393
13797
  // src/pipeline/doctor.ts
13394
13798
  import { constants, existsSync as existsSync27 } from "fs";
13395
13799
  import { access, stat as stat4 } from "fs/promises";
13396
- import path34 from "path";
13800
+ import path35 from "path";
13397
13801
  async function doctor(loaded, specs, lock, lockPath, opts = {}) {
13398
13802
  const checks = [];
13399
13803
  const add = (id, level, message6) => checks.push({ id, level, message: message6 });
@@ -13404,8 +13808,8 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
13404
13808
  );
13405
13809
  add("lock", "ok", `${Object.keys(lock.entries).length} valid v2 lock entr(ies)`);
13406
13810
  const dirs = [...new Set(specs.flatMap((spec) => [
13407
- path34.dirname(spec.outFile),
13408
- ...spec.quality ? [path34.dirname(spec.quality.outFile)] : []
13811
+ path35.dirname(spec.outFile),
13812
+ ...spec.quality ? [path35.dirname(spec.quality.outFile)] : []
13409
13813
  ]))];
13410
13814
  const unwritable = [];
13411
13815
  for (const dir of dirs) {
@@ -13428,7 +13832,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
13428
13832
  for (let index = 0; index < entry.outputs.length; index++) {
13429
13833
  const output = entry.outputs[index];
13430
13834
  const spec = specByKey.get(key);
13431
- const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, path34.dirname(lockPath));
13835
+ const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, path35.dirname(lockPath));
13432
13836
  const owner = outputOwners.get(absolute);
13433
13837
  if (owner && owner !== key) duplicateOutputs.push(`${output.path} (${owner}, ${key})`);
13434
13838
  outputOwners.set(absolute, key);
@@ -13443,7 +13847,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
13443
13847
  } else {
13444
13848
  add("lock-outputs", "ok", `${outputOwners.size} recorded output path(s) are uniquely owned`);
13445
13849
  }
13446
- const cacheDir = path34.join(path34.dirname(lockPath), ".pixelkiln", "cache");
13850
+ const cacheDir = path35.join(path35.dirname(lockPath), ".pixelkiln", "cache");
13447
13851
  const stranded = [];
13448
13852
  const invalidState = [];
13449
13853
  const staleMapUrls = [];
@@ -13459,7 +13863,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
13459
13863
  const hasSource = Boolean(entry.sourceUrl || entry.sourceUrls?.length);
13460
13864
  let hasCache = false;
13461
13865
  for (const output of entry.outputs) {
13462
- const cached = path34.join(
13866
+ const cached = path35.join(
13463
13867
  cacheDir,
13464
13868
  cacheFileName(output.sha256, output.mediaType ?? MediaType.PNG)
13465
13869
  );
@@ -13559,16 +13963,16 @@ async function nearestExistingDirectory(start) {
13559
13963
  if ((await stat4(current)).isDirectory()) return current;
13560
13964
  } catch {
13561
13965
  }
13562
- const parent = path34.dirname(current);
13966
+ const parent = path35.dirname(current);
13563
13967
  if (parent === current) return current;
13564
13968
  current = parent;
13565
13969
  }
13566
13970
  }
13567
13971
 
13568
13972
  // src/pipeline/audit.ts
13569
- import { readFile as readFile23 } from "fs/promises";
13973
+ import { readFile as readFile24 } from "fs/promises";
13570
13974
  import { existsSync as existsSync28 } from "fs";
13571
- import path35 from "path";
13975
+ import path36 from "path";
13572
13976
  function paletteDistance(asset, reference) {
13573
13977
  if (!asset.length || !reference.length) return 0;
13574
13978
  let total = 0;
@@ -13609,7 +14013,7 @@ async function auditStyle(loaded, specs, styleId, lock) {
13609
14013
  continue;
13610
14014
  }
13611
14015
  try {
13612
- const png = decodePng(await readFile23(output.absolutePath));
14016
+ const png = decodePng(await readFile24(output.absolutePath));
13613
14017
  const palette = extractPalette(png, 12);
13614
14018
  assets.push({
13615
14019
  assetId: spec.assetId,
@@ -13633,10 +14037,10 @@ async function auditStyle(loaded, specs, styleId, lock) {
13633
14037
  let referenceFromStyleImages = false;
13634
14038
  const refPalettes = [];
13635
14039
  for (const img of style.styleImages) {
13636
- const abs = path35.resolve(loaded.root, img.path);
14040
+ const abs = path36.resolve(loaded.root, img.path);
13637
14041
  if (!existsSync28(abs)) continue;
13638
14042
  try {
13639
- refPalettes.push(extractPalette(decodePng(await readFile23(abs)), 12));
14043
+ refPalettes.push(extractPalette(decodePng(await readFile24(abs)), 12));
13640
14044
  } catch {
13641
14045
  }
13642
14046
  }
@@ -13856,24 +14260,24 @@ async function runAudit(args) {
13856
14260
  }
13857
14261
 
13858
14262
  // src/cli/commands/pack.ts
13859
- import path37 from "path";
13860
- import { readFile as readFile24 } from "fs/promises";
14263
+ import path38 from "path";
14264
+ import { readFile as readFile25 } from "fs/promises";
13861
14265
 
13862
14266
  // src/pipeline/pack.ts
13863
14267
  import { readFileSync as readFileSync5 } from "fs";
13864
- import path36 from "path";
14268
+ import path37 from "path";
13865
14269
  var DEFAULT_FRAME_SET_FPS = 12;
13866
14270
  function resolvePackInputs(raw, inputsFilePath) {
13867
14271
  if (!Array.isArray(raw) || !raw.length) {
13868
14272
  throw new Error("--inputs must be a non-empty JSON array of { id, path }");
13869
14273
  }
13870
- const baseDir = path36.dirname(path36.resolve(inputsFilePath));
14274
+ const baseDir = path37.dirname(path37.resolve(inputsFilePath));
13871
14275
  return raw.map((entry, i) => {
13872
14276
  const e = entry;
13873
14277
  if (typeof e.id !== "string" || typeof e.path !== "string") {
13874
14278
  throw new Error(`--inputs[${i}] needs string "id" and "path"`);
13875
14279
  }
13876
- return { id: e.id, path: path36.resolve(baseDir, e.path) };
14280
+ return { id: e.id, path: path37.resolve(baseDir, e.path) };
13877
14281
  });
13878
14282
  }
13879
14283
  function packSprites(inputs, options = {}) {
@@ -13945,7 +14349,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
13945
14349
  }
13946
14350
  const inputs2 = Object.entries(options.sourceOverrides).map(([id, source]) => ({
13947
14351
  id,
13948
- path: path36.resolve(source)
14352
+ path: path37.resolve(source)
13949
14353
  }));
13950
14354
  if (!inputs2.length) throw new Error(`No approved quality outputs for style "${styleId}".`);
13951
14355
  const packed2 = packSprites(inputs2, options);
@@ -13969,7 +14373,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
13969
14373
  let outputs = resolveEntryOutputs(entry, id, manifestDir);
13970
14374
  const source = sources[id];
13971
14375
  if (source && outputs.length <= 1) {
13972
- inputs.push({ id, path: path36.resolve(manifestDir, source) });
14376
+ inputs.push({ id, path: path37.resolve(manifestDir, source) });
13973
14377
  continue;
13974
14378
  }
13975
14379
  if (source && sourceIsStem(source, entry, manifestDir)) {
@@ -14003,7 +14407,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
14003
14407
  }
14004
14408
  }
14005
14409
  for (const [id, source] of Object.entries(sources)) {
14006
- if (!locked.has(id)) inputs.push({ id, path: path36.resolve(manifestDir, source) });
14410
+ if (!locked.has(id)) inputs.push({ id, path: path37.resolve(manifestDir, source) });
14007
14411
  }
14008
14412
  if (!inputs.length) {
14009
14413
  throw new Error(`No readable sprites for style "${styleId}"; ${noOutput.length} skipped.`);
@@ -14111,7 +14515,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
14111
14515
  const source = sources[id];
14112
14516
  const entry = lock.entries[`${styleId}/${id}`];
14113
14517
  if (source && !(entry && sourceIsStem(source, entry, manifestDir))) {
14114
- placements.push({ id, path: path36.resolve(manifestDir, source), cell });
14518
+ placements.push({ id, path: path37.resolve(manifestDir, source), cell });
14115
14519
  continue;
14116
14520
  }
14117
14521
  if (source && entry) {
@@ -14144,7 +14548,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
14144
14548
  `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
14549
  );
14146
14550
  }
14147
- const basePng = mount.base ? readFileSync5(path36.resolve(manifestDir, mount.base)) : void 0;
14551
+ const basePng = mount.base ? readFileSync5(path37.resolve(manifestDir, mount.base)) : void 0;
14148
14552
  const mounted = mountSprites(placements, {
14149
14553
  cellWidth: mount.cellWidth,
14150
14554
  cellHeight: mount.cellHeight,
@@ -14154,7 +14558,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
14154
14558
  ...mounted,
14155
14559
  sources: mount.base ? [{
14156
14560
  id: "$base",
14157
- path: path36.resolve(manifestDir, mount.base),
14561
+ path: path37.resolve(manifestDir, mount.base),
14158
14562
  sha256: sha256(basePng),
14159
14563
  included: true
14160
14564
  }, ...mounted.sources] : mounted.sources,
@@ -14521,12 +14925,12 @@ async function runPack(args) {
14521
14925
  if (args.primaryOnly || args.outputRoles.length) {
14522
14926
  throw new Error("--primary-only and --output-role require manifest-driven pack");
14523
14927
  }
14524
- const raw = JSON.parse(await readFile24(path37.resolve(args.inputs), "utf8"));
14928
+ const raw = JSON.parse(await readFile25(path38.resolve(args.inputs), "utf8"));
14525
14929
  const inputs = resolvePackInputs(raw, args.inputs);
14526
14930
  const { png, atlas, skipped, sources } = packSprites(inputs, { columns: args.columns });
14527
14931
  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`) });
14932
+ const base = path38.resolve(args.out.replace(/\.(?:png|json|tres)$/i, ""));
14933
+ const { extension, document } = renderSheetDocument(format, atlas, { imageName: path38.basename(`${base}.png`) });
14530
14934
  const outputs = [
14531
14935
  { path: `${base}.png`, data: png },
14532
14936
  { path: `${base}${extension}`, data: document }
@@ -14539,7 +14943,7 @@ async function runPack(args) {
14539
14943
  log(
14540
14944
  ` ${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
14945
  );
14542
- log(` ${path37.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
14946
+ log(` ${path38.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
14543
14947
  for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
14544
14948
  return;
14545
14949
  }
@@ -14547,7 +14951,7 @@ async function runPack(args) {
14547
14951
  if (args.primaryOnly && args.outputRoles.length) {
14548
14952
  throw new Error("pack accepts either --primary-only or --output-role, not both");
14549
14953
  }
14550
- const manifestDir = path37.dirname(path37.resolve(args.manifest));
14954
+ const manifestDir = path38.dirname(path38.resolve(args.manifest));
14551
14955
  const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
14552
14956
  for (const styleId of styleIds) {
14553
14957
  const packagingSpecs = await resolveSpecs(loaded, { styles: [styleId] });
@@ -14560,9 +14964,9 @@ async function runPack(args) {
14560
14964
  sources: manifestSources(loaded.manifest, styleId)
14561
14965
  });
14562
14966
  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`);
14967
+ const base = args.out ? path38.resolve(args.out.replace(/\.(?:png|json|tres)$/i, "")) : path38.resolve(manifestDir, style.outDir, `${styleId}-sheet`);
14564
14968
  const format = sheetFormat(args);
14565
- const { extension, document } = renderSheetDocument(format, atlas, { imageName: path37.basename(`${base}.png`) });
14969
+ const { extension, document } = renderSheetDocument(format, atlas, { imageName: path38.basename(`${base}.png`) });
14566
14970
  const outputs = [
14567
14971
  { path: `${base}.png`, data: png },
14568
14972
  { path: `${base}${extension}`, data: document }
@@ -14593,13 +14997,13 @@ async function runPack(args) {
14593
14997
  log(
14594
14998
  ` ${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
14999
  );
14596
- log(` ${path37.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
15000
+ log(` ${path38.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
14597
15001
  for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
14598
15002
  }
14599
15003
  }
14600
15004
  async function runMount(args) {
14601
15005
  const { loaded, lock } = await openProject2(args);
14602
- const manifestDir = path37.dirname(path37.resolve(args.manifest));
15006
+ const manifestDir = path38.dirname(path38.resolve(args.manifest));
14603
15007
  const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
14604
15008
  for (const styleId of styleIds) {
14605
15009
  const style = loaded.manifest.styles[styleId];
@@ -14638,9 +15042,9 @@ async function runMount(args) {
14638
15042
  sources,
14639
15043
  outputRoles
14640
15044
  );
14641
- const out = path37.resolve(manifestDir, style.mount.out);
15045
+ const out = path38.resolve(manifestDir, style.mount.out);
14642
15046
  const format = sheetFormat(args);
14643
- const { extension, document } = renderSheetDocument(format, atlas, { imageName: path37.basename(out) });
15047
+ const { extension, document } = renderSheetDocument(format, atlas, { imageName: path38.basename(out) });
14644
15048
  const metadata = out.replace(/\.png$/, "") + extension;
14645
15049
  const companion = out.replace(/\.png$/, "") + ".pixelkiln.json";
14646
15050
  const outputs = [
@@ -14660,7 +15064,7 @@ async function runMount(args) {
14660
15064
  await provenanceFile("$lock", args.lock),
14661
15065
  ...qualityRecords,
14662
15066
  ...artifactSources.filter(
14663
- (source) => source.id !== "$base" || path37.resolve(source.path) !== out
15067
+ (source) => source.id !== "$base" || path38.resolve(source.path) !== out
14664
15068
  )
14665
15069
  ],
14666
15070
  options: {
@@ -14674,14 +15078,14 @@ async function runMount(args) {
14674
15078
  log(
14675
15079
  ` ${styleId}: ${atlas.frames.length} cell(s) into ${atlas.sheet.width}x${atlas.sheet.height}` + (overBase ? ` over ${style.mount.base}` : " (new sheet)")
14676
15080
  );
14677
- log(` ${path37.relative(process.cwd(), out)} + atlas/provenance JSON`);
15081
+ log(` ${path38.relative(process.cwd(), out)} + atlas/provenance JSON`);
14678
15082
  for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
14679
15083
  }
14680
15084
  }
14681
15085
  async function runExport(args) {
14682
15086
  const { loaded, specs, lock } = await openProject2(args);
14683
15087
  const format = args.format ?? "generic";
14684
- const manifestDir = path37.dirname(path37.resolve(args.manifest));
15088
+ const manifestDir = path38.dirname(path38.resolve(args.manifest));
14685
15089
  const selected = specs.filter((spec) => {
14686
15090
  if (spec.generator !== "tiles") return false;
14687
15091
  if (args.styles.length && !args.styles.includes(spec.styleId)) return false;
@@ -14697,12 +15101,12 @@ async function runExport(args) {
14697
15101
  for (const spec of selected) {
14698
15102
  const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
14699
15103
  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;
15104
+ const defaultBase = path38.resolve(manifestDir, style.outDir, `${spec.assetId}-tileset`);
15105
+ const base = args.out ? path38.resolve(args.out.replace(/\.(?:png|json|tsj|tres)$/i, "")) : defaultBase;
14702
15106
  const result = exportTileset(entry, spec, {
14703
15107
  format,
14704
15108
  manifestDir,
14705
- imageName: path37.basename(`${base}.png`),
15109
+ imageName: path38.basename(`${base}.png`),
14706
15110
  columns: args.columns
14707
15111
  });
14708
15112
  const outputs = [
@@ -14720,7 +15124,7 @@ async function runExport(args) {
14720
15124
  asset: spec.assetId,
14721
15125
  columns: args.columns ?? null,
14722
15126
  format,
14723
- image: path37.basename(`${base}.png`),
15127
+ image: path38.basename(`${base}.png`),
14724
15128
  providerRules: result.generic.providerRules,
14725
15129
  style: spec.styleId,
14726
15130
  tileType: spec.tileType ?? null
@@ -14730,7 +15134,7 @@ async function runExport(args) {
14730
15134
  ` ${spec.styleId}/${spec.assetId}: ${result.generic.tiles.length} tile(s), ${result.generic.sheet.width}x${result.generic.sheet.height} (${format})`
14731
15135
  );
14732
15136
  log(
14733
- ` ${path37.relative(process.cwd(), base)}.png + ${path37.basename(base)}${result.extension} + .pixelkiln.json`
15137
+ ` ${path38.relative(process.cwd(), base)}.png + ${path38.basename(base)}${result.extension} + .pixelkiln.json`
14734
15138
  );
14735
15139
  }
14736
15140
  }
@@ -14743,19 +15147,19 @@ function sheetFormat(args) {
14743
15147
  }
14744
15148
 
14745
15149
  // src/cli/commands/quality.ts
14746
- import path39 from "path";
14747
- import { readFile as readFile26 } from "fs/promises";
15150
+ import path40 from "path";
15151
+ import { readFile as readFile27 } from "fs/promises";
14748
15152
 
14749
15153
  // src/pipeline/quality-regression.ts
14750
- import path38 from "path";
15154
+ import path39 from "path";
14751
15155
  import { existsSync as existsSync29 } from "fs";
14752
- import { readFile as readFile25 } from "fs/promises";
15156
+ import { readFile as readFile26 } from "fs/promises";
14753
15157
  import { z as z9 } from "zod";
14754
15158
  var SHA256_RE = /^[0-9a-f]{64}$/;
14755
15159
  var HEX_RE = /^#[0-9a-f]{6}$/;
14756
15160
  var MAX_REDMEAN_DISTANCE = 765;
14757
15161
  var QualityPathSchema = z9.string().min(1).refine(
14758
- (value) => !path38.posix.isAbsolute(value) && !value.includes("\\"),
15162
+ (value) => !path39.posix.isAbsolute(value) && !value.includes("\\"),
14759
15163
  "expected a portable relative path"
14760
15164
  );
14761
15165
  var DEFAULT_QUALITY_TOLERANCES = {
@@ -14902,10 +15306,10 @@ function measureDecodedImageQuality(png, digest2) {
14902
15306
  };
14903
15307
  }
14904
15308
  async function measureImageQuality(file) {
14905
- const absolute = path38.resolve(file);
15309
+ const absolute = path39.resolve(file);
14906
15310
  let bytes;
14907
15311
  try {
14908
- bytes = await readFile25(absolute);
15312
+ bytes = await readFile26(absolute);
14909
15313
  } catch (error) {
14910
15314
  throw new Error(`Cannot read quality image ${absolute}: ${message4(error)}`, { cause: error });
14911
15315
  }
@@ -14924,11 +15328,11 @@ function resolveQualityInputs(raw, inputsFilePath) {
14924
15328
  }
14925
15329
  const ids = parsed.data.map((entry) => entry.id);
14926
15330
  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));
15331
+ const root = path39.dirname(path39.resolve(inputsFilePath));
14928
15332
  return parsed.data.map((entry) => ({
14929
15333
  id: entry.id,
14930
- file: path38.resolve(root, entry.path),
14931
- ...entry.record ? { record: path38.resolve(root, entry.record) } : {},
15334
+ file: path39.resolve(root, entry.path),
15335
+ ...entry.record ? { record: path39.resolve(root, entry.record) } : {},
14932
15336
  tolerances: QualityToleranceSchema.parse({
14933
15337
  ...DEFAULT_QUALITY_TOLERANCES,
14934
15338
  ...entry.tolerances
@@ -14936,7 +15340,7 @@ function resolveQualityInputs(raw, inputsFilePath) {
14936
15340
  }));
14937
15341
  }
14938
15342
  function portableRelative2(from, to) {
14939
- return path38.relative(from, path38.resolve(to)).split(path38.sep).join("/") || ".";
15343
+ return path39.relative(from, path39.resolve(to)).split(path39.sep).join("/") || ".";
14940
15344
  }
14941
15345
  async function verifyRecordForImage(recordPath, imagePath) {
14942
15346
  const verification = await verifyArtifactBundle(recordPath);
@@ -14951,20 +15355,20 @@ async function verifyRecordForImage(recordPath, imagePath) {
14951
15355
  if (record.kind !== "refine" || options === null || typeof options !== "object" || options.schema !== "pixelkiln-quality") {
14952
15356
  throw new Error(`Quality record ${recordPath} is not a PixelKiln refinement record.`);
14953
15357
  }
14954
- const outputs = record.outputs.map((output) => path38.resolve(path38.dirname(recordPath), output.path));
14955
- if (!outputs.includes(path38.resolve(imagePath))) {
15358
+ const outputs = record.outputs.map((output) => path39.resolve(path39.dirname(recordPath), output.path));
15359
+ if (!outputs.includes(path39.resolve(imagePath))) {
14956
15360
  throw new Error(`Quality record ${recordPath} does not own ${imagePath}.`);
14957
15361
  }
14958
15362
  }
14959
15363
  async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
14960
15364
  if (!inputs.length) throw new Error("Cannot snapshot an empty quality baseline.");
14961
- const absolute = path38.resolve(baselinePath);
15365
+ const absolute = path39.resolve(baselinePath);
14962
15366
  for (const input of inputs) {
14963
- if (path38.resolve(input.file) === absolute || input.record && path38.resolve(input.record) === absolute) {
15367
+ if (path39.resolve(input.file) === absolute || input.record && path39.resolve(input.record) === absolute) {
14964
15368
  throw new Error(`Quality baseline output would overwrite an input: ${absolute}.`);
14965
15369
  }
14966
15370
  }
14967
- const root = path38.dirname(absolute);
15371
+ const root = path39.dirname(absolute);
14968
15372
  const cases = [];
14969
15373
  for (const input of [...inputs].sort((a, b) => a.id.localeCompare(b.id))) {
14970
15374
  const expected = await measureImageQuality(input.file);
@@ -14992,7 +15396,7 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
14992
15396
  });
14993
15397
  const data = Buffer.from(JSON.stringify(baseline, null, 2) + "\n");
14994
15398
  if (existsSync29(absolute) && !options.force) {
14995
- const current = await readFile25(absolute);
15399
+ const current = await readFile26(absolute);
14996
15400
  if (!current.equals(data)) {
14997
15401
  throw new OverwriteRefusedError(
14998
15402
  `Quality baseline already exists with different content: ${absolute}. Pass --force to replace it.`,
@@ -15004,10 +15408,10 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
15004
15408
  return { baseline, path: absolute, changed: result.changed.length > 0 };
15005
15409
  }
15006
15410
  async function readQualityBaseline(baselinePath) {
15007
- const absolute = path38.resolve(baselinePath);
15411
+ const absolute = path39.resolve(baselinePath);
15008
15412
  let raw;
15009
15413
  try {
15010
- raw = JSON.parse(await readFile25(absolute, "utf8"));
15414
+ raw = JSON.parse(await readFile26(absolute, "utf8"));
15011
15415
  } catch (error) {
15012
15416
  throw new Error(`Could not read quality baseline ${absolute}: ${message4(error)}`, { cause: error });
15013
15417
  }
@@ -15077,12 +15481,12 @@ function regressionViolations(expected, actual, tolerances) {
15077
15481
  return { violations, warnings };
15078
15482
  }
15079
15483
  async function checkQualityBaseline(baselinePath) {
15080
- const absolute = path38.resolve(baselinePath);
15484
+ const absolute = path39.resolve(baselinePath);
15081
15485
  const baseline = await readQualityBaseline(absolute);
15082
- const root = path38.dirname(absolute);
15486
+ const root = path39.dirname(absolute);
15083
15487
  const cases = [];
15084
15488
  for (const entry of baseline.cases) {
15085
- const file = path38.resolve(root, entry.file);
15489
+ const file = path39.resolve(root, entry.file);
15086
15490
  let actual = null;
15087
15491
  const violations = [];
15088
15492
  const warnings = [];
@@ -15095,7 +15499,7 @@ async function checkQualityBaseline(baselinePath) {
15095
15499
  violations.push(message4(error));
15096
15500
  }
15097
15501
  if (entry.record) {
15098
- const recordPath = path38.resolve(root, entry.record.path);
15502
+ const recordPath = path39.resolve(root, entry.record.path);
15099
15503
  try {
15100
15504
  const recordHash = await sha256File(recordPath);
15101
15505
  if (recordHash !== entry.record.sha256) violations.push("quality record hash changed");
@@ -15137,15 +15541,15 @@ async function runQuality(args) {
15137
15541
  if (!args.out) throw new Error("quality snapshot needs --out <pixelkiln.quality.json>.");
15138
15542
  let raw;
15139
15543
  try {
15140
- raw = JSON.parse(await readFile26(path39.resolve(args.inputs), "utf8"));
15544
+ raw = JSON.parse(await readFile27(path40.resolve(args.inputs), "utf8"));
15141
15545
  } catch (error) {
15142
15546
  throw new Error(
15143
- `Could not read quality inputs ${path39.resolve(args.inputs)}: ${error instanceof Error ? error.message : String(error)}`,
15547
+ `Could not read quality inputs ${path40.resolve(args.inputs)}: ${error instanceof Error ? error.message : String(error)}`,
15144
15548
  { cause: error }
15145
15549
  );
15146
15550
  }
15147
15551
  const inputs = resolveQualityInputs(raw, args.inputs);
15148
- if (path39.resolve(args.inputs) === path39.resolve(args.out)) {
15552
+ if (path40.resolve(args.inputs) === path40.resolve(args.out)) {
15149
15553
  throw new Error("quality snapshot --out must not overwrite its --inputs file.");
15150
15554
  }
15151
15555
  const result = await snapshotQualityBaseline(inputs, args.out, { force: args.force });
@@ -15158,7 +15562,7 @@ async function runQuality(args) {
15158
15562
  }, null, 2));
15159
15563
  } else {
15160
15564
  log(
15161
- ` ${result.changed ? "wrote" : "unchanged"} ${path39.relative(process.cwd(), result.path)} (${result.baseline.cases.length} case(s))`
15565
+ ` ${result.changed ? "wrote" : "unchanged"} ${path40.relative(process.cwd(), result.path)} (${result.baseline.cases.length} case(s))`
15162
15566
  );
15163
15567
  log(` Review the tolerances, commit the baseline, then run quality check in CI.`);
15164
15568
  }
@@ -15184,12 +15588,12 @@ async function runQuality(args) {
15184
15588
  }
15185
15589
 
15186
15590
  // src/cli/commands/recipe.ts
15187
- import path41 from "path";
15591
+ import path42 from "path";
15188
15592
 
15189
15593
  // src/recipes.ts
15190
- import path40 from "path";
15594
+ import path41 from "path";
15191
15595
  import { existsSync as existsSync30 } from "fs";
15192
- import { readdir as readdir3, readFile as readFile27 } from "fs/promises";
15596
+ import { readdir as readdir3, readFile as readFile28 } from "fs/promises";
15193
15597
  import { fileURLToPath } from "url";
15194
15598
  import { z as z10 } from "zod";
15195
15599
  var SHA256_RE2 = /^[0-9a-f]{64}$/;
@@ -15198,7 +15602,7 @@ var VERSION_RE = /^\d+\.\d+\.\d+$/;
15198
15602
  var RECIPE_FILE = "pixelkiln.recipe.json";
15199
15603
  var RECIPE_DIR_TOKEN = "{{recipeDir}}";
15200
15604
  var PortablePathSchema = z10.string().min(1).refine((value) => {
15201
- if (value.includes("\\") || path40.posix.isAbsolute(value)) return false;
15605
+ if (value.includes("\\") || path41.posix.isAbsolute(value)) return false;
15202
15606
  const parts = value.split("/");
15203
15607
  return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
15204
15608
  }, "expected a portable relative path without '.', '..', or backslashes");
@@ -15374,10 +15778,10 @@ function message5(error) {
15374
15778
  return error instanceof Error ? error.message : String(error);
15375
15779
  }
15376
15780
  async function readRecipeFile(recipePath, bundled) {
15377
- const absolute = path40.resolve(recipePath);
15781
+ const absolute = path41.resolve(recipePath);
15378
15782
  let raw;
15379
15783
  try {
15380
- raw = JSON.parse(await readFile27(absolute, "utf8"));
15784
+ raw = JSON.parse(await readFile28(absolute, "utf8"));
15381
15785
  } catch (error) {
15382
15786
  throw new Error(`Could not read recipe ${absolute}: ${message5(error)}`, { cause: error });
15383
15787
  }
@@ -15388,13 +15792,13 @@ async function readRecipeFile(recipePath, bundled) {
15388
15792
  ` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
15389
15793
  );
15390
15794
  }
15391
- return { recipe: parsed.data, authored: raw, path: absolute, dir: path40.dirname(absolute), bundled };
15795
+ return { recipe: parsed.data, authored: raw, path: absolute, dir: path41.dirname(absolute), bundled };
15392
15796
  }
15393
15797
  async function walkRecipeFiles(root) {
15394
15798
  if (!existsSync30(root)) return [];
15395
15799
  const out = [];
15396
15800
  for (const entry of await readdir3(root, { withFileTypes: true })) {
15397
- const resolved = path40.join(root, entry.name);
15801
+ const resolved = path41.join(root, entry.name);
15398
15802
  if (entry.isDirectory()) out.push(...await walkRecipeFiles(resolved));
15399
15803
  else if (entry.isFile() && entry.name === RECIPE_FILE) out.push(resolved);
15400
15804
  }
@@ -15405,7 +15809,7 @@ function bundledRecipeRoot() {
15405
15809
  }
15406
15810
  async function listBundledRecipes(root = bundledRecipeRoot()) {
15407
15811
  const recipes = await Promise.all(
15408
- (await walkRecipeFiles(path40.resolve(root))).map((file) => readRecipeFile(file, true))
15812
+ (await walkRecipeFiles(path41.resolve(root))).map((file) => readRecipeFile(file, true))
15409
15813
  );
15410
15814
  return recipes.sort(
15411
15815
  (a, b) => a.recipe.id.localeCompare(b.recipe.id) || compareVersions(b.recipe.version, a.recipe.version)
@@ -15428,9 +15832,9 @@ function parseSelector(selector) {
15428
15832
  return { id: match[1], version: match[2] };
15429
15833
  }
15430
15834
  async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
15431
- const local = path40.resolve(target);
15835
+ const local = path41.resolve(target);
15432
15836
  if (existsSync30(local)) {
15433
- const recipePath = path40.basename(local) === RECIPE_FILE ? local : path40.join(local, RECIPE_FILE);
15837
+ const recipePath = path41.basename(local) === RECIPE_FILE ? local : path41.join(local, RECIPE_FILE);
15434
15838
  return readRecipeFile(recipePath, false);
15435
15839
  }
15436
15840
  if (target.startsWith(".") || target.startsWith("/") || target.includes("\\")) {
@@ -15444,7 +15848,7 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
15444
15848
  return matches[0];
15445
15849
  }
15446
15850
  async function verifyFile(root, file) {
15447
- const absolute = path40.resolve(root, ...file.path.split("/"));
15851
+ const absolute = path41.resolve(root, ...file.path.split("/"));
15448
15852
  if (!existsSync30(absolute)) {
15449
15853
  return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
15450
15854
  }
@@ -15460,7 +15864,7 @@ async function verifyRecipe(target, options = {}) {
15460
15864
  const loaded = await resolveRecipe(target, options.bundledRoot);
15461
15865
  const actualIntegrity = recipeDigest(loaded.authored);
15462
15866
  const files = await Promise.all(loaded.recipe.files.map((file) => verifyFile(loaded.dir, file)));
15463
- const modelRoot = options.modelRoot ? path40.resolve(options.modelRoot) : null;
15867
+ const modelRoot = options.modelRoot ? path41.resolve(options.modelRoot) : null;
15464
15868
  const models = modelRoot ? await Promise.all(loaded.recipe.models.map(async (model) => ({
15465
15869
  ...await verifyFile(modelRoot, model),
15466
15870
  source: model.source,
@@ -15494,7 +15898,7 @@ async function verifyRecipe(target, options = {}) {
15494
15898
  };
15495
15899
  }
15496
15900
  function renderStyle(style, destination, cwd) {
15497
- const portableDir = path40.relative(cwd, destination).split(path40.sep).join("/") || ".";
15901
+ const portableDir = path41.relative(cwd, destination).split(path41.sep).join("/") || ".";
15498
15902
  const replace = (value) => {
15499
15903
  if (typeof value === "string") return value.replaceAll(RECIPE_DIR_TOKEN, portableDir);
15500
15904
  if (Array.isArray(value)) return value.map(replace);
@@ -15506,25 +15910,25 @@ function renderStyle(style, destination, cwd) {
15506
15910
  return replace(style);
15507
15911
  }
15508
15912
  async function installRecipe(target, options = {}) {
15509
- const cwd = path40.resolve(options.cwd ?? process.cwd());
15913
+ const cwd = path41.resolve(options.cwd ?? process.cwd());
15510
15914
  const loaded = await resolveRecipe(target, options.bundledRoot);
15511
15915
  const verification = await verifyRecipe(loaded.path);
15512
15916
  if (!verification.ok) {
15513
15917
  throw new Error(`Recipe ${loaded.recipe.id}@${loaded.recipe.version} failed integrity verification.`);
15514
15918
  }
15515
- const destination = options.out ? path40.resolve(cwd, options.out) : path40.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
15919
+ const destination = options.out ? path41.resolve(cwd, options.out) : path41.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
15516
15920
  const sources = [
15517
15921
  { path: RECIPE_FILE, source: loaded.path },
15518
- ...loaded.recipe.files.map((file) => ({ path: file.path, source: path40.join(loaded.dir, ...file.path.split("/")) }))
15922
+ ...loaded.recipe.files.map((file) => ({ path: file.path, source: path41.join(loaded.dir, ...file.path.split("/")) }))
15519
15923
  ];
15520
15924
  const files = await Promise.all(sources.map(async (file) => ({
15521
- path: path40.join(destination, ...file.path.split("/")),
15522
- data: await readFile27(file.source)
15925
+ path: path41.join(destination, ...file.path.split("/")),
15926
+ data: await readFile28(file.source)
15523
15927
  })));
15524
15928
  if (!options.force) {
15525
15929
  for (const file of files) {
15526
15930
  if (!existsSync30(file.path)) continue;
15527
- const current = await readFile27(file.path);
15931
+ const current = await readFile28(file.path);
15528
15932
  if (!current.equals(file.data)) {
15529
15933
  throw new OverwriteRefusedError(
15530
15934
  `Recipe destination has local changes: ${file.path}. Choose another --out or pass --force to replace declared recipe files.`,
@@ -15618,14 +16022,14 @@ async function runRecipe(args) {
15618
16022
  }, null, 2));
15619
16023
  } else {
15620
16024
  log(` installed ${result.recipe.id}@${result.recipe.version}`);
15621
- log(` ${path41.relative(process.cwd(), result.destination) || "."}`);
16025
+ log(` ${path42.relative(process.cwd(), result.destination) || "."}`);
15622
16026
  log(`
15623
16027
  Add this entry under your manifest's styles object:`);
15624
16028
  log(JSON.stringify({ [result.styleId]: result.style }, null, 2));
15625
16029
  if (result.recipe.models.length) {
15626
16030
  log(`
15627
16031
  Models are not downloaded automatically. Verify them with:`);
15628
- log(` pixelkiln recipe verify ${path41.relative(process.cwd(), result.destination)} --model-root <ComfyUI/models>`);
16032
+ log(` pixelkiln recipe verify ${path42.relative(process.cwd(), result.destination)} --model-root <ComfyUI/models>`);
15629
16033
  }
15630
16034
  }
15631
16035
  return;
@@ -15633,7 +16037,7 @@ async function runRecipe(args) {
15633
16037
  }
15634
16038
 
15635
16039
  // src/cli/commands/refine.ts
15636
- import path42 from "path";
16040
+ import path43 from "path";
15637
16041
  async function runRefine(args) {
15638
16042
  if (args.from || args.subcommand === "approve") {
15639
16043
  if (!args.from) {
@@ -15663,12 +16067,12 @@ async function runRefine(args) {
15663
16067
  ` recovered ${result3.detection.columns}x${result3.detection.rows} native grid (${result3.detection.confidence}: ${result3.detection.consensus})`
15664
16068
  );
15665
16069
  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)}`);
16070
+ log(` wrote ${path43.relative(process.cwd(), result3.output)}`);
16071
+ log(` quality record: ${path43.relative(process.cwd(), result3.record)}`);
15668
16072
  log(`
15669
16073
  Automated checks passed. Human 1\xD7 review is still required:`);
15670
16074
  log(
15671
- ` pixelkiln refine approve --from ${path42.relative(process.cwd(), result3.record)} --reviewer "Your Name"`
16075
+ ` pixelkiln refine approve --from ${path43.relative(process.cwd(), result3.record)} --reviewer "Your Name"`
15672
16076
  );
15673
16077
  }
15674
16078
  return;
@@ -15696,7 +16100,7 @@ async function runRefine(args) {
15696
16100
  if (args.json) log(JSON.stringify(result3, null, 2));
15697
16101
  else {
15698
16102
  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()}`
16103
+ 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
16104
  );
15701
16105
  }
15702
16106
  return;
@@ -15705,7 +16109,7 @@ async function runRefine(args) {
15705
16109
  if (args.json) {
15706
16110
  log(JSON.stringify(result2, null, 2));
15707
16111
  } else {
15708
- log(` ${result2.safe ? "release-ready" : "not release-ready"}: ${path42.relative(process.cwd(), result2.output || result2.record)}`);
16112
+ log(` ${result2.safe ? "release-ready" : "not release-ready"}: ${path43.relative(process.cwd(), result2.output || result2.record)}`);
15709
16113
  for (const reason of result2.reasons) log(` ${reason}`);
15710
16114
  }
15711
16115
  if (!result2.safe) process.exitCode = 1;
@@ -15751,7 +16155,7 @@ async function runRefine(args) {
15751
16155
  log(` ${item.state.padEnd(16)} ${item.key} ${item.reason}`);
15752
16156
  if (item.state === "needs-approval") {
15753
16157
  log(
15754
- ` pixelkiln refine approve --from ${path42.relative(process.cwd(), item.record)} --reviewer "Your Name"`
16158
+ ` pixelkiln refine approve --from ${path43.relative(process.cwd(), item.record)} --reviewer "Your Name"`
15755
16159
  );
15756
16160
  }
15757
16161
  }
@@ -15803,15 +16207,15 @@ async function runTools(args) {
15803
16207
  }
15804
16208
 
15805
16209
  // src/cli/commands/workspace.ts
15806
- import path43 from "path";
16210
+ import path44 from "path";
15807
16211
  import { existsSync as existsSync31 } from "fs";
15808
16212
  async function runWorkspace(args) {
15809
- const workspacePath = path43.resolve(args.workspace ?? "pixelkiln.workspace.json");
15810
- const dir = path43.dirname(workspacePath);
16213
+ const workspacePath = path44.resolve(args.workspace ?? "pixelkiln.workspace.json");
16214
+ const dir = path44.dirname(workspacePath);
15811
16215
  if (args.subcommand === "add") {
15812
- const manifestPath = path43.resolve(args.target);
16216
+ const manifestPath = path44.resolve(args.target);
15813
16217
  const loadedTarget = await loadManifest(manifestPath);
15814
- const lockPath = args.explicitLock ? path43.resolve(args.explicitLock) : path43.join(path43.dirname(manifestPath), "pixelkiln.lock.json");
16218
+ const lockPath = args.explicitLock ? path44.resolve(args.explicitLock) : path44.join(path44.dirname(manifestPath), "pixelkiln.lock.json");
15815
16219
  const ws = await loadWorkspace(workspacePath);
15816
16220
  const id = args.name ?? loadedTarget.manifest.name;
15817
16221
  if (ws.projects.some((p) => p.id === id)) {
@@ -15831,7 +16235,7 @@ async function runWorkspace(args) {
15831
16235
  ...args.account ? { account: args.account } : {}
15832
16236
  };
15833
16237
  await saveWorkspace(workspacePath, { version: 1, projects: [...ws.projects, project] });
15834
- log(` registered "${id}" in ${path43.relative(process.cwd(), workspacePath)}`);
16238
+ log(` registered "${id}" in ${path44.relative(process.cwd(), workspacePath)}`);
15835
16239
  log(` manifest: ${project.manifest}`);
15836
16240
  log(` lock: ${project.lock}`);
15837
16241
  if (!existsSync31(lockPath)) {
@@ -15843,7 +16247,7 @@ async function runWorkspace(args) {
15843
16247
  }
15844
16248
  if (args.subcommand === "remove") {
15845
16249
  const ws = await loadWorkspace(workspacePath);
15846
- const resolvedTarget = path43.resolve(args.target);
16250
+ const resolvedTarget = path44.resolve(args.target);
15847
16251
  const match = ws.projects.find(
15848
16252
  (p) => p.id === args.target || resolveProject(dir, p).manifestPath === resolvedTarget
15849
16253
  );
@@ -15854,7 +16258,7 @@ async function runWorkspace(args) {
15854
16258
  version: 1,
15855
16259
  projects: ws.projects.filter((p) => p !== match)
15856
16260
  });
15857
- log(` removed "${match.id}" from ${path43.relative(process.cwd(), workspacePath)}`);
16261
+ log(` removed "${match.id}" from ${path44.relative(process.cwd(), workspacePath)}`);
15858
16262
  return;
15859
16263
  }
15860
16264
  if (args.subcommand === "list") {
@@ -15866,9 +16270,9 @@ async function runWorkspace(args) {
15866
16270
  if (args.json) {
15867
16271
  log(JSON.stringify({ version: 1, workspace: workspacePath, projects: ws.projects, diagnostics }, null, 2));
15868
16272
  } else if (!ws.projects.length) {
15869
- log(` no projects registered in ${path43.relative(process.cwd(), workspacePath)}`);
16273
+ log(` no projects registered in ${path44.relative(process.cwd(), workspacePath)}`);
15870
16274
  } else {
15871
- log(` ${ws.projects.length} project(s) in ${path43.relative(process.cwd(), workspacePath)}:`);
16275
+ log(` ${ws.projects.length} project(s) in ${path44.relative(process.cwd(), workspacePath)}:`);
15872
16276
  for (const p of ws.projects) {
15873
16277
  log(` ${p.id.padEnd(24)} ${p.manifest.padEnd(40)} (${p.provider}${p.account ? `, ${p.account}` : ""})`);
15874
16278
  }
@@ -15886,7 +16290,7 @@ async function runWorkspace(args) {
15886
16290
  if (args.json) {
15887
16291
  log(JSON.stringify({ ...report, workspace: workspacePath }, null, 2));
15888
16292
  } else {
15889
- log(` workspace: ${path43.relative(process.cwd(), workspacePath)}`);
16293
+ log(` workspace: ${path44.relative(process.cwd(), workspacePath)}`);
15890
16294
  for (const p of report.projects) {
15891
16295
  if (p.error) {
15892
16296
  log(`
@@ -15941,7 +16345,7 @@ async function runVersion() {
15941
16345
  for (const up of ["../package.json", "../../package.json"]) {
15942
16346
  const url = new URL(up, import.meta.url);
15943
16347
  if (!existsSync32(url)) continue;
15944
- const pkg = JSON.parse(await readFile28(url, "utf8"));
16348
+ const pkg = JSON.parse(await readFile29(url, "utf8"));
15945
16349
  if (pkg.name === "pixelkiln") {
15946
16350
  log(`${pkg.name} ${pkg.version}`);
15947
16351
  return;