pixelkiln 0.84.0 → 0.86.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -313,8 +313,16 @@ var RevisionDitheringSchema = z.enum(["none", "2x2", "4x4", "8x8"]);
313
313
  var RevisionSchema = z.object({
314
314
  /** What kind of controlled change the provider workflow performs. */
315
315
  mode: RevisionModeSchema,
316
- /** Parent asset id in the same style. */
316
+ /** Parent asset id. It lives in this asset's own style unless `fromStyle` names another. */
317
317
  from: z.string().min(1),
318
+ /**
319
+ * The style the parent lives in, when it is not this asset's own. The
320
+ * parent is generated by that style's provider and model, and this
321
+ * revision starts from its output file, so a chain (generate with one
322
+ * model, clean up with another) is declared instead of restated by hand.
323
+ * Styles may depend on one another but not in a cycle.
324
+ */
325
+ fromStyle: z.string().min(1).optional(),
318
326
  /** Manifest-relative black/white PNG. Required only for masked inpainting. */
319
327
  mask: z.string().min(1).optional(),
320
328
  /** Provider-neutral edit strength. The active adapter must bind it explicitly. */
@@ -1175,6 +1183,7 @@ var LockEntrySchema = z.object({
1175
1183
  revision: z.object({
1176
1184
  mode: RevisionModeSchema,
1177
1185
  sourceAssetId: z.string().min(1),
1186
+ sourceStyleId: z.string().min(1).optional(),
1178
1187
  sourceSha256: z.string().regex(/^[0-9a-f]{64}$/),
1179
1188
  maskSha256: z.string().regex(/^[0-9a-f]{64}$/).optional(),
1180
1189
  strength: z.number().min(0).max(1).optional(),
@@ -1816,9 +1825,9 @@ var PixelLabClient = class {
1816
1825
  apiKey;
1817
1826
  http;
1818
1827
  /** Retries transport failures, 429, and 5xx; see http.ts for the policy. */
1819
- async request(path37, init) {
1828
+ async request(path38, init) {
1820
1829
  const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
1821
- const res = await this.http(`${BASE}${path37}`, {
1830
+ const res = await this.http(`${BASE}${path38}`, {
1822
1831
  ...init,
1823
1832
  headers: {
1824
1833
  Authorization: auth,
@@ -1828,13 +1837,13 @@ var PixelLabClient = class {
1828
1837
  });
1829
1838
  const text = await res.text();
1830
1839
  if (!res.ok) {
1831
- throw new PixelLabError(`${init?.method ?? "GET"} ${path37} \u2192 ${res.status}`, res.status, text);
1840
+ throw new PixelLabError(`${init?.method ?? "GET"} ${path38} \u2192 ${res.status}`, res.status, text);
1832
1841
  }
1833
1842
  if (!text) return {};
1834
1843
  try {
1835
1844
  return JSON.parse(text);
1836
1845
  } catch {
1837
- throw new Error(`${init?.method ?? "GET"} ${path37} returned invalid JSON`);
1846
+ throw new Error(`${init?.method ?? "GET"} ${path38} returned invalid JSON`);
1838
1847
  }
1839
1848
  }
1840
1849
  async balance() {
@@ -2239,10 +2248,10 @@ var PixelLabClient = class {
2239
2248
  const palette = args.paletteSwatchBase64 ? { color_image: { type: "base64", base64: args.paletteSwatchBase64, format: "png" }, force_colors: true } : {};
2240
2249
  const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
2241
2250
  const south = args.reference?.south;
2242
- let path37;
2251
+ let path38;
2243
2252
  let body;
2244
2253
  if (args.mode === "v3") {
2245
- path37 = "/create-character-v3";
2254
+ path38 = "/create-character-v3";
2246
2255
  body = {
2247
2256
  description: args.description,
2248
2257
  image_size: imageSize,
@@ -2256,7 +2265,7 @@ var PixelLabClient = class {
2256
2265
  ...args.enhancePrompt ? { enhance_prompt: true } : {}
2257
2266
  };
2258
2267
  } else if (args.mode === "pro-flash") {
2259
- path37 = "/create-character-pro-flash";
2268
+ path38 = "/create-character-pro-flash";
2260
2269
  const traits = args.styleTraits;
2261
2270
  body = {
2262
2271
  description: args.description,
@@ -2280,7 +2289,7 @@ var PixelLabClient = class {
2280
2289
  } : {}
2281
2290
  };
2282
2291
  } else if (args.mode === "pro") {
2283
- path37 = "/create-character-pro";
2292
+ path38 = "/create-character-pro";
2284
2293
  body = {
2285
2294
  description: args.description,
2286
2295
  image_size: imageSize,
@@ -2294,7 +2303,7 @@ var PixelLabClient = class {
2294
2303
  ...!south && args.styleCharacterId ? { style_character_id: args.styleCharacterId } : {}
2295
2304
  };
2296
2305
  } else {
2297
- path37 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
2306
+ path38 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
2298
2307
  body = {
2299
2308
  description: args.description,
2300
2309
  image_size: imageSize,
@@ -2311,8 +2320,8 @@ var PixelLabClient = class {
2311
2320
  ...palette
2312
2321
  };
2313
2322
  }
2314
- const raw = await this.request(path37, { method: "POST", body: JSON.stringify(body) });
2315
- return validateResponse(CharacterSubmitSchema, raw, path37.slice(1));
2323
+ const raw = await this.request(path38, { method: "POST", body: JSON.stringify(body) });
2324
+ return validateResponse(CharacterSubmitSchema, raw, path38.slice(1));
2316
2325
  }
2317
2326
  /** A text edit of an existing character, applied to every direction and stored as a sibling. */
2318
2327
  async createCharacterState(args) {
@@ -3525,10 +3534,10 @@ import { createReadStream } from "fs";
3525
3534
  function sha256(data) {
3526
3535
  return createHash("sha256").update(data).digest("hex");
3527
3536
  }
3528
- async function sha256File(path37) {
3537
+ async function sha256File(path38) {
3529
3538
  return new Promise((resolve, reject) => {
3530
3539
  const hash = createHash("sha256");
3531
- const stream = createReadStream(path37);
3540
+ const stream = createReadStream(path38);
3532
3541
  stream.on("data", (chunk2) => hash.update(chunk2));
3533
3542
  stream.on("error", reject);
3534
3543
  stream.on("end", () => resolve(hash.digest("hex")));
@@ -3637,6 +3646,7 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
3637
3646
  revision: spec.revision ? {
3638
3647
  mode: spec.revision.mode,
3639
3648
  from: spec.revision.sourceAssetId,
3649
+ fromStyle: spec.revision.sourceStyleId,
3640
3650
  sourceSha256: spec.revision.sourceSha256,
3641
3651
  maskSha256: spec.revision.maskSha256,
3642
3652
  strength: spec.revision.strength,
@@ -5932,9 +5942,9 @@ var RetroDiffusionClient = class {
5932
5942
  if (!response.ok) throw new ProviderError("retrodiffusion", `Retro Diffusion download failed (${response.status})`, { status: response.status });
5933
5943
  return Buffer.from(await response.arrayBuffer());
5934
5944
  }
5935
- async call(path37, init = {}) {
5945
+ async call(path38, init = {}) {
5936
5946
  if (!this.token) throw new Error("RD_API_KEY is not set");
5937
- const response = await this.request(`${this.baseUrl}${path37}`, {
5947
+ const response = await this.request(`${this.baseUrl}${path38}`, {
5938
5948
  ...init,
5939
5949
  headers: {
5940
5950
  "Content-Type": "application/json",
@@ -6243,8 +6253,13 @@ function retroError(value) {
6243
6253
  }
6244
6254
 
6245
6255
  // src/providers/scenario.ts
6256
+ import { createHash as createHash2 } from "crypto";
6257
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync, writeFileSync as writeFileSync2 } from "fs";
6258
+ import os2 from "os";
6259
+ import path2 from "path";
6246
6260
  var DEFAULT_BASE_URL2 = "https://api.cloud.scenario.com/v1";
6247
6261
  var SOURCE_PROTOCOL2 = "scenario:";
6262
+ var SUBMIT_RETRIES = 8;
6248
6263
  var PROTECTED_PARAMETERS = /* @__PURE__ */ new Set([
6249
6264
  "dryRun",
6250
6265
  "height",
@@ -6291,7 +6306,11 @@ var ScenarioClient = class {
6291
6306
  async submit(modelId, body, projectId) {
6292
6307
  const response = await this.call(
6293
6308
  `/generate/custom/${encodeURIComponent(modelId)}`,
6294
- { method: "POST", body: JSON.stringify(body) },
6309
+ // A 429 here means this account's concurrent-job limit refused the
6310
+ // request, so nothing was accepted or billed; wait it out instead of
6311
+ // failing the asset. The default four retries give up after about half
6312
+ // a minute, before a running job has finished.
6313
+ { method: "POST", body: JSON.stringify(body), retries: SUBMIT_RETRIES },
6295
6314
  { projectId }
6296
6315
  );
6297
6316
  const job = response?.job;
@@ -6299,6 +6318,29 @@ var ScenarioClient = class {
6299
6318
  if (!jobId) throw new Error("Scenario did not return an async job id");
6300
6319
  return jobId;
6301
6320
  }
6321
+ /**
6322
+ * Uploads one image (POST /assets) and returns its asset id, which a model's
6323
+ * reference input takes. Free, and idempotent per image: the id is kept by
6324
+ * content hash so a rerun does not upload the same bytes again.
6325
+ */
6326
+ async uploadImage(image, projectId) {
6327
+ const bytes = Buffer.from(image.base64, "base64");
6328
+ const digest2 = sha256(bytes);
6329
+ const cached = readUploadCache()[uploadCacheKey(this.baseUrl, projectId, digest2)];
6330
+ if (cached) return cached;
6331
+ const response = await this.call(
6332
+ "/assets",
6333
+ {
6334
+ method: "POST",
6335
+ body: JSON.stringify({ image: image.base64, name: `pixelkiln-${digest2.slice(0, 12)}.${image.format === "png" ? "png" : "jpg"}` })
6336
+ },
6337
+ { projectId }
6338
+ );
6339
+ const id = response?.asset?.id;
6340
+ if (typeof id !== "string" || !id) throw new Error("Scenario did not return an uploaded asset id");
6341
+ writeUploadCache(uploadCacheKey(this.baseUrl, projectId, digest2), id);
6342
+ return id;
6343
+ }
6302
6344
  async job(id, projectId) {
6303
6345
  const response = await this.call(`/jobs/${encodeURIComponent(id)}`, {}, { projectId });
6304
6346
  const raw = response?.job;
@@ -6425,6 +6467,14 @@ var ScenarioProvider = class _ScenarioProvider {
6425
6467
  supports(generator) {
6426
6468
  return generator === "map";
6427
6469
  }
6470
+ /**
6471
+ * An edit is a generation with the parent as a reference image, which the
6472
+ * editing models (GPT Image, Gemini, FLUX) take. Masked inpainting and the
6473
+ * PixelLab-specific modes have no common Scenario input.
6474
+ */
6475
+ supportsRevision(mode2) {
6476
+ return mode2 === "image-to-image";
6477
+ }
6428
6478
  estimate(spec) {
6429
6479
  const options = scenarioOptions(spec);
6430
6480
  return {
@@ -6445,11 +6495,31 @@ var ScenarioProvider = class _ScenarioProvider {
6445
6495
  if (!Number.isInteger(count) || count < 1 || count > 4) {
6446
6496
  throw new Error("Scenario numOutputs must be a whole number from 1 to 4");
6447
6497
  }
6448
- if (spec.width < 128 || spec.width > 2048 || spec.width % 16 !== 0 || spec.height < 128 || spec.height > 2048 || spec.height % 16 !== 0) {
6449
- throw new Error("Scenario width and height must be multiples of 16 from 128 to 2048");
6498
+ if (!Number.isInteger(spec.width) || spec.width < 16 || spec.width > 4096 || !Number.isInteger(spec.height) || spec.height < 16 || spec.height > 4096) {
6499
+ throw new Error("Scenario width and height must be whole numbers from 16 to 4096");
6450
6500
  }
6451
- if (styleImages.length) {
6452
- throw new Error("Scenario styleImages are not supported yet; use a reusable Scenario model");
6501
+ const revision = spec.revision;
6502
+ if (revision) {
6503
+ if (revision.sourceMembers?.length) {
6504
+ throw new Error(
6505
+ `Scenario edits one image; ${revision.sourceAssetId} is a ${revision.sourceMembers.length}-member set`
6506
+ );
6507
+ }
6508
+ if (revision.strength != null) {
6509
+ throw new Error("Scenario image-to-image takes no strength; use the model's own parameters");
6510
+ }
6511
+ }
6512
+ const parameter = referenceParameter(options);
6513
+ if ((styleImages.length || revision) && Object.hasOwn(options.parameters ?? {}, parameter)) {
6514
+ throw new Error(
6515
+ `Scenario parameters.${parameter} is the reference-image input and is filled from styleImages and revisions`
6516
+ );
6517
+ }
6518
+ if (styleImages.length + (revision ? 1 : 0) > 1 && options.referenceArray === false) {
6519
+ throw new Error(`Scenario ${parameter} takes one image; use one style image or a revision, not both`);
6520
+ }
6521
+ if (options.referenceParameter != null && (typeof options.referenceParameter !== "string" || !options.referenceParameter.trim())) {
6522
+ throw new Error("Scenario referenceParameter must be a non-empty string");
6453
6523
  }
6454
6524
  if (options.projectId != null && (typeof options.projectId !== "string" || !options.projectId.trim())) {
6455
6525
  throw new Error("Scenario projectId must be a non-empty string");
@@ -6462,7 +6532,12 @@ var ScenarioProvider = class _ScenarioProvider {
6462
6532
  async submit(spec, styleImages) {
6463
6533
  this.validate(spec, styleImages);
6464
6534
  const options = scenarioOptions(spec);
6465
- const body = requestBody(spec, options);
6535
+ const references = [];
6536
+ if (spec.revision) references.push(readRevisionSource(spec));
6537
+ for (const image of styleImages) references.push({ base64: image.base64, format: image.format });
6538
+ const referenceAssetIds = [];
6539
+ for (const image of references) referenceAssetIds.push(await this.client.uploadImage(image, options.projectId));
6540
+ const body = requestBody(spec, options, referenceAssetIds);
6466
6541
  const quote = await this.client.quote(options.modelId, body, options.projectId);
6467
6542
  if (quote.total > options.maxComputeUnits + Number.EPSILON) {
6468
6543
  throw new Error(
@@ -6479,7 +6554,8 @@ var ScenarioProvider = class _ScenarioProvider {
6479
6554
  quotedComputeUnits: quote.total,
6480
6555
  creativeUnitsCost: quote.creativeUnitsCost,
6481
6556
  costDetails: quote.costDetails,
6482
- ipDetectionCost: quote.ipDetectionCost
6557
+ ipDetectionCost: quote.ipDetectionCost,
6558
+ ...referenceAssetIds.length ? { referenceAssetIds } : {}
6483
6559
  }
6484
6560
  };
6485
6561
  }
@@ -6501,7 +6577,13 @@ var ScenarioProvider = class _ScenarioProvider {
6501
6577
  }
6502
6578
  const assetIds = scenarioAssetIds(job);
6503
6579
  if (!assetIds.length) return { status: "failed", error: "Scenario job returned no asset IDs" };
6504
- const assets = await this.assets(assetIds, reference.projectId);
6580
+ let assets;
6581
+ try {
6582
+ assets = await this.assets(assetIds, reference.projectId);
6583
+ } catch (error) {
6584
+ if (error instanceof UnsupportedScenarioOutput) return { status: "failed", error: error.message };
6585
+ throw error;
6586
+ }
6505
6587
  if (assets.length > 1) {
6506
6588
  return {
6507
6589
  status: "review",
@@ -6556,6 +6638,32 @@ var ScenarioProvider = class _ScenarioProvider {
6556
6638
  return assets.sort((left, right) => left.outputIndex - right.outputIndex || (jobOrder.get(left.id) ?? Number.MAX_SAFE_INTEGER) - (jobOrder.get(right.id) ?? Number.MAX_SAFE_INTEGER) || left.id.localeCompare(right.id));
6557
6639
  }
6558
6640
  };
6641
+ function uploadCachePath() {
6642
+ return process.env.SCENARIO_UPLOAD_CACHE ?? path2.join(os2.tmpdir(), "pixelkiln-scenario", "uploads.json");
6643
+ }
6644
+ function uploadCacheKey(baseUrl, projectId, digest2) {
6645
+ return createHash2("sha256").update(`${baseUrl}
6646
+ ${projectId ?? ""}
6647
+ ${digest2}`).digest("hex");
6648
+ }
6649
+ function readUploadCache() {
6650
+ try {
6651
+ const value = JSON.parse(readFileSync2(uploadCachePath(), "utf8"));
6652
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
6653
+ } catch {
6654
+ return {};
6655
+ }
6656
+ }
6657
+ function writeUploadCache(key, assetId) {
6658
+ try {
6659
+ const file = uploadCachePath();
6660
+ mkdirSync2(path2.dirname(file), { recursive: true });
6661
+ const tmp = `${file}.${process.pid}.tmp`;
6662
+ writeFileSync2(tmp, JSON.stringify({ ...readUploadCache(), [key]: assetId }));
6663
+ renameSync(tmp, file);
6664
+ } catch {
6665
+ }
6666
+ }
6559
6667
  function scenarioOptions(spec) {
6560
6668
  return spec.providerOptions;
6561
6669
  }
@@ -6571,9 +6679,25 @@ function scenarioBaseUrl(value) {
6571
6679
  }
6572
6680
  return url.href.replace(/\/$/, "");
6573
6681
  }
6574
- function requestBody(spec, options) {
6682
+ function referenceParameter(options) {
6683
+ return options.referenceParameter?.trim() || "referenceImages";
6684
+ }
6685
+ function readRevisionSource(spec) {
6686
+ const revision = spec.revision;
6687
+ if (!revision.sourceFile || !revision.sourceSha256 || !revision.sourceFormat) {
6688
+ throw new Error(`${spec.styleId}/${spec.assetId}: revision source is not ready`);
6689
+ }
6690
+ const bytes = readFileSync2(revision.sourceFile);
6691
+ if (sha256(bytes) !== revision.sourceSha256) {
6692
+ throw new Error(`${spec.styleId}/${spec.assetId}: revision source changed after the manifest was resolved`);
6693
+ }
6694
+ return { base64: bytes.toString("base64"), format: revision.sourceFormat };
6695
+ }
6696
+ function requestBody(spec, options, referenceAssetIds = []) {
6697
+ const parameter = referenceParameter(options);
6575
6698
  return {
6576
6699
  ...options.parameters ?? {},
6700
+ ...referenceAssetIds.length ? { [parameter]: options.referenceArray === false ? referenceAssetIds[0] : referenceAssetIds } : {},
6577
6701
  prompt: spec.prompt,
6578
6702
  width: spec.width,
6579
6703
  height: spec.height,
@@ -6663,9 +6787,11 @@ function scenarioAssetIds(job) {
6663
6787
  function assetDownloadUrl(asset) {
6664
6788
  return asset.originalFileUrl || asset.url;
6665
6789
  }
6790
+ var UnsupportedScenarioOutput = class extends Error {
6791
+ };
6666
6792
  function assertPngAsset(asset) {
6667
6793
  if (asset.originalMimeType && asset.originalMimeType !== MediaType.PNG) {
6668
- throw new Error(
6794
+ throw new UnsupportedScenarioOutput(
6669
6795
  `Scenario asset ${asset.id} is ${asset.originalMimeType}; PixelKiln's Scenario MVP requires PNG output`
6670
6796
  );
6671
6797
  }
@@ -6732,7 +6858,7 @@ function redactScenarioError(value, secrets) {
6732
6858
  // src/providers/comfyui.ts
6733
6859
  import { randomUUID as randomUUID2 } from "crypto";
6734
6860
  import { readFile } from "fs/promises";
6735
- import path2 from "path";
6861
+ import path3 from "path";
6736
6862
  var DEFAULT_BASE_URL3 = "http://127.0.0.1:8188";
6737
6863
  var BUILTIN_BINDINGS = /* @__PURE__ */ new Set([
6738
6864
  "prompt",
@@ -6869,7 +6995,7 @@ var ComfyUIProvider = class _ComfyUIProvider {
6869
6995
  }
6870
6996
  async resolveOptions(value, context) {
6871
6997
  const options = parseOptions(value);
6872
- const workflowPath = path2.resolve(context.root, options.workflowFile);
6998
+ const workflowPath = path3.resolve(context.root, options.workflowFile);
6873
6999
  let raw;
6874
7000
  try {
6875
7001
  raw = await readFile(workflowPath, "utf8");
@@ -6924,7 +7050,7 @@ var ComfyUIProvider = class _ComfyUIProvider {
6924
7050
  if (typeof item !== "string") {
6925
7051
  throw new Error(`ComfyUI ${label} must be a manifest-relative PNG or JPEG path`);
6926
7052
  }
6927
- const file = path2.resolve(context.root, item);
7053
+ const file = path3.resolve(context.root, item);
6928
7054
  let bytes;
6929
7055
  try {
6930
7056
  bytes = await readFile(file);
@@ -7636,7 +7762,7 @@ function canonical(value) {
7636
7762
  }
7637
7763
 
7638
7764
  // src/providers/fake.ts
7639
- import { createHash as createHash2 } from "crypto";
7765
+ import { createHash as createHash3 } from "crypto";
7640
7766
  var FAKE_PNG = Buffer.from(
7641
7767
  "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
7642
7768
  "base64"
@@ -7738,7 +7864,7 @@ var FakeProvider = class {
7738
7864
  }
7739
7865
  async download(url) {
7740
7866
  if (!url.startsWith("fake://")) throw new Error(`fake provider cannot download ${url}`);
7741
- const salt = createHash2("sha256").update(url).digest();
7867
+ const salt = createHash3("sha256").update(url).digest();
7742
7868
  return Buffer.concat([FAKE_PNG, salt.subarray(0, 4)]);
7743
7869
  }
7744
7870
  async balance() {
@@ -7887,7 +8013,7 @@ registerProvider({
7887
8013
  // src/manifest.ts
7888
8014
  import { readFile as readFile3 } from "fs/promises";
7889
8015
  import { existsSync as existsSync4 } from "fs";
7890
- import path5 from "path";
8016
+ import path6 from "path";
7891
8017
 
7892
8018
  // src/loop-directions.ts
7893
8019
  var SINGLE_OUTPUT_FIELDS = ["file", "cell", "remoteId", "source", "sourceByStyle", "outputRole"];
@@ -7976,12 +8102,12 @@ function isObject2(value) {
7976
8102
 
7977
8103
  // src/outputs.ts
7978
8104
  import { existsSync as existsSync3 } from "fs";
7979
- import path4 from "path";
8105
+ import path5 from "path";
7980
8106
 
7981
8107
  // src/lock.ts
7982
8108
  import { readFile as readFile2, writeFile, rename, mkdir, rmdir, rm, stat } from "fs/promises";
7983
8109
  import { existsSync as existsSync2 } from "fs";
7984
- import path3 from "path";
8110
+ import path4 from "path";
7985
8111
  async function loadLock(lockPath) {
7986
8112
  if (!existsSync2(lockPath)) return { version: 2, entries: {} };
7987
8113
  try {
@@ -7998,7 +8124,7 @@ var saveQueues = /* @__PURE__ */ new Map();
7998
8124
  var dirtyPatches = /* @__PURE__ */ new WeakMap();
7999
8125
  var dirtyDeletes = /* @__PURE__ */ new WeakMap();
8000
8126
  function saveLock(lockPath, lock) {
8001
- const queueKey = path3.resolve(lockPath);
8127
+ const queueKey = path4.resolve(lockPath);
8002
8128
  const previous = saveQueues.get(queueKey) ?? Promise.resolve();
8003
8129
  const next = previous.catch(() => {
8004
8130
  }).then(() => writeLockNow(lockPath, lock));
@@ -8010,7 +8136,7 @@ function saveLock(lockPath, lock) {
8010
8136
  return next;
8011
8137
  }
8012
8138
  async function writeLockNow(lockPath, lock) {
8013
- await mkdir(path3.dirname(path3.resolve(lockPath)), { recursive: true });
8139
+ await mkdir(path4.dirname(path4.resolve(lockPath)), { recursive: true });
8014
8140
  const release = await acquireFileLock(lockPath);
8015
8141
  try {
8016
8142
  await writeLockWhileHeld(lockPath, lock);
@@ -8155,22 +8281,22 @@ function totalSpend(lock, unit = "generations") {
8155
8281
 
8156
8282
  // src/outputs.ts
8157
8283
  function portableOutputPath(file, manifestDir) {
8158
- const root = path4.resolve(manifestDir);
8159
- const absolute = path4.isAbsolute(file) ? path4.normalize(file) : path4.resolve(root, file);
8160
- const relative = path4.relative(root, absolute);
8161
- const value = path4.isAbsolute(relative) ? absolute : relative;
8162
- return value.split(path4.sep).join("/");
8284
+ const root = path5.resolve(manifestDir);
8285
+ const absolute = path5.isAbsolute(file) ? path5.normalize(file) : path5.resolve(root, file);
8286
+ const relative = path5.relative(root, absolute);
8287
+ const value = path5.isAbsolute(relative) ? absolute : relative;
8288
+ return value.split(path5.sep).join("/");
8163
8289
  }
8164
8290
  function resolveOutputPath(recordedPath, manifestDir) {
8165
- if (path4.isAbsolute(recordedPath)) return path4.normalize(recordedPath);
8166
- if (path4.win32.isAbsolute(recordedPath)) return recordedPath;
8167
- return path4.resolve(manifestDir, recordedPath.split(/[\\/]/).join(path4.sep));
8291
+ if (path5.isAbsolute(recordedPath)) return path5.normalize(recordedPath);
8292
+ if (path5.win32.isAbsolute(recordedPath)) return recordedPath;
8293
+ return path5.resolve(manifestDir, recordedPath.split(/[\\/]/).join(path5.sep));
8168
8294
  }
8169
8295
  function expectedOutputPath(spec, role, index, total, mediaType) {
8170
8296
  return memberPath(spec.outFile, role, index, total, mediaType);
8171
8297
  }
8172
8298
  function memberPath(file, role, index, total, mediaType) {
8173
- const originalExt = path4.extname(file);
8299
+ const originalExt = path5.extname(file);
8174
8300
  const ext = mediaType ? mediaExtension(mediaType) : originalExt || ".png";
8175
8301
  const stem = originalExt ? file.slice(0, -originalExt.length) : file;
8176
8302
  if (total === 1) return `${stem}${ext}`;
@@ -8184,10 +8310,10 @@ function isFrameSetEntry(entry) {
8184
8310
  return entry.generator === "frames" && isMemberSetEntry(entry);
8185
8311
  }
8186
8312
  function sourceIsStem(source, entry, manifestDir) {
8187
- return isMemberSetEntry(entry) && !existsSync3(path4.resolve(manifestDir, source));
8313
+ return isMemberSetEntry(entry) && !existsSync3(path5.resolve(manifestDir, source));
8188
8314
  }
8189
8315
  function sourceOutputPath(source, entry, index, manifestDir) {
8190
- const absolute = path4.resolve(manifestDir, source);
8316
+ const absolute = path5.resolve(manifestDir, source);
8191
8317
  if (!sourceIsStem(source, entry, manifestDir)) return absolute;
8192
8318
  const output = entry.outputs[index];
8193
8319
  return memberPath(absolute, output.role, index, entry.outputs.length, output.mediaType);
@@ -8274,7 +8400,7 @@ function resolveSpecOutputs(spec, lock, manifestDir) {
8274
8400
  id: spec.assetId,
8275
8401
  index: 0,
8276
8402
  path: spec.outFile,
8277
- absolutePath: path4.resolve(spec.outFile),
8403
+ absolutePath: path5.resolve(spec.outFile),
8278
8404
  sha256: ""
8279
8405
  }];
8280
8406
  }
@@ -8290,7 +8416,7 @@ function frameSetFps(entry) {
8290
8416
 
8291
8417
  // src/manifest.ts
8292
8418
  async function loadManifest(manifestPath) {
8293
- const abs = path5.resolve(manifestPath);
8419
+ const abs = path6.resolve(manifestPath);
8294
8420
  if (!existsSync4(abs)) throw new ProjectError(`No manifest at ${abs}`);
8295
8421
  const expansion = expandLoopDirections(JSON.parse(await readFile3(abs, "utf8")));
8296
8422
  if (expansion.issues.length) {
@@ -8335,13 +8461,24 @@ ${issues}`);
8335
8461
  }
8336
8462
  }
8337
8463
  if (asset.revision) {
8338
- if (!parsed.data.assets[asset.revision.from]) {
8464
+ const parent = parsed.data.assets[asset.revision.from];
8465
+ if (!parent) {
8339
8466
  unknownReferences.push(
8340
8467
  `assets.${assetId}.revision.from: unknown asset "${asset.revision.from}"`
8341
8468
  );
8342
8469
  } else if (asset.revision.from === assetId) {
8343
8470
  unknownReferences.push(`assets.${assetId}.revision.from: an asset cannot revise itself`);
8344
8471
  }
8472
+ const fromStyle = asset.revision.fromStyle;
8473
+ if (fromStyle) {
8474
+ if (!styleIds.has(fromStyle)) {
8475
+ unknownReferences.push(`assets.${assetId}.revision.fromStyle: unknown style "${fromStyle}"`);
8476
+ } else if (parent && parent.styles.length && !parent.styles.includes(fromStyle)) {
8477
+ unknownReferences.push(
8478
+ `assets.${assetId}.revision.fromStyle: "${asset.revision.from}" is not in style "${fromStyle}"`
8479
+ );
8480
+ }
8481
+ }
8345
8482
  }
8346
8483
  for (const shape of ["state", "animation"]) {
8347
8484
  const of = asset[shape]?.of;
@@ -8383,7 +8520,7 @@ ${issues}`);
8383
8520
  ${unknownReferences.map((i) => ` ${i}`).join("\n")}`);
8384
8521
  }
8385
8522
  const loopFamilies = Object.keys(expansion.families).length ? { loopFamilies: expansion.families } : {};
8386
- return { manifest: parsed.data, root: path5.dirname(abs), path: abs, ...loopFamilies };
8523
+ return { manifest: parsed.data, root: path6.dirname(abs), path: abs, ...loopFamilies };
8387
8524
  }
8388
8525
  function parentAssetId(asset) {
8389
8526
  return asset?.revision?.from ?? asset?.state?.of ?? asset?.animation?.of ?? asset?.portrait?.of ?? asset?.outfit?.of;
@@ -8457,7 +8594,7 @@ async function resolveSpecs(loaded, filter) {
8457
8594
  return provider;
8458
8595
  };
8459
8596
  const specs = [];
8460
- const styleIds = Object.keys(manifest.styles).filter(
8597
+ const requestedStyleIds = Object.keys(manifest.styles).filter(
8461
8598
  (id) => !filter?.styles?.length || filter.styles.includes(id)
8462
8599
  );
8463
8600
  for (const unknownStyle of filter?.styles ?? []) {
@@ -8482,9 +8619,42 @@ async function resolveSpecs(loaded, filter) {
8482
8619
  current = parentAssetId(manifest.assets[current]);
8483
8620
  }
8484
8621
  }
8622
+ const styleDependencies = /* @__PURE__ */ new Map();
8623
+ for (const styleId of Object.keys(manifest.styles)) {
8624
+ const needed = /* @__PURE__ */ new Set();
8625
+ for (const assetId of resolutionAssetIds) {
8626
+ const asset = manifest.assets[assetId];
8627
+ if (asset?.styles.length && !asset.styles.includes(styleId)) continue;
8628
+ const fromStyle = asset?.revision?.fromStyle;
8629
+ if (fromStyle && fromStyle !== styleId) needed.add(fromStyle);
8630
+ }
8631
+ styleDependencies.set(styleId, needed);
8632
+ }
8633
+ const styleIds = [];
8634
+ const emitStyles = new Set(requestedStyleIds);
8635
+ {
8636
+ const placed = /* @__PURE__ */ new Set();
8637
+ const visiting = [];
8638
+ const place = (styleId) => {
8639
+ if (placed.has(styleId)) return;
8640
+ const cycleStart = visiting.indexOf(styleId);
8641
+ if (cycleStart >= 0) {
8642
+ throw new Error(
8643
+ `Styles revise each other in a cycle: ${[...visiting.slice(cycleStart), styleId].join(" -> ")}. revision.fromStyle must form a chain, not a loop.`
8644
+ );
8645
+ }
8646
+ visiting.push(styleId);
8647
+ for (const dependency of styleDependencies.get(styleId) ?? []) place(dependency);
8648
+ visiting.pop();
8649
+ placed.add(styleId);
8650
+ styleIds.push(styleId);
8651
+ };
8652
+ for (const styleId of requestedStyleIds) place(styleId);
8653
+ }
8654
+ const styleFinalizers = /* @__PURE__ */ new Map();
8485
8655
  const styleImageCache = /* @__PURE__ */ new Map();
8486
8656
  async function loadStyleImage(rel, what = "Style image") {
8487
- const abs = path5.resolve(root, rel);
8657
+ const abs = path6.resolve(root, rel);
8488
8658
  let hit = styleImageCache.get(abs);
8489
8659
  if (!hit) {
8490
8660
  if (!existsSync4(abs)) throw new Error(`${what} not found: ${abs}`);
@@ -8514,7 +8684,7 @@ async function resolveSpecs(loaded, filter) {
8514
8684
  styleImageDimensions.push(loadedImage);
8515
8685
  }
8516
8686
  const resolvedImages = style.styleImages.map((image) => {
8517
- const hit = styleImageCache.get(path5.resolve(root, image.path));
8687
+ const hit = styleImageCache.get(path6.resolve(root, image.path));
8518
8688
  return { base64: hit.base64, width: hit.width, height: hit.height, format: hit.format };
8519
8689
  });
8520
8690
  const styleSpecs = /* @__PURE__ */ new Map();
@@ -8615,9 +8785,9 @@ async function resolveSpecs(loaded, filter) {
8615
8785
  } else {
8616
8786
  prompt = [style.promptPrefix, subject, style.promptSuffix].map((p) => p.trim()).filter(Boolean).join(", ");
8617
8787
  }
8618
- const relFile = asset.file ?? path5.join(asset.category ?? "", `${assetId}.png`);
8619
- const outFile = path5.resolve(root, style.outDir, relFile);
8620
- const qualityOutFile = style.quality ? path5.resolve(root, style.quality.outDir, pngPath(relFile)) : void 0;
8788
+ const relFile = asset.file ?? path6.join(asset.category ?? "", `${assetId}.png`);
8789
+ const outFile = path6.resolve(root, style.outDir, relFile);
8790
+ const qualityOutFile = style.quality ? path6.resolve(root, style.quality.outDir, pngPath(relFile)) : void 0;
8621
8791
  const tileSize = generator === "tiles" ? size : style.tileSize ?? 32;
8622
8792
  const tileVariations = tileFeatureOutputCount(generator === "tiles" ? style.tileFeature : void 0) ?? tileVariationCount(countNumberedDescriptions(prompt));
8623
8793
  const terrainTiles = generator === "terrain" ? terrainTileCount(style.terrainTransitionSize) : 0;
@@ -8712,7 +8882,7 @@ async function resolveSpecs(loaded, filter) {
8712
8882
  minGridConfidence: style.quality.minGridConfidence,
8713
8883
  ...style.quality.minTransparency == null ? {} : { minTransparency: style.quality.minTransparency },
8714
8884
  ...style.quality.fixerRevision ? { fixerRevision: style.quality.fixerRevision } : {},
8715
- ...style.quality.fixerPython ? { fixerPython: path5.resolve(root, style.quality.fixerPython) } : {},
8885
+ ...style.quality.fixerPython ? { fixerPython: path6.resolve(root, style.quality.fixerPython) } : {},
8716
8886
  ...style.quality.fps == null ? {} : { fps: style.quality.fps }
8717
8887
  }
8718
8888
  } : {},
@@ -8848,7 +9018,7 @@ async function resolveSpecs(loaded, filter) {
8848
9018
  animation: sourceSpec.character.animation,
8849
9019
  outfit: {
8850
9020
  reference: {
8851
- path: path5.resolve(root, asset.outfit.reference),
9021
+ path: path6.resolve(root, asset.outfit.reference),
8852
9022
  sha256: referenceImage.hash,
8853
9023
  width: referenceImage.width,
8854
9024
  height: referenceImage.height,
@@ -8869,28 +9039,30 @@ async function resolveSpecs(loaded, filter) {
8869
9039
  `Provider "${activeProvider.id}" does not support ${asset.revision.mode} revisions`
8870
9040
  );
8871
9041
  }
8872
- const sourceSpec = await finalize(asset.revision.from);
8873
- const sourceStem = sourceSpec.quality?.outFile ?? (sourceSpec.source ? path5.resolve(root, sourceSpec.source) : sourceSpec.outFile);
9042
+ const sourceStyleId = asset.revision.fromStyle && asset.revision.fromStyle !== styleId ? asset.revision.fromStyle : void 0;
9043
+ const sourceSpec = sourceStyleId ? await styleFinalizers.get(sourceStyleId)(asset.revision.from) : await finalize(asset.revision.from);
9044
+ const sourceStem = sourceSpec.quality?.outFile ?? (sourceSpec.source ? path6.resolve(root, sourceSpec.source) : sourceSpec.outFile);
8874
9045
  const sourceMembers = await revisionSourceMembers(sourceStem, `revision source ${asset.revision.from}`);
8875
9046
  const sourceFile = sourceMembers?.[0]?.file ?? sourceStem;
8876
9047
  const sourceImage = sourceMembers ? { ...sourceMembers[0], hash: sha256(JSON.stringify(sourceMembers.map((member) => member.sha256))) } : await optionalRevisionImage(sourceFile, "revision source");
8877
9048
  const sourceFps = sourceSpec.character?.animation?.fps ?? sourceSpec.objectPro?.animation?.fps ?? sourceSpec.revision?.fps ?? sourceSpec.revision?.sourceFps;
8878
- const maskFile = asset.revision.mask ? path5.resolve(root, asset.revision.mask) : void 0;
9049
+ const maskFile = asset.revision.mask ? path6.resolve(root, asset.revision.mask) : void 0;
8879
9050
  const maskImage = maskFile ? await optionalRevisionImage(maskFile, "revision mask", "png") : null;
8880
9051
  if (sourceImage && maskImage && (sourceImage.width !== maskImage.width || sourceImage.height !== maskImage.height)) {
8881
9052
  throw new Error(
8882
9053
  `Revision mask for ${styleId}/${assetId} is ${maskImage.width}x${maskImage.height}; source ${asset.revision.from} is ${sourceImage.width}x${sourceImage.height}`
8883
9054
  );
8884
9055
  }
8885
- const paletteImageFile = asset.revision.paletteImage ? path5.resolve(root, asset.revision.paletteImage) : void 0;
9056
+ const paletteImageFile = asset.revision.paletteImage ? path6.resolve(root, asset.revision.paletteImage) : void 0;
8886
9057
  const paletteImage = paletteImageFile ? await optionalRevisionImage(paletteImageFile, "revision palette image", "png") : null;
8887
- const lastFrameFile = asset.revision.lastFrame ? path5.resolve(root, asset.revision.lastFrame) : void 0;
9058
+ const lastFrameFile = asset.revision.lastFrame ? path6.resolve(root, asset.revision.lastFrame) : void 0;
8888
9059
  const lastFrame = lastFrameFile ? await optionalRevisionImage(lastFrameFile, "revision last frame") : null;
8889
- const keypointsFile = asset.revision.keypointsFile ? path5.resolve(root, asset.revision.keypointsFile) : void 0;
9060
+ const keypointsFile = asset.revision.keypointsFile ? path6.resolve(root, asset.revision.keypointsFile) : void 0;
8890
9061
  const keypoints = keypointsFile ? await optionalSkeletonSet(keypointsFile) : null;
8891
9062
  resolved.revision = {
8892
9063
  mode: asset.revision.mode,
8893
9064
  sourceAssetId: asset.revision.from,
9065
+ ...sourceStyleId ? { sourceStyleId } : {},
8894
9066
  sourceFile,
8895
9067
  sourceSha256: sourceImage?.hash ?? null,
8896
9068
  sourceWidth: sourceImage?.width ?? null,
@@ -9012,6 +9184,8 @@ async function resolveSpecs(loaded, filter) {
9012
9184
  finalized.add(assetId);
9013
9185
  return resolved;
9014
9186
  };
9187
+ styleFinalizers.set(styleId, finalize);
9188
+ if (!emitStyles.has(styleId)) continue;
9015
9189
  for (const assetId of Object.keys(manifest.assets)) {
9016
9190
  if (!requestedAssetIds.has(assetId) || !styleSpecs.has(assetId)) continue;
9017
9191
  specs.push(await finalize(assetId));
@@ -9053,7 +9227,7 @@ async function resolveSpecs(loaded, filter) {
9053
9227
  return specs;
9054
9228
  }
9055
9229
  function pngPath(file) {
9056
- const extension = path5.extname(file);
9230
+ const extension = path6.extname(file);
9057
9231
  return extension ? `${file.slice(0, -extension.length)}.png` : `${file}.png`;
9058
9232
  }
9059
9233
  async function resolveMapScene(root, styleId, assetId, generator, scene, provider) {
@@ -9063,12 +9237,12 @@ async function resolveMapScene(root, styleId, assetId, generator, scene, provide
9063
9237
  if (!provider.supportsMapScene) {
9064
9238
  throw new Error(`assets.${assetId}.scene: provider "${provider.id}" cannot draw into a scene; PixelLab's /map-objects can`);
9065
9239
  }
9066
- const file = path5.resolve(root, scene.image);
9240
+ const file = path6.resolve(root, scene.image);
9067
9241
  const image = await optionalRevisionImage(file, "scene image", "png");
9068
9242
  const base = { file, sha256: image?.hash ?? null, width: image?.width ?? null, height: image?.height ?? null };
9069
9243
  const placement = scene.placement;
9070
9244
  if ("mask" in placement) {
9071
- const maskFile = path5.resolve(root, placement.mask);
9245
+ const maskFile = path6.resolve(root, placement.mask);
9072
9246
  const mask = await optionalRevisionImage(maskFile, "scene mask", "png");
9073
9247
  if (image && mask && (image.width !== mask.width || image.height !== mask.height)) {
9074
9248
  throw new Error(
@@ -9140,7 +9314,7 @@ async function resolveStyleImages(loaded, styleId) {
9140
9314
  if (!style) throw new Error(`Unknown style "${styleId}"`);
9141
9315
  const out = [];
9142
9316
  for (const img of style.styleImages) {
9143
- const file = path5.resolve(loaded.root, img.path);
9317
+ const file = path6.resolve(loaded.root, img.path);
9144
9318
  const buf = await readFile3(file);
9145
9319
  const metadata = imageMetadata(buf);
9146
9320
  if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${file}`);
@@ -9171,7 +9345,7 @@ async function resolveCharacterShape(asset, style, kind, files) {
9171
9345
  };
9172
9346
  if (asset.concept) {
9173
9347
  const image = await files.load(asset.concept, "Concept image");
9174
- shape.concept = { path: path5.resolve(files.root, asset.concept), sha256: image.hash, width: image.width, height: image.height, format: image.format };
9348
+ shape.concept = { path: path6.resolve(files.root, asset.concept), sha256: image.hash, width: image.width, height: image.height, format: image.format };
9175
9349
  }
9176
9350
  if (asset.reference) {
9177
9351
  const byDirection = typeof asset.reference === "string" ? { south: asset.reference } : asset.reference;
@@ -9179,7 +9353,7 @@ async function resolveCharacterShape(asset, style, kind, files) {
9179
9353
  for (const [direction, rel] of Object.entries(byDirection)) {
9180
9354
  const image = await files.load(rel, `Reference sprite (${direction})`);
9181
9355
  shape.reference[direction] = {
9182
- path: path5.resolve(files.root, rel),
9356
+ path: path6.resolve(files.root, rel),
9183
9357
  sha256: image.hash,
9184
9358
  width: image.width,
9185
9359
  height: image.height,
@@ -9200,7 +9374,7 @@ async function resolveCharacterShape(asset, style, kind, files) {
9200
9374
  const animationMode = asset.animation.mode ?? (asset.animation.template ? "template" : "v3");
9201
9375
  const pose = async (rel, what) => {
9202
9376
  const image = await files.load(rel, what);
9203
- return { path: path5.resolve(files.root, rel), sha256: image.hash, width: image.width, height: image.height, format: image.format };
9377
+ return { path: path6.resolve(files.root, rel), sha256: image.hash, width: image.width, height: image.height, format: image.format };
9204
9378
  };
9205
9379
  shape.animation = {
9206
9380
  mode: animationMode,
@@ -9231,7 +9405,7 @@ async function resolveObjectProShape(assetId, asset, style, kind, files) {
9231
9405
  const south = typeof asset.reference === "string" ? asset.reference : asset.reference.south;
9232
9406
  if (south) {
9233
9407
  const image = await files.load(south, "Reference sprite");
9234
- shape.reference = { path: path5.resolve(files.root, south), sha256: image.hash, width: image.width, height: image.height, format: image.format };
9408
+ shape.reference = { path: path6.resolve(files.root, south), sha256: image.hash, width: image.width, height: image.height, format: image.format };
9235
9409
  }
9236
9410
  }
9237
9411
  if (asset.state) {
@@ -9259,7 +9433,7 @@ async function resolveObjectProShape(assetId, asset, style, kind, files) {
9259
9433
  }
9260
9434
  const pose = async (rel, what) => {
9261
9435
  const image = await files.load(rel, what);
9262
- return { path: path5.resolve(files.root, rel), sha256: image.hash, width: image.width, height: image.height, format: image.format };
9436
+ return { path: path6.resolve(files.root, rel), sha256: image.hash, width: image.width, height: image.height, format: image.format };
9263
9437
  };
9264
9438
  shape.animation = {
9265
9439
  mode: asset.animation.mode ?? "v3",
@@ -9279,11 +9453,11 @@ function characterDirections(directions) {
9279
9453
  }
9280
9454
 
9281
9455
  // src/project.ts
9282
- import path13 from "path";
9456
+ import path14 from "path";
9283
9457
 
9284
9458
  // src/env.ts
9285
- import { readFileSync as readFileSync2, existsSync as existsSync5 } from "fs";
9286
- import path6 from "path";
9459
+ import { readFileSync as readFileSync3, existsSync as existsSync5 } from "fs";
9460
+ import path7 from "path";
9287
9461
  function loadEnvFiles(dir) {
9288
9462
  const loaded = [];
9289
9463
  for (const [file, vars] of envFiles(dir)) {
@@ -9297,10 +9471,10 @@ function loadEnvFiles(dir) {
9297
9471
  function envFiles(dir) {
9298
9472
  const out = [];
9299
9473
  for (const name of [".env.local", ".env"]) {
9300
- const file = path6.join(dir, name);
9474
+ const file = path7.join(dir, name);
9301
9475
  if (!existsSync5(file)) continue;
9302
9476
  try {
9303
- out.push([file, parseEnv(readFileSync2(file, "utf8"))]);
9477
+ out.push([file, parseEnv(readFileSync3(file, "utf8"))]);
9304
9478
  } catch {
9305
9479
  }
9306
9480
  }
@@ -9327,11 +9501,11 @@ function parseEnv(contents) {
9327
9501
 
9328
9502
  // src/pipeline/plan.ts
9329
9503
  import { existsSync as existsSync8 } from "fs";
9330
- import path12 from "path";
9504
+ import path13 from "path";
9331
9505
 
9332
9506
  // src/pipeline/mirror.ts
9333
9507
  import { mkdir as mkdir2, readFile as readFile4, rename as rename2, writeFile as writeFile2 } from "fs/promises";
9334
- import path7 from "path";
9508
+ import path8 from "path";
9335
9509
 
9336
9510
  // src/palette.ts
9337
9511
  function normalizePalette(values) {
@@ -9470,7 +9644,7 @@ async function deriveMirror(spec, lock, lockPath, opts = {}) {
9470
9644
  for (const [index, member] of members.entries()) {
9471
9645
  const bytes = flipHorizontal(await readFile4(member.file));
9472
9646
  const target = expectedOutputPath(spec, member.role, index, members.length, MediaType.PNG);
9473
- await mkdir2(path7.dirname(target), { recursive: true });
9647
+ await mkdir2(path8.dirname(target), { recursive: true });
9474
9648
  const tmp = `${target}.pixelkiln.tmp`;
9475
9649
  await writeFile2(tmp, bytes);
9476
9650
  await rename2(tmp, target);
@@ -9533,28 +9707,28 @@ async function deriveMirror(spec, lock, lockPath, opts = {}) {
9533
9707
 
9534
9708
  // src/pipeline/quality-profile.ts
9535
9709
  import { existsSync as existsSync6 } from "fs";
9536
- import path10 from "path";
9710
+ import path11 from "path";
9537
9711
 
9538
9712
  // src/pipeline/refine.ts
9539
- import path9 from "path";
9713
+ import path10 from "path";
9540
9714
  import { execFile } from "child_process";
9541
9715
  import { mkdtemp, readFile as readFile6, rm as rm3 } from "fs/promises";
9542
9716
  import { tmpdir } from "os";
9543
9717
  import { promisify } from "util";
9544
9718
 
9545
9719
  // src/artifacts.ts
9546
- import path8 from "path";
9547
- import { createHash as createHash3, randomUUID as randomUUID3 } from "crypto";
9720
+ import path9 from "path";
9721
+ import { createHash as createHash4, randomUUID as randomUUID3 } from "crypto";
9548
9722
  import { mkdir as mkdir3, readFile as readFile5, rename as rename3, rm as rm2, writeFile as writeFile3 } from "fs/promises";
9549
9723
  var activeTransactions = /* @__PURE__ */ new Set();
9550
9724
  function message(error) {
9551
9725
  return error instanceof Error ? error.message : String(error);
9552
9726
  }
9553
9727
  function digest(data) {
9554
- return createHash3("sha256").update(data).digest("hex");
9728
+ return createHash4("sha256").update(data).digest("hex");
9555
9729
  }
9556
9730
  function portableRelative(from, to) {
9557
- return path8.relative(from, path8.resolve(to)).split(path8.sep).join("/") || ".";
9731
+ return path9.relative(from, path9.resolve(to)).split(path9.sep).join("/") || ".";
9558
9732
  }
9559
9733
  function canonical2(value) {
9560
9734
  if (value === null || typeof value === "string" || typeof value === "boolean") return value;
@@ -9604,7 +9778,7 @@ function parseArtifactManifest(absolute, data) {
9604
9778
  return raw;
9605
9779
  }
9606
9780
  async function readArtifactBundleManifest(manifestPath) {
9607
- const absolute = path8.resolve(manifestPath);
9781
+ const absolute = path9.resolve(manifestPath);
9608
9782
  return parseArtifactManifest(absolute, await readFile5(absolute, "utf8"));
9609
9783
  }
9610
9784
  async function readOptional(file) {
@@ -9628,8 +9802,8 @@ function processIsAlive(pid) {
9628
9802
  }
9629
9803
  }
9630
9804
  function validTemporaryPath(candidate, destination, type) {
9631
- return path8.dirname(candidate) === path8.dirname(destination) && path8.basename(candidate).startsWith(
9632
- `.${path8.basename(destination)}.pixelkiln-${type}-`
9805
+ return path9.dirname(candidate) === path9.dirname(destination) && path9.basename(candidate).startsWith(
9806
+ `.${path9.basename(destination)}.pixelkiln-${type}-`
9633
9807
  );
9634
9808
  }
9635
9809
  function parseTransaction(journal, data) {
@@ -9649,7 +9823,7 @@ async function removeTransactionFiles(journal) {
9649
9823
  await rm2(transactionMarker(journal), { force: true });
9650
9824
  }
9651
9825
  async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
9652
- const journal = path8.resolve(recoveryFile);
9826
+ const journal = path9.resolve(recoveryFile);
9653
9827
  const bytes = await readOptional(journal);
9654
9828
  if (!bytes) {
9655
9829
  await rm2(transactionMarker(journal), { force: true });
@@ -9665,7 +9839,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
9665
9839
  );
9666
9840
  }
9667
9841
  for (const entry of transaction.entries) {
9668
- if (typeof entry.destination !== "string" || typeof entry.stage !== "string" || typeof entry.sha256 !== "string" || entry.backup !== void 0 && typeof entry.backup !== "string" || !allowedDestinations.has(path8.resolve(entry.destination)) || !validTemporaryPath(entry.stage, entry.destination, "stage") || entry.backup !== void 0 && !validTemporaryPath(entry.backup, entry.destination, "backup")) {
9842
+ if (typeof entry.destination !== "string" || typeof entry.stage !== "string" || typeof entry.sha256 !== "string" || entry.backup !== void 0 && typeof entry.backup !== "string" || !allowedDestinations.has(path9.resolve(entry.destination)) || !validTemporaryPath(entry.stage, entry.destination, "stage") || entry.backup !== void 0 && !validTemporaryPath(entry.backup, entry.destination, "backup")) {
9669
9843
  throw new Error(
9670
9844
  `Refusing unsafe artifact recovery from ${journal}; its destinations or temporary paths do not match the current bundle.`
9671
9845
  );
@@ -9711,7 +9885,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
9711
9885
  await removeTransactionFiles(journal);
9712
9886
  }
9713
9887
  function createArtifactBundleManifest(manifestPath, outputs, provenance) {
9714
- const root = path8.dirname(path8.resolve(manifestPath));
9888
+ const root = path9.dirname(path9.resolve(manifestPath));
9715
9889
  const sources = provenance.sources.map((source) => ({
9716
9890
  ...source,
9717
9891
  path: portableRelative(root, source.path)
@@ -9741,11 +9915,11 @@ function withArtifactManifest(manifestPath, outputs, provenance) {
9741
9915
  ];
9742
9916
  }
9743
9917
  async function writeManagedArtifactBundle(manifestPath, outputs, provenance, options = {}) {
9744
- const absoluteManifest = path8.resolve(manifestPath);
9918
+ const absoluteManifest = path9.resolve(manifestPath);
9745
9919
  const recoveryFile = `${absoluteManifest}.transaction`;
9746
9920
  const allowedDestinations = /* @__PURE__ */ new Set([
9747
9921
  absoluteManifest,
9748
- ...outputs.map((output) => path8.resolve(output.path))
9922
+ ...outputs.map((output) => path9.resolve(output.path))
9749
9923
  ]);
9750
9924
  await recoverArtifactTransaction(recoveryFile, allowedDestinations);
9751
9925
  const existingManifestBytes = await readOptional(absoluteManifest);
@@ -9763,13 +9937,13 @@ async function writeManagedArtifactBundle(manifestPath, outputs, provenance, opt
9763
9937
  }
9764
9938
  }
9765
9939
  if (!options.force) {
9766
- const root = path8.dirname(absoluteManifest);
9940
+ const root = path9.dirname(absoluteManifest);
9767
9941
  const recorded = new Map(
9768
- previous?.outputs.map((output) => [path8.resolve(root, output.path), output.sha256]) ?? []
9942
+ previous?.outputs.map((output) => [path9.resolve(root, output.path), output.sha256]) ?? []
9769
9943
  );
9770
9944
  const conflicts = [];
9771
9945
  for (const output of outputs) {
9772
- const destination = path8.resolve(output.path);
9946
+ const destination = path9.resolve(output.path);
9773
9947
  const current = await readOptional(destination);
9774
9948
  if (!current || digest(current) === digest(output.data)) continue;
9775
9949
  const expected = recorded.get(destination);
@@ -9791,15 +9965,15 @@ Pass { force: true } (CLI: --force) to take ownership and replace the complete b
9791
9965
  );
9792
9966
  }
9793
9967
  async function verifyArtifactBundle(manifestPath) {
9794
- const absolute = path8.resolve(manifestPath);
9968
+ const absolute = path9.resolve(manifestPath);
9795
9969
  const raw = await readArtifactBundleManifest(absolute);
9796
- const root = path8.dirname(absolute);
9970
+ const root = path9.dirname(absolute);
9797
9971
  const changedSources = [];
9798
9972
  const changedOutputs = [];
9799
9973
  for (const source of raw.sources) {
9800
9974
  let actual = null;
9801
9975
  try {
9802
- actual = digest(await readFile5(path8.resolve(root, source.path)));
9976
+ actual = digest(await readFile5(path9.resolve(root, source.path)));
9803
9977
  } catch (error) {
9804
9978
  if (!isCode(error, "ENOENT")) throw error;
9805
9979
  }
@@ -9807,7 +9981,7 @@ async function verifyArtifactBundle(manifestPath) {
9807
9981
  }
9808
9982
  for (const output of raw.outputs) {
9809
9983
  try {
9810
- if (digest(await readFile5(path8.resolve(root, output.path))) !== output.sha256) {
9984
+ if (digest(await readFile5(path9.resolve(root, output.path))) !== output.sha256) {
9811
9985
  changedOutputs.push(output.path);
9812
9986
  }
9813
9987
  } catch (error) {
@@ -9863,7 +10037,7 @@ async function writeArtifactBundle(files, options = {}) {
9863
10037
  if (!files.length) throw new Error("An artifact bundle must contain at least one file.");
9864
10038
  const normalized = files.map((file) => {
9865
10039
  if (!file.path.trim()) throw new Error("Artifact paths cannot be empty.");
9866
- return { destination: path8.resolve(file.path), data: Buffer.from(file.data) };
10040
+ return { destination: path9.resolve(file.path), data: Buffer.from(file.data) };
9867
10041
  });
9868
10042
  const destinations = /* @__PURE__ */ new Set();
9869
10043
  for (const artifact of normalized) {
@@ -9893,21 +10067,21 @@ async function writeArtifactBundle(files, options = {}) {
9893
10067
  if (!changed.length) return { changed: [], unchanged };
9894
10068
  const token = randomUUID3();
9895
10069
  for (const [index, artifact] of changed.entries()) {
9896
- const basename = path8.basename(artifact.destination);
9897
- artifact.stage = path8.join(
9898
- path8.dirname(artifact.destination),
10070
+ const basename = path9.basename(artifact.destination);
10071
+ artifact.stage = path9.join(
10072
+ path9.dirname(artifact.destination),
9899
10073
  `.${basename}.pixelkiln-stage-${token}-${index}`
9900
10074
  );
9901
10075
  if (artifact.existed) {
9902
- artifact.backup = path8.join(
9903
- path8.dirname(artifact.destination),
10076
+ artifact.backup = path9.join(
10077
+ path9.dirname(artifact.destination),
9904
10078
  `.${basename}.pixelkiln-backup-${token}-${index}`
9905
10079
  );
9906
10080
  }
9907
10081
  }
9908
- const journal = options.recoveryFile ? path8.resolve(options.recoveryFile) : null;
10082
+ const journal = options.recoveryFile ? path9.resolve(options.recoveryFile) : null;
9909
10083
  if (journal) {
9910
- await mkdir3(path8.dirname(journal), { recursive: true });
10084
+ await mkdir3(path9.dirname(journal), { recursive: true });
9911
10085
  const transaction = {
9912
10086
  format: "pixelkiln-artifact-transaction",
9913
10087
  version: 1,
@@ -9932,7 +10106,7 @@ async function writeArtifactBundle(files, options = {}) {
9932
10106
  }
9933
10107
  try {
9934
10108
  for (const [index, artifact] of changed.entries()) {
9935
- await mkdir3(path8.dirname(artifact.destination), { recursive: true });
10109
+ await mkdir3(path9.dirname(artifact.destination), { recursive: true });
9936
10110
  await options.beforeStage?.(artifact.destination, index);
9937
10111
  await writeFile3(artifact.stage, artifact.data, { flag: "wx" });
9938
10112
  }
@@ -10198,10 +10372,10 @@ function sameFrameGrid(first, next) {
10198
10372
  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;
10199
10373
  }
10200
10374
  async function refineAsset(options) {
10201
- const source = path9.resolve(options.source);
10202
- const output = path9.resolve(options.output);
10375
+ const source = path10.resolve(options.source);
10376
+ const output = path10.resolve(options.output);
10203
10377
  if (source === output) throw new Error("Refinement source and output must be different files.");
10204
- const record = path9.resolve(qualityRecordPath(output));
10378
+ const record = path10.resolve(qualityRecordPath(output));
10205
10379
  const palette = normalizePalette(options.palette).map((color) => color.hex);
10206
10380
  const minGridConfidence = requireConfidence(options.minGridConfidence ?? "high");
10207
10381
  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.");
@@ -10224,8 +10398,8 @@ async function refineAsset(options) {
10224
10398
  );
10225
10399
  }
10226
10400
  }
10227
- const temp = await mkdtemp(path9.join(tmpdir(), "pixelkiln-refine-"));
10228
- const recovered = path9.join(temp, "native.png");
10401
+ const temp = await mkdtemp(path10.join(tmpdir(), "pixelkiln-refine-"));
10402
+ const recovered = path10.join(temp, "native.png");
10229
10403
  const fixerCommand = compatibilityCommand ?? options.fixerPython ?? process.env.PIXELKILN_PIXEL_FIXER_PYTHON ?? "python3";
10230
10404
  const args = compatibilityCommand ? [...options.fixerArgsPrefix ?? [], source, "--extract", recovered] : ["-c", PYTHON_FIXER_BRIDGE, source, recovered];
10231
10405
  try {
@@ -10310,18 +10484,18 @@ async function refineFrameSet(options) {
10310
10484
  if (roles.some((role) => !role.trim()) || new Set(roles).size !== roles.length) {
10311
10485
  throw new Error("Frame-set roles must be non-empty and unique.");
10312
10486
  }
10313
- const output = path9.resolve(options.output);
10314
- const record = path9.resolve(qualityRecordPath(output));
10487
+ const output = path10.resolve(options.output);
10488
+ const record = path10.resolve(qualityRecordPath(output));
10315
10489
  const palette = normalizeRefinementPalette(options.palette);
10316
10490
  const finalOutputs = options.sources.map((source, index) => qualityFrameOutputPath(output, source.role, index));
10317
10491
  if (new Set(finalOutputs).size !== finalOutputs.length) {
10318
10492
  throw new Error("Frame-set roles resolve to duplicate output filenames.");
10319
10493
  }
10320
- const sourceSet = new Set(options.sources.map((source) => path9.resolve(source.path)));
10494
+ const sourceSet = new Set(options.sources.map((source) => path10.resolve(source.path)));
10321
10495
  if (finalOutputs.some((file) => sourceSet.has(file))) {
10322
10496
  throw new Error("Frame-set refinement sources and outputs must be different files.");
10323
10497
  }
10324
- const temp = await mkdtemp(path9.join(tmpdir(), "pixelkiln-refine-frames-"));
10498
+ const temp = await mkdtemp(path10.join(tmpdir(), "pixelkiln-refine-frames-"));
10325
10499
  try {
10326
10500
  const frames = [];
10327
10501
  const outputs = [];
@@ -10329,8 +10503,8 @@ async function refineFrameSet(options) {
10329
10503
  const sourcePaths = [];
10330
10504
  let firstOptions = null;
10331
10505
  for (const [index, sourceEntry] of options.sources.entries()) {
10332
- const source = path9.resolve(sourceEntry.path);
10333
- const temporaryOutput = path9.join(temp, `${String(index).padStart(2, "0")}.png`);
10506
+ const source = path10.resolve(sourceEntry.path);
10507
+ const temporaryOutput = path10.join(temp, `${String(index).padStart(2, "0")}.png`);
10334
10508
  const refined = await refineAsset({
10335
10509
  source,
10336
10510
  output: temporaryOutput,
@@ -10405,7 +10579,7 @@ async function refineFrameSet(options) {
10405
10579
  );
10406
10580
  return {
10407
10581
  sources: sourcePaths,
10408
- outputs: outputs.map((item) => path9.resolve(item.path)),
10582
+ outputs: outputs.map((item) => path10.resolve(item.path)),
10409
10583
  record,
10410
10584
  frames,
10411
10585
  palette,
@@ -10416,12 +10590,12 @@ async function refineFrameSet(options) {
10416
10590
  }
10417
10591
  }
10418
10592
  async function checkQualityRecord(recordPath) {
10419
- const record = path9.resolve(recordPath);
10593
+ const record = path10.resolve(recordPath);
10420
10594
  const manifest = await readArtifactBundleManifest(record);
10421
10595
  const options = parseRecordOptions(manifest, record);
10422
10596
  const verification = await verifyArtifactBundle(record);
10423
- const sources = manifest.sources.map((source2) => path9.resolve(path9.dirname(record), source2.path));
10424
- const outputs = manifest.outputs.map((output2) => path9.resolve(path9.dirname(record), output2.path));
10597
+ const sources = manifest.sources.map((source2) => path10.resolve(path10.dirname(record), source2.path));
10598
+ const outputs = manifest.outputs.map((output2) => path10.resolve(path10.dirname(record), output2.path));
10425
10599
  const source = sources[0] ?? "";
10426
10600
  const output = outputs[0] ?? "";
10427
10601
  const expectedCount = options.frameSet?.count ?? 1;
@@ -10454,7 +10628,7 @@ async function checkQualityRecord(recordPath) {
10454
10628
  async function approveQualityRecord(recordPath, approval) {
10455
10629
  const reviewer = approval.reviewer.trim();
10456
10630
  if (!reviewer) throw new Error("A human reviewer name is required.");
10457
- const record = path9.resolve(recordPath);
10631
+ const record = path10.resolve(recordPath);
10458
10632
  const manifest = await readArtifactBundleManifest(record);
10459
10633
  const options = parseRecordOptions(manifest, record);
10460
10634
  const verification = await verifyArtifactBundle(record);
@@ -10475,14 +10649,14 @@ async function approveQualityRecord(recordPath, approval) {
10475
10649
  `A refinement record must contain exactly ${expectedCount} source(s) and output(s).`
10476
10650
  );
10477
10651
  }
10478
- const root = path9.dirname(record);
10652
+ const root = path10.dirname(record);
10479
10653
  const outputs = await Promise.all(manifest.outputs.map(async (entry) => {
10480
- const outputPath = path9.resolve(root, entry.path);
10654
+ const outputPath = path10.resolve(root, entry.path);
10481
10655
  return { path: outputPath, data: await readFile6(outputPath) };
10482
10656
  }));
10483
10657
  const sources = manifest.sources.map((source) => ({
10484
10658
  ...source,
10485
- path: path9.resolve(root, source.path)
10659
+ path: path10.resolve(root, source.path)
10486
10660
  }));
10487
10661
  const approved = {
10488
10662
  ...options,
@@ -10514,7 +10688,7 @@ async function qualitySource(spec, lock) {
10514
10688
  if (spec.generator === "frames") {
10515
10689
  return { items: null, reason: "frame-set quality requires ordered downloaded provider outputs" };
10516
10690
  }
10517
- const source = path10.resolve(spec.root, spec.source);
10691
+ const source = path11.resolve(spec.root, spec.source);
10518
10692
  return existsSync6(source) ? { items: [{ role: "source", path: source }] } : { items: null, reason: `declared source is missing: ${spec.source}` };
10519
10693
  }
10520
10694
  const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
@@ -10581,7 +10755,7 @@ async function inspectQualityProfile(spec, lock) {
10581
10755
  const expectedRevision = spec.quality.fixerRevision ?? PIXEL_ART_FIXER_REVISION;
10582
10756
  const expectedTransparency = spec.quality.minTransparency ?? null;
10583
10757
  const expectedFrameSet = spec.generator === "frames" ? { fps: spec.quality.fps ?? 12, count: source.items.length, roles: source.items.map((item) => item.role) } : null;
10584
- const contractChanged = JSON.stringify(check.sources.map((item) => path10.resolve(item))) !== JSON.stringify(sourcePaths.map((item) => path10.resolve(item))) || JSON.stringify(check.outputs.map((item) => path10.resolve(item))) !== JSON.stringify(expectedOutputs.map((item) => path10.resolve(item))) || JSON.stringify(check.options.palette.colors) !== JSON.stringify(expectedPalette) || check.options.nativeGrid.revision !== expectedRevision || check.options.audit.thresholds.minGridConfidence !== spec.quality.minGridConfidence || check.options.audit.thresholds.minTransparency !== expectedTransparency || (expectedFrameSet ? !check.options.frameSet || check.options.frameSet.fps !== expectedFrameSet.fps || check.options.frameSet.count !== expectedFrameSet.count || JSON.stringify(check.options.frameSet.roles) !== JSON.stringify(expectedFrameSet.roles) : check.options.frameSet !== void 0);
10758
+ const contractChanged = JSON.stringify(check.sources.map((item) => path11.resolve(item))) !== JSON.stringify(sourcePaths.map((item) => path11.resolve(item))) || JSON.stringify(check.outputs.map((item) => path11.resolve(item))) !== JSON.stringify(expectedOutputs.map((item) => path11.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);
10585
10759
  if (contractChanged) {
10586
10760
  return {
10587
10761
  key,
@@ -10745,7 +10919,7 @@ async function requireApprovedQualitySources(specs, lock, assetIds, outputRoles)
10745
10919
 
10746
10920
  // src/pipeline/revision.ts
10747
10921
  import { existsSync as existsSync7 } from "fs";
10748
- import path11 from "path";
10922
+ import path12 from "path";
10749
10923
  async function inspectRevisionReadiness(spec, lock) {
10750
10924
  if (spec.mirror) return inspectMirror(spec, lock, /* @__PURE__ */ new Set());
10751
10925
  if (spec.character?.kind === "outfit") return inspectOutfit(spec, lock, /* @__PURE__ */ new Set());
@@ -10843,7 +11017,7 @@ async function inspectRevision(spec, lock, seen) {
10843
11017
  if (!dependency.ready) {
10844
11018
  return {
10845
11019
  ready: false,
10846
- reason: `revision source ${spec.styleId}/${revision.sourceAssetId} is not ready: ${dependency.reason}`
11020
+ reason: `revision source ${revision.sourceStyleId ?? spec.styleId}/${revision.sourceAssetId} is not ready: ${dependency.reason}`
10847
11021
  };
10848
11022
  }
10849
11023
  if (revision.sourceMembers) {
@@ -10888,8 +11062,8 @@ async function inspectSourceMembers(spec, lock) {
10888
11062
  if (parent.source) return { ready: true, reason: "declared source set is present" };
10889
11063
  const entry = lock.entries[lockKey(parent.styleId, parent.assetId)];
10890
11064
  if (!entry) return { ready: false, reason: "revision source is not in the lockfile" };
10891
- const recorded = entry.outputs.map((_, index) => path11.resolve(currentEntryOutputPath(entry, parent, index))).sort();
10892
- const found = members.map((member) => path11.resolve(member.file)).sort();
11065
+ const recorded = entry.outputs.map((_, index) => path12.resolve(currentEntryOutputPath(entry, parent, index))).sort();
11066
+ const found = members.map((member) => path12.resolve(member.file)).sort();
10893
11067
  if (recorded.length !== found.length || recorded.some((file, index) => file !== found[index])) {
10894
11068
  return {
10895
11069
  ready: false,
@@ -10938,7 +11112,7 @@ async function inspectParent(spec, lock, seen) {
10938
11112
  };
10939
11113
  }
10940
11114
  if (spec.source) {
10941
- const source = path11.resolve(spec.root, spec.source);
11115
+ const source = path12.resolve(spec.root, spec.source);
10942
11116
  return existsSync7(source) ? { ready: true, reason: "declared source is present" } : { ready: false, reason: `declared source is missing: ${spec.source}` };
10943
11117
  }
10944
11118
  const entry = lock.entries[key];
@@ -11017,7 +11191,7 @@ async function buildPlan(specs, lock, opts = {}) {
11017
11191
  state = "missing";
11018
11192
  reason = "--force";
11019
11193
  } else if (!entry && spec.source && spec.generator !== "frames") {
11020
- if (existsSync8(path12.resolve(spec.root, spec.source))) {
11194
+ if (existsSync8(path13.resolve(spec.root, spec.source))) {
11021
11195
  state = "ok";
11022
11196
  reason = `placed from ${spec.source}; not generated`;
11023
11197
  } else {
@@ -11159,13 +11333,13 @@ function summarize(plan) {
11159
11333
 
11160
11334
  // src/project.ts
11161
11335
  async function openProject(manifestPath = "pixelkiln.manifest.json", opts = {}) {
11162
- const absolute = path13.resolve(manifestPath);
11163
- const root = path13.dirname(absolute);
11336
+ const absolute = path14.resolve(manifestPath);
11337
+ const root = path14.dirname(absolute);
11164
11338
  if (opts.env !== false) {
11165
11339
  loadEnvFiles(root);
11166
- if (path13.resolve(process.cwd()) !== root) loadEnvFiles(process.cwd());
11340
+ if (path14.resolve(process.cwd()) !== root) loadEnvFiles(process.cwd());
11167
11341
  }
11168
- const lockPath = path13.resolve(opts.lockPath ?? path13.join(root, "pixelkiln.lock.json"));
11342
+ const lockPath = path14.resolve(opts.lockPath ?? path14.join(root, "pixelkiln.lock.json"));
11169
11343
  const loaded = await loadManifest(absolute);
11170
11344
  const specs = await resolveSpecs(loaded, { styles: opts.styles ?? [], assets: opts.assets ?? [] });
11171
11345
  const lock = await loadLock(lockPath);
@@ -11232,7 +11406,7 @@ function shouldPersistSourceUrl(value) {
11232
11406
  }
11233
11407
 
11234
11408
  // src/recipes.ts
11235
- import path14 from "path";
11409
+ import path15 from "path";
11236
11410
  import { existsSync as existsSync9 } from "fs";
11237
11411
  import { readdir, readFile as readFile7 } from "fs/promises";
11238
11412
  import { fileURLToPath } from "url";
@@ -11243,7 +11417,7 @@ var VERSION_RE = /^\d+\.\d+\.\d+$/;
11243
11417
  var RECIPE_FILE = "pixelkiln.recipe.json";
11244
11418
  var RECIPE_DIR_TOKEN = "{{recipeDir}}";
11245
11419
  var PortablePathSchema = z4.string().min(1).refine((value) => {
11246
- if (value.includes("\\") || path14.posix.isAbsolute(value)) return false;
11420
+ if (value.includes("\\") || path15.posix.isAbsolute(value)) return false;
11247
11421
  const parts = value.split("/");
11248
11422
  return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
11249
11423
  }, "expected a portable relative path without '.', '..', or backslashes");
@@ -11419,7 +11593,7 @@ function message3(error) {
11419
11593
  return error instanceof Error ? error.message : String(error);
11420
11594
  }
11421
11595
  async function readRecipeFile(recipePath, bundled) {
11422
- const absolute = path14.resolve(recipePath);
11596
+ const absolute = path15.resolve(recipePath);
11423
11597
  let raw;
11424
11598
  try {
11425
11599
  raw = JSON.parse(await readFile7(absolute, "utf8"));
@@ -11433,13 +11607,13 @@ async function readRecipeFile(recipePath, bundled) {
11433
11607
  ` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
11434
11608
  );
11435
11609
  }
11436
- return { recipe: parsed.data, authored: raw, path: absolute, dir: path14.dirname(absolute), bundled };
11610
+ return { recipe: parsed.data, authored: raw, path: absolute, dir: path15.dirname(absolute), bundled };
11437
11611
  }
11438
11612
  async function walkRecipeFiles(root) {
11439
11613
  if (!existsSync9(root)) return [];
11440
11614
  const out = [];
11441
11615
  for (const entry of await readdir(root, { withFileTypes: true })) {
11442
- const resolved = path14.join(root, entry.name);
11616
+ const resolved = path15.join(root, entry.name);
11443
11617
  if (entry.isDirectory()) out.push(...await walkRecipeFiles(resolved));
11444
11618
  else if (entry.isFile() && entry.name === RECIPE_FILE) out.push(resolved);
11445
11619
  }
@@ -11450,7 +11624,7 @@ function bundledRecipeRoot() {
11450
11624
  }
11451
11625
  async function listBundledRecipes(root = bundledRecipeRoot()) {
11452
11626
  const recipes = await Promise.all(
11453
- (await walkRecipeFiles(path14.resolve(root))).map((file) => readRecipeFile(file, true))
11627
+ (await walkRecipeFiles(path15.resolve(root))).map((file) => readRecipeFile(file, true))
11454
11628
  );
11455
11629
  return recipes.sort(
11456
11630
  (a, b) => a.recipe.id.localeCompare(b.recipe.id) || compareVersions(b.recipe.version, a.recipe.version)
@@ -11473,9 +11647,9 @@ function parseSelector(selector) {
11473
11647
  return { id: match[1], version: match[2] };
11474
11648
  }
11475
11649
  async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
11476
- const local = path14.resolve(target);
11650
+ const local = path15.resolve(target);
11477
11651
  if (existsSync9(local)) {
11478
- const recipePath = path14.basename(local) === RECIPE_FILE ? local : path14.join(local, RECIPE_FILE);
11652
+ const recipePath = path15.basename(local) === RECIPE_FILE ? local : path15.join(local, RECIPE_FILE);
11479
11653
  return readRecipeFile(recipePath, false);
11480
11654
  }
11481
11655
  if (target.startsWith(".") || target.startsWith("/") || target.includes("\\")) {
@@ -11489,7 +11663,7 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
11489
11663
  return matches[0];
11490
11664
  }
11491
11665
  async function verifyFile(root, file) {
11492
- const absolute = path14.resolve(root, ...file.path.split("/"));
11666
+ const absolute = path15.resolve(root, ...file.path.split("/"));
11493
11667
  if (!existsSync9(absolute)) {
11494
11668
  return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
11495
11669
  }
@@ -11505,7 +11679,7 @@ async function verifyRecipe(target, options = {}) {
11505
11679
  const loaded = await resolveRecipe(target, options.bundledRoot);
11506
11680
  const actualIntegrity = recipeDigest(loaded.authored);
11507
11681
  const files = await Promise.all(loaded.recipe.files.map((file) => verifyFile(loaded.dir, file)));
11508
- const modelRoot = options.modelRoot ? path14.resolve(options.modelRoot) : null;
11682
+ const modelRoot = options.modelRoot ? path15.resolve(options.modelRoot) : null;
11509
11683
  const models = modelRoot ? await Promise.all(loaded.recipe.models.map(async (model) => ({
11510
11684
  ...await verifyFile(modelRoot, model),
11511
11685
  source: model.source,
@@ -11539,7 +11713,7 @@ async function verifyRecipe(target, options = {}) {
11539
11713
  };
11540
11714
  }
11541
11715
  function renderStyle(style, destination, cwd) {
11542
- const portableDir = path14.relative(cwd, destination).split(path14.sep).join("/") || ".";
11716
+ const portableDir = path15.relative(cwd, destination).split(path15.sep).join("/") || ".";
11543
11717
  const replace = (value) => {
11544
11718
  if (typeof value === "string") return value.replaceAll(RECIPE_DIR_TOKEN, portableDir);
11545
11719
  if (Array.isArray(value)) return value.map(replace);
@@ -11551,19 +11725,19 @@ function renderStyle(style, destination, cwd) {
11551
11725
  return replace(style);
11552
11726
  }
11553
11727
  async function installRecipe(target, options = {}) {
11554
- const cwd = path14.resolve(options.cwd ?? process.cwd());
11728
+ const cwd = path15.resolve(options.cwd ?? process.cwd());
11555
11729
  const loaded = await resolveRecipe(target, options.bundledRoot);
11556
11730
  const verification = await verifyRecipe(loaded.path);
11557
11731
  if (!verification.ok) {
11558
11732
  throw new Error(`Recipe ${loaded.recipe.id}@${loaded.recipe.version} failed integrity verification.`);
11559
11733
  }
11560
- const destination = options.out ? path14.resolve(cwd, options.out) : path14.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
11734
+ const destination = options.out ? path15.resolve(cwd, options.out) : path15.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
11561
11735
  const sources = [
11562
11736
  { path: RECIPE_FILE, source: loaded.path },
11563
- ...loaded.recipe.files.map((file) => ({ path: file.path, source: path14.join(loaded.dir, ...file.path.split("/")) }))
11737
+ ...loaded.recipe.files.map((file) => ({ path: file.path, source: path15.join(loaded.dir, ...file.path.split("/")) }))
11564
11738
  ];
11565
11739
  const files = await Promise.all(sources.map(async (file) => ({
11566
- path: path14.join(destination, ...file.path.split("/")),
11740
+ path: path15.join(destination, ...file.path.split("/")),
11567
11741
  data: await readFile7(file.source)
11568
11742
  })));
11569
11743
  if (!options.force) {
@@ -11595,7 +11769,7 @@ async function installRecipe(target, options = {}) {
11595
11769
  // src/pipeline/audit.ts
11596
11770
  import { readFile as readFile8 } from "fs/promises";
11597
11771
  import { existsSync as existsSync10 } from "fs";
11598
- import path15 from "path";
11772
+ import path16 from "path";
11599
11773
  function paletteDistance(asset, reference) {
11600
11774
  if (!asset.length || !reference.length) return 0;
11601
11775
  let total = 0;
@@ -11660,7 +11834,7 @@ async function auditStyle(loaded, specs, styleId, lock) {
11660
11834
  let referenceFromStyleImages = false;
11661
11835
  const refPalettes = [];
11662
11836
  for (const img of style.styleImages) {
11663
- const abs = path15.resolve(loaded.root, img.path);
11837
+ const abs = path16.resolve(loaded.root, img.path);
11664
11838
  if (!existsSync10(abs)) continue;
11665
11839
  try {
11666
11840
  refPalettes.push(extractPalette(decodePng(await readFile8(abs)), 12));
@@ -11741,16 +11915,16 @@ function hex(c) {
11741
11915
  // src/pixellab-utils.ts
11742
11916
  import { existsSync as existsSync11 } from "fs";
11743
11917
  import { mkdir as mkdir4, readFile as readFile9, writeFile as writeFile4 } from "fs/promises";
11744
- import path16 from "path";
11918
+ import path17 from "path";
11745
11919
  var UNZOOM_MIN_SIDE = 256;
11746
11920
  var UNZOOM_MAX_AREA = 2048 * 2048;
11747
11921
  function defaultUnzoomOut(input) {
11748
- const ext = path16.extname(input);
11922
+ const ext = path17.extname(input);
11749
11923
  return `${ext ? input.slice(0, -ext.length) : input}.unzoomed.png`;
11750
11924
  }
11751
11925
  async function unzoomFile(client2, input, opts = {}) {
11752
- const out = path16.resolve(opts.out ?? defaultUnzoomOut(input));
11753
- if (path16.resolve(input) === out) throw new Error("unzoom will not overwrite its own input; pass a different --out");
11926
+ const out = path17.resolve(opts.out ?? defaultUnzoomOut(input));
11927
+ if (path17.resolve(input) === out) throw new Error("unzoom will not overwrite its own input; pass a different --out");
11754
11928
  if (existsSync11(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
11755
11929
  const bytes = await readFile9(input);
11756
11930
  const metadata = imageMetadata(bytes);
@@ -11765,7 +11939,7 @@ async function unzoomFile(client2, input, opts = {}) {
11765
11939
  throw new Error(`${input} is ${width}x${height}; PixelLab's unzoom takes at most 2048x2048 worth of pixels`);
11766
11940
  }
11767
11941
  const res = await client2.unzoom({ image: { base64: bytes.toString("base64"), format }, quantize: opts.quantize });
11768
- await mkdir4(path16.dirname(out), { recursive: true });
11942
+ await mkdir4(path17.dirname(out), { recursive: true });
11769
11943
  await writeFile4(out, res.png);
11770
11944
  return { out, original: res.originalSize, unzoomed: res.unzoomedSize, zoomFactor: res.zoomFactor, usage: res.usage };
11771
11945
  }
@@ -11773,7 +11947,7 @@ var FONT_WEIGHTS = ["Bold", "Regular"];
11773
11947
  var FONT_GLYPH_SIZES = [8, 16, 32, 64];
11774
11948
  var FONT_COST_GENERATIONS = 25;
11775
11949
  function fontOutputPaths(out) {
11776
- const base = path16.resolve(out.replace(/\.(ttf|png)$/i, ""));
11950
+ const base = path17.resolve(out.replace(/\.(ttf|png)$/i, ""));
11777
11951
  return { ttf: `${base}.ttf`, atlas: `${base}.png` };
11778
11952
  }
11779
11953
  async function generateFont(client2, opts) {
@@ -11807,7 +11981,7 @@ async function generateFont(client2, opts) {
11807
11981
  client2.download(job.download_ttf_url),
11808
11982
  client2.download(job.download_atlas_url)
11809
11983
  ]);
11810
- await mkdir4(path16.dirname(ttf), { recursive: true });
11984
+ await mkdir4(path17.dirname(ttf), { recursive: true });
11811
11985
  await writeFile4(ttf, ttfBytes);
11812
11986
  await writeFile4(atlas, atlasBytes);
11813
11987
  return { jobId, ttf, atlas, glyphPx: job.glyph_px ?? null, usage: job.usage };
@@ -11822,7 +11996,7 @@ async function generateFont(client2, opts) {
11822
11996
  }
11823
11997
 
11824
11998
  // src/pipeline/quality-regression.ts
11825
- import path17 from "path";
11999
+ import path18 from "path";
11826
12000
  import { existsSync as existsSync12 } from "fs";
11827
12001
  import { readFile as readFile10 } from "fs/promises";
11828
12002
  import { z as z5 } from "zod";
@@ -11830,7 +12004,7 @@ var SHA256_RE2 = /^[0-9a-f]{64}$/;
11830
12004
  var HEX_RE = /^#[0-9a-f]{6}$/;
11831
12005
  var MAX_REDMEAN_DISTANCE = 765;
11832
12006
  var QualityPathSchema = z5.string().min(1).refine(
11833
- (value) => !path17.posix.isAbsolute(value) && !value.includes("\\"),
12007
+ (value) => !path18.posix.isAbsolute(value) && !value.includes("\\"),
11834
12008
  "expected a portable relative path"
11835
12009
  );
11836
12010
  var DEFAULT_QUALITY_TOLERANCES = {
@@ -11977,7 +12151,7 @@ function measureDecodedImageQuality(png, digest2) {
11977
12151
  };
11978
12152
  }
11979
12153
  async function measureImageQuality(file) {
11980
- const absolute = path17.resolve(file);
12154
+ const absolute = path18.resolve(file);
11981
12155
  let bytes;
11982
12156
  try {
11983
12157
  bytes = await readFile10(absolute);
@@ -11999,11 +12173,11 @@ function resolveQualityInputs(raw, inputsFilePath) {
11999
12173
  }
12000
12174
  const ids = parsed.data.map((entry) => entry.id);
12001
12175
  if (new Set(ids).size !== ids.length) throw new Error("--inputs contains a duplicate quality case id.");
12002
- const root = path17.dirname(path17.resolve(inputsFilePath));
12176
+ const root = path18.dirname(path18.resolve(inputsFilePath));
12003
12177
  return parsed.data.map((entry) => ({
12004
12178
  id: entry.id,
12005
- file: path17.resolve(root, entry.path),
12006
- ...entry.record ? { record: path17.resolve(root, entry.record) } : {},
12179
+ file: path18.resolve(root, entry.path),
12180
+ ...entry.record ? { record: path18.resolve(root, entry.record) } : {},
12007
12181
  tolerances: QualityToleranceSchema.parse({
12008
12182
  ...DEFAULT_QUALITY_TOLERANCES,
12009
12183
  ...entry.tolerances
@@ -12011,7 +12185,7 @@ function resolveQualityInputs(raw, inputsFilePath) {
12011
12185
  }));
12012
12186
  }
12013
12187
  function portableRelative2(from, to) {
12014
- return path17.relative(from, path17.resolve(to)).split(path17.sep).join("/") || ".";
12188
+ return path18.relative(from, path18.resolve(to)).split(path18.sep).join("/") || ".";
12015
12189
  }
12016
12190
  async function verifyRecordForImage(recordPath, imagePath) {
12017
12191
  const verification = await verifyArtifactBundle(recordPath);
@@ -12026,20 +12200,20 @@ async function verifyRecordForImage(recordPath, imagePath) {
12026
12200
  if (record.kind !== "refine" || options === null || typeof options !== "object" || options.schema !== "pixelkiln-quality") {
12027
12201
  throw new Error(`Quality record ${recordPath} is not a PixelKiln refinement record.`);
12028
12202
  }
12029
- const outputs = record.outputs.map((output) => path17.resolve(path17.dirname(recordPath), output.path));
12030
- if (!outputs.includes(path17.resolve(imagePath))) {
12203
+ const outputs = record.outputs.map((output) => path18.resolve(path18.dirname(recordPath), output.path));
12204
+ if (!outputs.includes(path18.resolve(imagePath))) {
12031
12205
  throw new Error(`Quality record ${recordPath} does not own ${imagePath}.`);
12032
12206
  }
12033
12207
  }
12034
12208
  async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
12035
12209
  if (!inputs.length) throw new Error("Cannot snapshot an empty quality baseline.");
12036
- const absolute = path17.resolve(baselinePath);
12210
+ const absolute = path18.resolve(baselinePath);
12037
12211
  for (const input of inputs) {
12038
- if (path17.resolve(input.file) === absolute || input.record && path17.resolve(input.record) === absolute) {
12212
+ if (path18.resolve(input.file) === absolute || input.record && path18.resolve(input.record) === absolute) {
12039
12213
  throw new Error(`Quality baseline output would overwrite an input: ${absolute}.`);
12040
12214
  }
12041
12215
  }
12042
- const root = path17.dirname(absolute);
12216
+ const root = path18.dirname(absolute);
12043
12217
  const cases = [];
12044
12218
  for (const input of [...inputs].sort((a, b) => a.id.localeCompare(b.id))) {
12045
12219
  const expected = await measureImageQuality(input.file);
@@ -12079,7 +12253,7 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
12079
12253
  return { baseline, path: absolute, changed: result.changed.length > 0 };
12080
12254
  }
12081
12255
  async function readQualityBaseline(baselinePath) {
12082
- const absolute = path17.resolve(baselinePath);
12256
+ const absolute = path18.resolve(baselinePath);
12083
12257
  let raw;
12084
12258
  try {
12085
12259
  raw = JSON.parse(await readFile10(absolute, "utf8"));
@@ -12152,12 +12326,12 @@ function regressionViolations(expected, actual, tolerances) {
12152
12326
  return { violations, warnings };
12153
12327
  }
12154
12328
  async function checkQualityBaseline(baselinePath) {
12155
- const absolute = path17.resolve(baselinePath);
12329
+ const absolute = path18.resolve(baselinePath);
12156
12330
  const baseline = await readQualityBaseline(absolute);
12157
- const root = path17.dirname(absolute);
12331
+ const root = path18.dirname(absolute);
12158
12332
  const cases = [];
12159
12333
  for (const entry of baseline.cases) {
12160
- const file = path17.resolve(root, entry.file);
12334
+ const file = path18.resolve(root, entry.file);
12161
12335
  let actual = null;
12162
12336
  const violations = [];
12163
12337
  const warnings = [];
@@ -12170,7 +12344,7 @@ async function checkQualityBaseline(baselinePath) {
12170
12344
  violations.push(message4(error));
12171
12345
  }
12172
12346
  if (entry.record) {
12173
- const recordPath = path17.resolve(root, entry.record.path);
12347
+ const recordPath = path18.resolve(root, entry.record.path);
12174
12348
  try {
12175
12349
  const recordHash = await sha256File(recordPath);
12176
12350
  if (recordHash !== entry.record.sha256) violations.push("quality record hash changed");
@@ -12208,7 +12382,7 @@ async function checkQualityBaseline(baselinePath) {
12208
12382
  // src/pipeline/cache-health.ts
12209
12383
  import { existsSync as existsSync16 } from "fs";
12210
12384
  import { readFile as readFile13, readdir as readdir2, rm as rm6 } from "fs/promises";
12211
- import path20 from "path";
12385
+ import path21 from "path";
12212
12386
 
12213
12387
  // src/cache.ts
12214
12388
  import { mkdir as mkdir5, readFile as readFile11, rename as rename4, rm as rm4, writeFile as writeFile5 } from "fs/promises";
@@ -12227,10 +12401,10 @@ function isSha256Hash(value) {
12227
12401
  function parseCache(value) {
12228
12402
  return HashCacheSchema.parse(value);
12229
12403
  }
12230
- async function loadCache(path37) {
12231
- if (!existsSync13(path37)) return { version: 1, hashes: {} };
12404
+ async function loadCache(path38) {
12405
+ if (!existsSync13(path38)) return { version: 1, hashes: {} };
12232
12406
  try {
12233
- const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile11(path37, "utf8")));
12407
+ const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile11(path38, "utf8")));
12234
12408
  if (!parsed.success) return { version: 1, hashes: {} };
12235
12409
  return {
12236
12410
  version: 1,
@@ -12242,18 +12416,18 @@ async function loadCache(path37) {
12242
12416
  return { version: 1, hashes: {} };
12243
12417
  }
12244
12418
  }
12245
- async function saveCache(path37, cache) {
12419
+ async function saveCache(path38, cache) {
12246
12420
  const sorted = {};
12247
12421
  for (const key of Object.keys(cache.hashes).sort()) {
12248
12422
  const hash = cache.hashes[key];
12249
12423
  if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
12250
12424
  sorted[key] = hash;
12251
12425
  }
12252
- await mkdir5(pathModule.dirname(pathModule.resolve(path37)), { recursive: true });
12253
- const tmp = `${path37}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
12426
+ await mkdir5(pathModule.dirname(pathModule.resolve(path38)), { recursive: true });
12427
+ const tmp = `${path38}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
12254
12428
  try {
12255
12429
  await writeFile5(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
12256
- await rename4(tmp, path37);
12430
+ await rename4(tmp, path38);
12257
12431
  } finally {
12258
12432
  await rm4(tmp, { force: true });
12259
12433
  }
@@ -12278,19 +12452,19 @@ function cachePathFor(lockPath) {
12278
12452
 
12279
12453
  // src/pipeline/history.ts
12280
12454
  import { existsSync as existsSync15 } from "fs";
12281
- import path19 from "path";
12455
+ import path20 from "path";
12282
12456
 
12283
12457
  // src/pipeline/fetch.ts
12284
- import { existsSync as existsSync14, readFileSync as readFileSync3 } from "fs";
12458
+ import { existsSync as existsSync14, readFileSync as readFileSync4 } from "fs";
12285
12459
  import { mkdir as mkdir6, readFile as readFile12, rename as rename5, rm as rm5, writeFile as writeFile6 } from "fs/promises";
12286
- import path18 from "path";
12460
+ import path19 from "path";
12287
12461
  async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12288
12462
  const log = opts.onProgress ?? (() => {
12289
12463
  });
12290
12464
  const result = { downloaded: 0, skipped: 0, failed: 0, ...opts.refresh ? { unchanged: 0 } : {} };
12291
12465
  const specByKey = new Map(specs.map((s) => [lockKey(s.styleId, s.assetId), s]));
12292
12466
  normalizeLockOutputPaths(lock, specs);
12293
- const cacheDir = opts.cacheDir === false ? null : path18.resolve(opts.cacheDir ?? path18.join(path18.dirname(lockPath), ".pixelkiln", "cache"));
12467
+ const cacheDir = opts.cacheDir === false ? null : path19.resolve(opts.cacheDir ?? path19.join(path19.dirname(lockPath), ".pixelkiln", "cache"));
12294
12468
  const pending = Object.entries(lock.entries).filter(([key, e]) => {
12295
12469
  if (e.provider !== provider.id || !specByKey.has(key)) return false;
12296
12470
  if (specByKey.get(key)?.mirror) return false;
@@ -12301,7 +12475,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12301
12475
  if (spec && e.specHash === spec.specHash && e.outputs.length && !postprocessCurrent(e, spec)) {
12302
12476
  return Boolean(opts.force) || e.outputs.every((output) => {
12303
12477
  const file = resolveOutputPath(output.path, spec.root);
12304
- return existsSync14(file) && sha256(readFileSync3(file)) === output.sha256;
12478
+ return existsSync14(file) && sha256(readFileSync4(file)) === output.sha256;
12305
12479
  });
12306
12480
  }
12307
12481
  if (!opts.repair) return false;
@@ -12309,7 +12483,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12309
12483
  return e.outputs.some((output) => {
12310
12484
  const file = resolveOutputPath(output.path, spec.root);
12311
12485
  if (!existsSync14(file)) return true;
12312
- const current = sha256(readFileSync3(file));
12486
+ const current = sha256(readFileSync4(file));
12313
12487
  if (current === output.sha256) return false;
12314
12488
  return Boolean(opts.force) || (e.supersededOutputs ?? []).some((old) => old.sha256 === current);
12315
12489
  });
@@ -12392,7 +12566,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12392
12566
  if (!source.url) throw new Error(`no durable source URL to refresh ${source.role ?? "asset"} from`);
12393
12567
  fresh = await provider.download(source.url);
12394
12568
  if (recorded && sha256(fresh) === recordedRaw && !reapply) {
12395
- log(` current ${path18.relative(process.cwd(), target)}`);
12569
+ log(` current ${path19.relative(process.cwd(), target)}`);
12396
12570
  outputs.push({ ...recorded, path: portableOutputPath(target, spec.root) });
12397
12571
  continue;
12398
12572
  }
@@ -12416,9 +12590,9 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12416
12590
  }
12417
12591
  if (reapply && !fresh) {
12418
12592
  if (!recorded.raw) raw = await readFile12(target);
12419
- log(` reapply ${path18.relative(process.cwd(), target)}`);
12593
+ log(` reapply ${path19.relative(process.cwd(), target)}`);
12420
12594
  } else {
12421
- log(` changed ${path18.relative(process.cwd(), target)} (upstream)`);
12595
+ log(` changed ${path19.relative(process.cwd(), target)} (upstream)`);
12422
12596
  }
12423
12597
  } else {
12424
12598
  const superseded = (entry.supersededOutputs ?? []).find(
@@ -12446,7 +12620,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12446
12620
  }
12447
12621
  }
12448
12622
  log(
12449
- ` replace ${path18.relative(process.cwd(), target)}` + (opts.force ? " (--force)" : " (previous tracked generation)")
12623
+ ` replace ${path19.relative(process.cwd(), target)}` + (opts.force ? " (--force)" : " (previous tracked generation)")
12450
12624
  );
12451
12625
  }
12452
12626
  }
@@ -12455,7 +12629,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12455
12629
  } else if (!raw && recorded && cacheDir) {
12456
12630
  if (!reapply) final = await readCachedMedia(cacheDir, recorded.sha256, expectedMediaType);
12457
12631
  if (!final && recordedRaw) raw = await readCachedMedia(cacheDir, recordedRaw, expectedMediaType);
12458
- if (final || raw) log(` cached ${path18.relative(process.cwd(), target)}`);
12632
+ if (final || raw) log(` cached ${path19.relative(process.cwd(), target)}`);
12459
12633
  }
12460
12634
  if (!final && !raw) {
12461
12635
  if (!source.url) {
@@ -12482,11 +12656,11 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12482
12656
  buf = processed.bytes;
12483
12657
  if (processed.changed) {
12484
12658
  rawHash = sha256(raw);
12485
- log(` snapped ${path18.relative(process.cwd(), target)} to ${wanted.palette.colors.length} colours`);
12659
+ log(` snapped ${path19.relative(process.cwd(), target)} to ${wanted.palette.colors.length} colours`);
12486
12660
  }
12487
12661
  }
12488
12662
  if (cacheDir) await cacheMedia(cacheDir, buf, mediaType);
12489
- await mkdir6(path18.dirname(target), { recursive: true });
12663
+ await mkdir6(path19.dirname(target), { recursive: true });
12490
12664
  const tmp = `${target}.pixelkiln.tmp`;
12491
12665
  await writeFile6(tmp, buf);
12492
12666
  outputs.push({
@@ -12500,7 +12674,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12500
12674
  await saveLock(lockPath, lock);
12501
12675
  await rename5(tmp, target);
12502
12676
  wrote = true;
12503
- log(` wrote ${path18.relative(process.cwd(), target)}`);
12677
+ log(` wrote ${path19.relative(process.cwd(), target)}`);
12504
12678
  }
12505
12679
  if (opts.refresh && !wrote) {
12506
12680
  if (rotated) {
@@ -12544,7 +12718,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12544
12718
  return result;
12545
12719
  }
12546
12720
  async function readCachedMedia(cacheDir, hash, mediaType) {
12547
- const file = path18.join(cacheDir, cacheFileName(hash, mediaType));
12721
+ const file = path19.join(cacheDir, cacheFileName(hash, mediaType));
12548
12722
  if (!existsSync14(file)) return null;
12549
12723
  try {
12550
12724
  const buf = await readFile12(file);
@@ -12558,7 +12732,7 @@ async function readCachedMedia(cacheDir, hash, mediaType) {
12558
12732
  async function cacheMedia(cacheDir, buf, mediaType, knownHash) {
12559
12733
  const hash = knownHash ?? sha256(buf);
12560
12734
  validateMedia(buf, mediaType);
12561
- const file = path18.join(cacheDir, cacheFileName(hash, mediaType));
12735
+ const file = path19.join(cacheDir, cacheFileName(hash, mediaType));
12562
12736
  if (await readCachedMedia(cacheDir, hash, mediaType)) return;
12563
12737
  await mkdir6(cacheDir, { recursive: true });
12564
12738
  const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
@@ -12688,7 +12862,7 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
12688
12862
  if (!existsSync15(file)) continue;
12689
12863
  if (await sha256File(file) !== entry.outputs[i].sha256 && !opts.force) {
12690
12864
  throw new OverwriteRefusedError(
12691
- `${path19.relative(process.cwd(), file)} was changed after download; keep it with pixelkiln edit, or pass --force to replace it`,
12865
+ `${path20.relative(process.cwd(), file)} was changed after download; keep it with pixelkiln edit, or pass --force to replace it`,
12692
12866
  [file]
12693
12867
  );
12694
12868
  }
@@ -12741,10 +12915,10 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
12741
12915
  // src/pipeline/cache-health.ts
12742
12916
  async function inspectCaches(lock, lockPath, options = {}) {
12743
12917
  if (options.prune && !existsSync16(lockPath)) {
12744
- throw new Error(`Refusing to prune without an existing lockfile at ${path20.resolve(lockPath)}`);
12918
+ throw new Error(`Refusing to prune without an existing lockfile at ${path21.resolve(lockPath)}`);
12745
12919
  }
12746
- const contentDir = path20.resolve(path20.dirname(lockPath), ".pixelkiln", "cache");
12747
- const remotePath = path20.resolve(cachePathFor(lockPath));
12920
+ const contentDir = path21.resolve(path21.dirname(lockPath), ".pixelkiln", "cache");
12921
+ const remotePath = path21.resolve(cachePathFor(lockPath));
12748
12922
  const referenced = referencedHashes(lock);
12749
12923
  let content = await inspectContentCache(contentDir, referenced);
12750
12924
  let remoteHashes = await inspectRemoteHashCache(remotePath);
@@ -12756,7 +12930,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
12756
12930
  ]);
12757
12931
  for (const name of names) {
12758
12932
  try {
12759
- await rm6(path20.join(contentDir, name), { force: true });
12933
+ await rm6(path21.join(contentDir, name), { force: true });
12760
12934
  removed.contentFiles++;
12761
12935
  } catch {
12762
12936
  }
@@ -12804,7 +12978,7 @@ async function inspectContentCache(contentDir, referenced) {
12804
12978
  continue;
12805
12979
  }
12806
12980
  report.files++;
12807
- const file = path20.join(contentDir, entry.name);
12981
+ const file = path21.join(contentDir, entry.name);
12808
12982
  const mediaType = mediaTypeFromExtension(entry.name);
12809
12983
  const expected = mediaType ? entry.name.slice(0, -4) : "";
12810
12984
  let bytes;
@@ -12871,20 +13045,20 @@ async function inspectRemoteHashCache(remotePath) {
12871
13045
  }
12872
13046
 
12873
13047
  // src/pipeline/pack.ts
12874
- import { readFileSync as readFileSync4 } from "fs";
12875
- import path21 from "path";
13048
+ import { readFileSync as readFileSync5 } from "fs";
13049
+ import path22 from "path";
12876
13050
  var DEFAULT_FRAME_SET_FPS = 12;
12877
13051
  function resolvePackInputs(raw, inputsFilePath) {
12878
13052
  if (!Array.isArray(raw) || !raw.length) {
12879
13053
  throw new Error("--inputs must be a non-empty JSON array of { id, path }");
12880
13054
  }
12881
- const baseDir = path21.dirname(path21.resolve(inputsFilePath));
13055
+ const baseDir = path22.dirname(path22.resolve(inputsFilePath));
12882
13056
  return raw.map((entry, i) => {
12883
13057
  const e = entry;
12884
13058
  if (typeof e.id !== "string" || typeof e.path !== "string") {
12885
13059
  throw new Error(`--inputs[${i}] needs string "id" and "path"`);
12886
13060
  }
12887
- return { id: e.id, path: path21.resolve(baseDir, e.path) };
13061
+ return { id: e.id, path: path22.resolve(baseDir, e.path) };
12888
13062
  });
12889
13063
  }
12890
13064
  function packSprites(inputs, options = {}) {
@@ -12905,7 +13079,7 @@ function packSprites(inputs, options = {}) {
12905
13079
  for (const input of ordered) {
12906
13080
  let sourceHash = null;
12907
13081
  try {
12908
- const bytes = readFileSync4(input.path);
13082
+ const bytes = readFileSync5(input.path);
12909
13083
  sourceHash = sha256(bytes);
12910
13084
  const png = decodePng(bytes);
12911
13085
  sprites.push({ id: input.id, width: png.width, height: png.height, pixels: png.pixels });
@@ -12968,7 +13142,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
12968
13142
  }
12969
13143
  const inputs2 = Object.entries(options.sourceOverrides).map(([id, source]) => ({
12970
13144
  id,
12971
- path: path21.resolve(source)
13145
+ path: path22.resolve(source)
12972
13146
  }));
12973
13147
  if (!inputs2.length) throw new Error(`No approved quality outputs for style "${styleId}".`);
12974
13148
  const packed2 = packSprites(inputs2, options);
@@ -12992,7 +13166,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
12992
13166
  let outputs = resolveEntryOutputs(entry, id, manifestDir);
12993
13167
  const source = sources[id];
12994
13168
  if (source && outputs.length <= 1) {
12995
- inputs.push({ id, path: path21.resolve(manifestDir, source) });
13169
+ inputs.push({ id, path: path22.resolve(manifestDir, source) });
12996
13170
  continue;
12997
13171
  }
12998
13172
  if (source && sourceIsStem(source, entry, manifestDir)) {
@@ -13026,7 +13200,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
13026
13200
  }
13027
13201
  }
13028
13202
  for (const [id, source] of Object.entries(sources)) {
13029
- if (!locked.has(id)) inputs.push({ id, path: path21.resolve(manifestDir, source) });
13203
+ if (!locked.has(id)) inputs.push({ id, path: path22.resolve(manifestDir, source) });
13030
13204
  }
13031
13205
  if (!inputs.length) {
13032
13206
  throw new Error(`No readable sprites for style "${styleId}"; ${noOutput.length} skipped.`);
@@ -13063,7 +13237,7 @@ function mountSprites(placements, options) {
13063
13237
  for (const p of placements) {
13064
13238
  let sourceHash = null;
13065
13239
  try {
13066
- const bytes = readFileSync4(p.path);
13240
+ const bytes = readFileSync5(p.path);
13067
13241
  sourceHash = sha256(bytes);
13068
13242
  const png = decodePng(bytes);
13069
13243
  if (png.width > cellWidth || png.height > cellHeight) {
@@ -13134,7 +13308,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
13134
13308
  const source = sources[id];
13135
13309
  const entry = lock.entries[`${styleId}/${id}`];
13136
13310
  if (source && !(entry && sourceIsStem(source, entry, manifestDir))) {
13137
- placements.push({ id, path: path21.resolve(manifestDir, source), cell });
13311
+ placements.push({ id, path: path22.resolve(manifestDir, source), cell });
13138
13312
  continue;
13139
13313
  }
13140
13314
  if (source && entry) {
@@ -13167,7 +13341,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
13167
13341
  `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}`
13168
13342
  );
13169
13343
  }
13170
- const basePng = mount.base ? readFileSync4(path21.resolve(manifestDir, mount.base)) : void 0;
13344
+ const basePng = mount.base ? readFileSync5(path22.resolve(manifestDir, mount.base)) : void 0;
13171
13345
  const mounted = mountSprites(placements, {
13172
13346
  cellWidth: mount.cellWidth,
13173
13347
  cellHeight: mount.cellHeight,
@@ -13177,7 +13351,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
13177
13351
  ...mounted,
13178
13352
  sources: mount.base ? [{
13179
13353
  id: "$base",
13180
- path: path21.resolve(manifestDir, mount.base),
13354
+ path: path22.resolve(manifestDir, mount.base),
13181
13355
  sha256: sha256(basePng),
13182
13356
  included: true
13183
13357
  }, ...mounted.sources] : mounted.sources,
@@ -13754,6 +13928,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
13754
13928
  revision: spec.revision ? {
13755
13929
  mode: spec.revision.mode,
13756
13930
  sourceAssetId: spec.revision.sourceAssetId,
13931
+ ...spec.revision.sourceStyleId ? { sourceStyleId: spec.revision.sourceStyleId } : {},
13757
13932
  sourceSha256: spec.revision.sourceSha256,
13758
13933
  ...spec.revision.maskSha256 ? { maskSha256: spec.revision.maskSha256 } : {},
13759
13934
  ...spec.revision.strength == null ? {} : { strength: spec.revision.strength },
@@ -13989,7 +14164,7 @@ async function poll(provider, lock, lockPath, opts = {}) {
13989
14164
  // src/pipeline/doctor.ts
13990
14165
  import { constants, existsSync as existsSync17 } from "fs";
13991
14166
  import { access, stat as stat2 } from "fs/promises";
13992
- import path22 from "path";
14167
+ import path23 from "path";
13993
14168
  async function doctor(loaded, specs, lock, lockPath, opts = {}) {
13994
14169
  const checks = [];
13995
14170
  const add = (id, level, message6) => checks.push({ id, level, message: message6 });
@@ -14000,8 +14175,8 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
14000
14175
  );
14001
14176
  add("lock", "ok", `${Object.keys(lock.entries).length} valid v2 lock entr(ies)`);
14002
14177
  const dirs = [...new Set(specs.flatMap((spec) => [
14003
- path22.dirname(spec.outFile),
14004
- ...spec.quality ? [path22.dirname(spec.quality.outFile)] : []
14178
+ path23.dirname(spec.outFile),
14179
+ ...spec.quality ? [path23.dirname(spec.quality.outFile)] : []
14005
14180
  ]))];
14006
14181
  const unwritable = [];
14007
14182
  for (const dir of dirs) {
@@ -14024,7 +14199,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
14024
14199
  for (let index = 0; index < entry.outputs.length; index++) {
14025
14200
  const output = entry.outputs[index];
14026
14201
  const spec = specByKey.get(key);
14027
- const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, path22.dirname(lockPath));
14202
+ const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, path23.dirname(lockPath));
14028
14203
  const owner = outputOwners.get(absolute);
14029
14204
  if (owner && owner !== key) duplicateOutputs.push(`${output.path} (${owner}, ${key})`);
14030
14205
  outputOwners.set(absolute, key);
@@ -14039,7 +14214,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
14039
14214
  } else {
14040
14215
  add("lock-outputs", "ok", `${outputOwners.size} recorded output path(s) are uniquely owned`);
14041
14216
  }
14042
- const cacheDir = path22.join(path22.dirname(lockPath), ".pixelkiln", "cache");
14217
+ const cacheDir = path23.join(path23.dirname(lockPath), ".pixelkiln", "cache");
14043
14218
  const stranded = [];
14044
14219
  const invalidState = [];
14045
14220
  const staleMapUrls = [];
@@ -14055,7 +14230,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
14055
14230
  const hasSource = Boolean(entry.sourceUrl || entry.sourceUrls?.length);
14056
14231
  let hasCache = false;
14057
14232
  for (const output of entry.outputs) {
14058
- const cached = path22.join(
14233
+ const cached = path23.join(
14059
14234
  cacheDir,
14060
14235
  cacheFileName(output.sha256, output.mediaType ?? MediaType.PNG)
14061
14236
  );
@@ -14155,7 +14330,7 @@ async function nearestExistingDirectory(start) {
14155
14330
  if ((await stat2(current)).isDirectory()) return current;
14156
14331
  } catch {
14157
14332
  }
14158
- const parent = path22.dirname(current);
14333
+ const parent = path23.dirname(current);
14159
14334
  if (parent === current) return current;
14160
14335
  current = parent;
14161
14336
  }
@@ -14164,7 +14339,7 @@ async function nearestExistingDirectory(start) {
14164
14339
  // src/pipeline/adopt.ts
14165
14340
  import { existsSync as existsSync18 } from "fs";
14166
14341
  import { readFile as readFile14, writeFile as writeFile7 } from "fs/promises";
14167
- import path23 from "path";
14342
+ import path24 from "path";
14168
14343
  async function adopt(provider, specs, lock, lockPath, opts = {}) {
14169
14344
  const log = opts.onProgress ?? (() => {
14170
14345
  });
@@ -14303,7 +14478,7 @@ import { readFile as readFile17 } from "fs/promises";
14303
14478
  // src/pipeline/init.ts
14304
14479
  import { readdir as readdir3, readFile as readFile15, writeFile as writeFile8 } from "fs/promises";
14305
14480
  import { existsSync as existsSync19 } from "fs";
14306
- import path24 from "path";
14481
+ import path25 from "path";
14307
14482
  function pngSize(buf) {
14308
14483
  if (buf.length < 24) return null;
14309
14484
  const isPng = buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834;
@@ -14317,7 +14492,7 @@ function slugify(value) {
14317
14492
  async function walk(dir, exclude) {
14318
14493
  const out = [];
14319
14494
  for (const entry of await readdir3(dir, { withFileTypes: true })) {
14320
- const full = path24.join(dir, entry.name);
14495
+ const full = path25.join(dir, entry.name);
14321
14496
  if (exclude.some((x) => full.includes(x))) continue;
14322
14497
  if (entry.isDirectory()) out.push(...await walk(full, exclude));
14323
14498
  else if (entry.name.toLowerCase().endsWith(".png")) out.push(full);
@@ -14336,9 +14511,9 @@ async function scanAssets(root, opts = {}) {
14336
14511
  skipped.push(`${file} (not a readable PNG)`);
14337
14512
  continue;
14338
14513
  }
14339
- const rel = path24.relative(root, file);
14340
- const category = path24.dirname(rel) === "." ? "" : path24.dirname(rel);
14341
- let id = slugify(path24.basename(rel));
14514
+ const rel = path25.relative(root, file);
14515
+ const category = path25.dirname(rel) === "." ? "" : path25.dirname(rel);
14516
+ let id = slugify(path25.basename(rel));
14342
14517
  if (seen.has(id)) {
14343
14518
  const n = seen.get(id) + 1;
14344
14519
  seen.set(id, n);
@@ -15061,15 +15236,15 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
15061
15236
  }
15062
15237
 
15063
15238
  // src/gallery/snapshot.ts
15064
- import { createHash as createHash4 } from "crypto";
15239
+ import { createHash as createHash5 } from "crypto";
15065
15240
  import { existsSync as existsSync26 } from "fs";
15066
15241
  import { readFile as readFile24, stat as stat3 } from "fs/promises";
15067
- import path32 from "path";
15242
+ import path33 from "path";
15068
15243
 
15069
15244
  // src/workspace.ts
15070
15245
  import { mkdir as mkdir7, readFile as readFile18, rename as rename6, rm as rm7, writeFile as writeFile9 } from "fs/promises";
15071
15246
  import { existsSync as existsSync21 } from "fs";
15072
- import path25 from "path";
15247
+ import path26 from "path";
15073
15248
  import { z as z7 } from "zod";
15074
15249
  var WorkspaceProjectSchema = z7.object({
15075
15250
  id: z7.string().min(1),
@@ -15111,7 +15286,7 @@ async function saveWorkspace(workspacePath, ws) {
15111
15286
  version: 1,
15112
15287
  projects: [...ws.projects].sort((a, b) => a.id.localeCompare(b.id))
15113
15288
  };
15114
- await mkdir7(path25.dirname(path25.resolve(workspacePath)), { recursive: true });
15289
+ await mkdir7(path26.dirname(path26.resolve(workspacePath)), { recursive: true });
15115
15290
  const tmp = `${workspacePath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
15116
15291
  try {
15117
15292
  await writeFile9(tmp, JSON.stringify(sorted, null, 2) + "\n");
@@ -15121,12 +15296,12 @@ async function saveWorkspace(workspacePath, ws) {
15121
15296
  }
15122
15297
  }
15123
15298
  function toPortablePath(dir, absolute) {
15124
- return path25.relative(dir, absolute).split(path25.sep).join("/");
15299
+ return path26.relative(dir, absolute).split(path26.sep).join("/");
15125
15300
  }
15126
15301
  function resolveProject(dir, project) {
15127
15302
  return {
15128
- manifestPath: path25.resolve(dir, project.manifest.split("/").join(path25.sep)),
15129
- lockPath: path25.resolve(dir, project.lock.split("/").join(path25.sep))
15303
+ manifestPath: path26.resolve(dir, project.manifest.split("/").join(path26.sep)),
15304
+ lockPath: path26.resolve(dir, project.lock.split("/").join(path26.sep))
15130
15305
  };
15131
15306
  }
15132
15307
  function validateWorkspace(ws, dir) {
@@ -15139,7 +15314,7 @@ function validateWorkspace(ws, dir) {
15139
15314
  const { manifestPath, lockPath } = resolveProject(dir, project);
15140
15315
  lockOwners.set(lockPath, [...lockOwners.get(lockPath) ?? [], project.id]);
15141
15316
  manifestOwners.set(manifestPath, [...manifestOwners.get(manifestPath) ?? [], project.id]);
15142
- if (path25.isAbsolute(project.manifest) || path25.isAbsolute(project.lock)) {
15317
+ if (path26.isAbsolute(project.manifest) || path26.isAbsolute(project.lock)) {
15143
15318
  diagnostics.push({
15144
15319
  id: "absolute-path",
15145
15320
  level: "warning",
@@ -15201,15 +15376,15 @@ function validateWorkspace(ws, dir) {
15201
15376
 
15202
15377
  // src/gallery/edit.ts
15203
15378
  import { readFile as readFile23, rename as rename9, writeFile as writeFile15 } from "fs/promises";
15204
- import path31 from "path";
15379
+ import path32 from "path";
15205
15380
  import { z as z13 } from "zod";
15206
15381
 
15207
15382
  // src/manifest-edit.ts
15208
15383
  import { readFile as readFile19, rename as rename7, rm as rm8, writeFile as writeFile10 } from "fs/promises";
15209
- import path26 from "path";
15384
+ import path27 from "path";
15210
15385
  import { z as z8 } from "zod";
15211
15386
  var HexSha = z8.string().regex(/^[0-9a-f]{64}$/);
15212
- var ProjectFileSchema = z8.string().min(1).refine((file) => !path26.isAbsolute(file) && !file.split(/[\\/]/).includes(".."), "must be a path inside the project");
15387
+ var ProjectFileSchema = z8.string().min(1).refine((file) => !path27.isAbsolute(file) && !file.split(/[\\/]/).includes(".."), "must be a path inside the project");
15213
15388
  var AssetPatchSchema = z8.object({
15214
15389
  prompt: z8.string().optional(),
15215
15390
  /** Per-style override for the style being viewed; null removes it. */
@@ -15628,7 +15803,7 @@ function applyEdit(raw, edit) {
15628
15803
  }
15629
15804
  }
15630
15805
  async function withCandidate(manifestPath, edit, use, unchanged) {
15631
- const absolute = path26.resolve(manifestPath);
15806
+ const absolute = path27.resolve(manifestPath);
15632
15807
  const original = await readFile19(absolute, "utf8");
15633
15808
  if (sha256(original) !== edit.expectedSha256) throw new ManifestDriftError(absolute);
15634
15809
  let raw;
@@ -15642,9 +15817,9 @@ async function withCandidate(manifestPath, edit, use, unchanged) {
15642
15817
  applyEdit(raw, edit);
15643
15818
  const next = serializeLike(original, raw);
15644
15819
  if (next === original) return unchanged(absolute, original);
15645
- const tmp = path26.join(
15646
- path26.dirname(absolute),
15647
- `.${path26.basename(absolute)}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`
15820
+ const tmp = path27.join(
15821
+ path27.dirname(absolute),
15822
+ `.${path27.basename(absolute)}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`
15648
15823
  );
15649
15824
  try {
15650
15825
  await writeFile10(tmp, next);
@@ -15675,16 +15850,16 @@ async function applyManifestEdit(manifestPath, edit) {
15675
15850
  // src/pipeline/hand-edit.ts
15676
15851
  import { existsSync as existsSync22 } from "fs";
15677
15852
  import { copyFile, mkdir as mkdir8, readFile as readFile20, rename as rename8, rm as rm9, writeFile as writeFile11 } from "fs/promises";
15678
- import path27 from "path";
15853
+ import path28 from "path";
15679
15854
  import { z as z9 } from "zod";
15680
15855
  var HAND_EDIT_DIR = "edits";
15681
15856
  function handEditPath(loaded, spec) {
15682
15857
  const style = loaded.manifest.styles[spec.styleId];
15683
- const outDir = path27.resolve(loaded.root, style?.outDir ?? ".");
15684
- const relative = path27.relative(outDir, spec.outFile);
15685
- const inside = relative && !relative.startsWith("..") && !path27.isAbsolute(relative);
15686
- const file = inside ? relative : path27.basename(spec.outFile);
15687
- return path27.join(outDir, HAND_EDIT_DIR, file.replace(/\.gif$/i, ".png"));
15858
+ const outDir = path28.resolve(loaded.root, style?.outDir ?? ".");
15859
+ const relative = path28.relative(outDir, spec.outFile);
15860
+ const inside = relative && !relative.startsWith("..") && !path28.isAbsolute(relative);
15861
+ const file = inside ? relative : path28.basename(spec.outFile);
15862
+ return path28.join(outDir, HAND_EDIT_DIR, file.replace(/\.gif$/i, ".png"));
15688
15863
  }
15689
15864
  function handEditBases(spec, lock) {
15690
15865
  const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
@@ -15730,7 +15905,7 @@ async function startHandEdit(loaded, lock, spec, opts = {}) {
15730
15905
  `${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`
15731
15906
  );
15732
15907
  }
15733
- const existing = spec.source ? path27.resolve(loaded.root, spec.source) : null;
15908
+ const existing = spec.source ? path28.resolve(loaded.root, spec.source) : null;
15734
15909
  const editPath = existing ?? handEditPath(loaded, spec);
15735
15910
  const members = handEditMembers(loaded, spec, lock, editPath);
15736
15911
  let created = false;
@@ -15741,7 +15916,7 @@ async function startHandEdit(loaded, lock, spec, opts = {}) {
15741
15916
  `${spec.styleId}/${spec.assetId} has no generated PNG on disk to start from; generate or restore it first`
15742
15917
  );
15743
15918
  }
15744
- await mkdir8(path27.dirname(member.path), { recursive: true });
15919
+ await mkdir8(path28.dirname(member.path), { recursive: true });
15745
15920
  await copyFile(member.base, member.path);
15746
15921
  created = true;
15747
15922
  }
@@ -15879,7 +16054,7 @@ async function saveHandEdit(loaded, lock, spec, input) {
15879
16054
  editor: input.editor,
15880
16055
  protocol: input.protocol,
15881
16056
  savedAt: (input.now ?? /* @__PURE__ */ new Date()).toISOString(),
15882
- project: projectPath ? path27.basename(projectPath) : null,
16057
+ project: projectPath ? path28.basename(projectPath) : null,
15883
16058
  outputs: incoming.map((frame, index) => ({
15884
16059
  role: frame.role,
15885
16060
  basedOn: basedOnHash(entry, index),
@@ -15923,7 +16098,7 @@ function openInEditor(file, editor = process.env.PIXELKILN_EDITOR) {
15923
16098
  // src/gallery/upload.ts
15924
16099
  import { existsSync as existsSync23 } from "fs";
15925
16100
  import { mkdir as mkdir9, writeFile as writeFile12 } from "fs/promises";
15926
- import path28 from "path";
16101
+ import path29 from "path";
15927
16102
  import { z as z10 } from "zod";
15928
16103
  var MAX_UPLOAD_BYTES = 8 * 1024 * 1024;
15929
16104
  var UploadImageSchema = z10.object({
@@ -15936,12 +16111,12 @@ var UploadImageSchema = z10.object({
15936
16111
  overwrite: z10.boolean().optional()
15937
16112
  }).strict();
15938
16113
  async function saveProjectImage(root, request) {
15939
- const file = path28.resolve(root, request.path);
15940
- const relative = path28.relative(root, file);
15941
- if (relative.startsWith("..") || path28.isAbsolute(relative)) {
16114
+ const file = path29.resolve(root, request.path);
16115
+ const relative = path29.relative(root, file);
16116
+ if (relative.startsWith("..") || path29.isAbsolute(relative)) {
15942
16117
  throw new ManifestEditError("an upload must stay inside the project");
15943
16118
  }
15944
- const extension = path28.extname(file).toLowerCase();
16119
+ const extension = path29.extname(file).toLowerCase();
15945
16120
  const expected = extension === ".png" ? "png" : extension === ".jpg" || extension === ".jpeg" ? "jpeg" : null;
15946
16121
  if (!expected) throw new ManifestEditError("an upload must be a .png, .jpg, or .jpeg path");
15947
16122
  const bytes = Buffer.from(request.base64, "base64");
@@ -15954,17 +16129,17 @@ async function saveProjectImage(root, request) {
15954
16129
  throw new ManifestEditError(`the upload is a ${metadata.format.toUpperCase()}; name it ${metadata.format === "png" ? ".png" : ".jpg"}`);
15955
16130
  }
15956
16131
  if (existsSync23(file) && !request.overwrite) {
15957
- throw new ManifestEditError(`${relative.split(path28.sep).join("/")} already exists; choose another name, or replace it`);
16132
+ throw new ManifestEditError(`${relative.split(path29.sep).join("/")} already exists; choose another name, or replace it`);
15958
16133
  }
15959
- await mkdir9(path28.dirname(file), { recursive: true });
16134
+ await mkdir9(path29.dirname(file), { recursive: true });
15960
16135
  await writeFile12(file, bytes);
15961
- return relative.split(path28.sep).join("/");
16136
+ return relative.split(path29.sep).join("/");
15962
16137
  }
15963
16138
 
15964
16139
  // src/gallery/image-revision.ts
15965
16140
  import { existsSync as existsSync24 } from "fs";
15966
16141
  import { readFile as readFile21, rm as rm10, writeFile as writeFile13 } from "fs/promises";
15967
- import path29 from "path";
16142
+ import path30 from "path";
15968
16143
  import { z as z11 } from "zod";
15969
16144
  var InpaintRevisionRequestSchema = z11.object({
15970
16145
  action: z11.literal("create-inpaint-revision"),
@@ -15983,14 +16158,14 @@ var InpaintRevisionRequestSchema = z11.object({
15983
16158
  overwrite: z11.boolean().optional()
15984
16159
  }).strict();
15985
16160
  async function createInpaintRevision(manifestPath, request) {
15986
- const root = path29.dirname(path29.resolve(manifestPath));
15987
- if (path29.extname(request.maskPath).toLowerCase() !== ".png") {
16161
+ const root = path30.dirname(path30.resolve(manifestPath));
16162
+ if (path30.extname(request.maskPath).toLowerCase() !== ".png") {
15988
16163
  throw new ManifestEditError("the mask must be a .png path inside the project");
15989
16164
  }
15990
16165
  const bytes = Buffer.from(request.maskBase64, "base64");
15991
16166
  const metadata = imageMetadata(bytes);
15992
16167
  if (!metadata || metadata.format !== "png") throw new ManifestEditError("the mask is not a readable PNG");
15993
- const file = path29.resolve(root, request.maskPath);
16168
+ const file = path30.resolve(root, request.maskPath);
15994
16169
  const previous = existsSync24(file) ? await readFile21(file) : null;
15995
16170
  const mask = await saveProjectImage(root, {
15996
16171
  action: "upload-image",
@@ -16020,7 +16195,7 @@ async function createInpaintRevision(manifestPath, request) {
16020
16195
  // src/gallery/skeleton.ts
16021
16196
  import { existsSync as existsSync25 } from "fs";
16022
16197
  import { mkdir as mkdir10, readFile as readFile22, rm as rm11, writeFile as writeFile14 } from "fs/promises";
16023
- import path30 from "path";
16198
+ import path31 from "path";
16024
16199
  import { z as z12 } from "zod";
16025
16200
  var SkeletonEstimateRequestSchema = z12.object({
16026
16201
  project: z12.string().min(1).optional(),
@@ -16058,10 +16233,10 @@ var SkeletonKeypointsUpdateSchema = z12.object({
16058
16233
  set: SkeletonSetSchema
16059
16234
  }).strict();
16060
16235
  async function createSkeletonAnimation(manifestPath, request) {
16061
- const root = path30.dirname(path30.resolve(manifestPath));
16062
- const file = path30.resolve(root, request.keypointsFile);
16063
- const relative = path30.relative(root, file);
16064
- if (!request.keypointsFile.endsWith(".json") || relative.startsWith("..") || path30.isAbsolute(relative)) {
16236
+ const root = path31.dirname(path31.resolve(manifestPath));
16237
+ const file = path31.resolve(root, request.keypointsFile);
16238
+ const relative = path31.relative(root, file);
16239
+ if (!request.keypointsFile.endsWith(".json") || relative.startsWith("..") || path31.isAbsolute(relative)) {
16065
16240
  throw new ManifestEditError("the keypoints file must be a .json path inside the project");
16066
16241
  }
16067
16242
  const existed = existsSync25(file);
@@ -16071,7 +16246,7 @@ async function createSkeletonAnimation(manifestPath, request) {
16071
16246
  throw new ManifestEditError(`${request.keypointsFile} already exists; choose another path, or replace it`);
16072
16247
  }
16073
16248
  if (existed) previous = await readFile22(file);
16074
- await mkdir10(path30.dirname(file), { recursive: true });
16249
+ await mkdir10(path31.dirname(file), { recursive: true });
16075
16250
  await writeFile14(file, JSON.stringify(request.set, null, 2) + "\n");
16076
16251
  } else if (!existed) {
16077
16252
  throw new ManifestEditError(`${request.keypointsFile} does not exist; paste poses or estimate them from the sprite`);
@@ -16087,7 +16262,7 @@ async function createSkeletonAnimation(manifestPath, request) {
16087
16262
  revision: {
16088
16263
  mode: "animate-skeleton",
16089
16264
  from: request.from,
16090
- keypointsFile: relative.split(path30.sep).join("/"),
16265
+ keypointsFile: relative.split(path31.sep).join("/"),
16091
16266
  direction: request.direction,
16092
16267
  ...request.description ? { description: request.description } : {},
16093
16268
  ...request.skeletonTemplate ? { skeletonTemplate: request.skeletonTemplate } : {}
@@ -16108,19 +16283,19 @@ async function updateSkeletonKeypoints(ctx, request) {
16108
16283
  if (!spec) throw new ManifestEditError(`"${key}" is not declared by the manifest`);
16109
16284
  const file = spec.revision?.mode === "animate-skeleton" ? spec.revision.keypointsFile : void 0;
16110
16285
  if (!file) throw new ManifestEditError(`${key} is not an animate-skeleton revision`);
16111
- const relative = path30.relative(ctx.loaded.root, file);
16112
- if (relative.startsWith("..") || path30.isAbsolute(relative)) {
16286
+ const relative = path31.relative(ctx.loaded.root, file);
16287
+ if (relative.startsWith("..") || path31.isAbsolute(relative)) {
16113
16288
  throw new ManifestEditError(`${key}'s keypoints file is outside the project; edit it by hand`);
16114
16289
  }
16115
16290
  const current = existsSync25(file) ? sha256(await readFile22(file)) : null;
16116
16291
  if (current !== request.expectedSha256) {
16117
16292
  throw Object.assign(
16118
- new Error(`${relative.split(path30.sep).join("/")} changed on disk since this page loaded it; press Refresh and edit again`),
16293
+ new Error(`${relative.split(path31.sep).join("/")} changed on disk since this page loaded it; press Refresh and edit again`),
16119
16294
  { status: 409 }
16120
16295
  );
16121
16296
  }
16122
16297
  await writeFile14(file, JSON.stringify(request.set, null, 2) + "\n");
16123
- return relative.split(path30.sep).join("/");
16298
+ return relative.split(path31.sep).join("/");
16124
16299
  }
16125
16300
 
16126
16301
  // src/gallery/edit.ts
@@ -16174,7 +16349,7 @@ function createGalleryEditHandler(opts) {
16174
16349
  const history = /* @__PURE__ */ new Map();
16175
16350
  const manifestOf = async (project) => {
16176
16351
  try {
16177
- return path31.resolve(await opts.manifestFor(typeof project === "string" ? project : void 0));
16352
+ return path32.resolve(await opts.manifestFor(typeof project === "string" ? project : void 0));
16178
16353
  } catch {
16179
16354
  return null;
16180
16355
  }
@@ -16197,7 +16372,7 @@ function createGalleryEditHandler(opts) {
16197
16372
  await writeFile15(tmp, last.before);
16198
16373
  await rename9(tmp, manifestPath2);
16199
16374
  stack.pop();
16200
- log(` manifest edit undone in ${path31.relative(process.cwd(), manifestPath2) || manifestPath2}`);
16375
+ log(` manifest edit undone in ${path32.relative(process.cwd(), manifestPath2) || manifestPath2}`);
16201
16376
  return opts.reload();
16202
16377
  }
16203
16378
  const manifestPath = await manifestOf(body?.project);
@@ -16259,7 +16434,7 @@ function createEditApplier(opts, log) {
16259
16434
  } catch (error) {
16260
16435
  throw new ManifestEditError(error instanceof Error ? error.message : String(error));
16261
16436
  }
16262
- const written = await saveProjectImage(path31.dirname(path31.resolve(manifestPath2)), upload.data);
16437
+ const written = await saveProjectImage(path32.dirname(path32.resolve(manifestPath2)), upload.data);
16263
16438
  log(` image added: ${written}`);
16264
16439
  return opts.reload();
16265
16440
  }
@@ -16326,7 +16501,7 @@ function createEditApplier(opts, log) {
16326
16501
  }
16327
16502
  const result = await applyManifestEdit(manifestPath, parsed.data);
16328
16503
  if (result.changed) {
16329
- const relative = path31.relative(process.cwd(), result.manifestPath);
16504
+ const relative = path32.relative(process.cwd(), result.manifestPath);
16330
16505
  const shown = !relative || relative.startsWith("..") ? result.manifestPath : relative;
16331
16506
  log(` manifest edited: ${describeEdit(parsed.data)} in ${shown}`);
16332
16507
  }
@@ -16354,7 +16529,7 @@ function describeEdit(edit) {
16354
16529
  // src/gallery/snapshot.ts
16355
16530
  var PROJECT_FILE_TYPE = "application/octet-stream";
16356
16531
  function galleryMediaId(absolutePath) {
16357
- return createHash4("sha256").update(path32.resolve(absolutePath)).digest("hex").slice(0, 24);
16532
+ return createHash5("sha256").update(path33.resolve(absolutePath)).digest("hex").slice(0, 24);
16358
16533
  }
16359
16534
  function galleryMediaRoute(id) {
16360
16535
  return `/media/${id}`;
@@ -16373,7 +16548,7 @@ function describeHistory(media, entry, cacheDir) {
16373
16548
  return (entry.history ?? []).map((generation, i) => {
16374
16549
  const outputs = generation.outputs.map((output) => {
16375
16550
  const mediaType = output.mediaType ?? MediaType.PNG;
16376
- const file = path32.join(cacheDir, cacheFileName(output.sha256, mediaType));
16551
+ const file = path33.join(cacheDir, cacheFileName(output.sha256, mediaType));
16377
16552
  let url = null;
16378
16553
  if (existsSync26(file)) {
16379
16554
  const id = galleryMediaId(file);
@@ -16505,9 +16680,9 @@ async function describeOutput(media, root, absolutePath, recorded = {}) {
16505
16680
  async function describeQuality(media, root, inspection) {
16506
16681
  const outputPaths = inspection.outputs ?? [inspection.output];
16507
16682
  const outputs = await Promise.all(
16508
- outputPaths.map((file) => describeOutput(media, root, path32.resolve(root, file)))
16683
+ outputPaths.map((file) => describeOutput(media, root, path33.resolve(root, file)))
16509
16684
  );
16510
- const recordPath = path32.resolve(root, inspection.record);
16685
+ const recordPath = path33.resolve(root, inspection.record);
16511
16686
  const recordExists = existsSync26(recordPath);
16512
16687
  let options = null;
16513
16688
  let check = null;
@@ -16527,7 +16702,7 @@ async function describeQuality(media, root, inspection) {
16527
16702
  return {
16528
16703
  state: inspection.state,
16529
16704
  reason: inspection.reason,
16530
- output: portableOutputPath(path32.resolve(root, inspection.output), root),
16705
+ output: portableOutputPath(path33.resolve(root, inspection.output), root),
16531
16706
  record: portableOutputPath(recordPath, root),
16532
16707
  recordExists,
16533
16708
  outputs,
@@ -16567,7 +16742,7 @@ async function buildGallerySnapshot(opts) {
16567
16742
  const items = [];
16568
16743
  const manifestText = await readFile24(loaded.path, "utf8");
16569
16744
  const manifestSha256 = sha256(manifestText);
16570
- const cacheDir = path32.resolve(path32.dirname(opts.lockPath), ".pixelkiln", "cache");
16745
+ const cacheDir = path33.resolve(path33.dirname(opts.lockPath), ".pixelkiln", "cache");
16571
16746
  let rawStyles = {};
16572
16747
  try {
16573
16748
  const raw = JSON.parse(manifestText);
@@ -16591,7 +16766,7 @@ async function buildGallerySnapshot(opts) {
16591
16766
  )
16592
16767
  );
16593
16768
  if (spec.source) {
16594
- const editPath = path32.resolve(root, spec.source);
16769
+ const editPath = path33.resolve(root, spec.source);
16595
16770
  const memberCount = sourceIsStem(spec.source, entry, root) ? entry.outputs.length : 1;
16596
16771
  const editShas = await Promise.all(
16597
16772
  Array.from({ length: memberCount }, async (_, index) => {
@@ -16640,7 +16815,7 @@ async function buildGallerySnapshot(opts) {
16640
16815
  editStatus = edits.some((member) => !member.exists) ? "missing" : editChanged.every((changed) => !changed) ? "same" : regenerated ? "regenerated-since" : "edited";
16641
16816
  }
16642
16817
  } else if (spec.source) {
16643
- outputs = [await describeOutput(media, root, path32.resolve(root, spec.source))];
16818
+ outputs = [await describeOutput(media, root, path33.resolve(root, spec.source))];
16644
16819
  } else if (!entry && existsSync26(spec.outFile)) {
16645
16820
  outputs = [await describeOutput(media, root, spec.outFile)];
16646
16821
  } else {
@@ -16681,7 +16856,7 @@ async function buildGallerySnapshot(opts) {
16681
16856
  recordedSpecHash: entry?.specHash ?? null,
16682
16857
  currentSpecHash: spec.specHash,
16683
16858
  revision: entry?.revision ?? null,
16684
- revisionParentKey: spec.revision ? lockKey(spec.styleId, spec.revision.sourceAssetId) : entry?.revision ? lockKey(spec.styleId, entry.revision.sourceAssetId) : null,
16859
+ revisionParentKey: spec.revision ? lockKey(spec.revision.sourceStyleId ?? spec.styleId, spec.revision.sourceAssetId) : entry?.revision ? lockKey(entry.revision.sourceStyleId ?? spec.styleId, entry.revision.sourceAssetId) : null,
16685
16860
  mirrorOfKey: spec.mirror ? lockKey(spec.styleId, spec.mirror.sourceAssetId) : entry?.mirror ? lockKey(spec.styleId, entry.mirror.sourceAssetId) : null,
16686
16861
  skeleton: spec.revision?.mode === "animate-skeleton" && spec.revision.keypointsFile ? {
16687
16862
  keypointsFile: portableOutputPath(spec.revision.keypointsFile, root),
@@ -16751,7 +16926,7 @@ async function buildGallerySnapshot(opts) {
16751
16926
  recordedSpecHash: entry.specHash,
16752
16927
  currentSpecHash: null,
16753
16928
  revision: entry.revision,
16754
- revisionParentKey: entry.revision ? lockKey(entry.styleId, entry.revision.sourceAssetId) : null,
16929
+ revisionParentKey: entry.revision ? lockKey(entry.revision.sourceStyleId ?? entry.styleId, entry.revision.sourceAssetId) : null,
16755
16930
  mirrorOfKey: entry.mirror ? lockKey(entry.styleId, entry.mirror.sourceAssetId) : null,
16756
16931
  skeleton: null,
16757
16932
  character: describeCharacter(void 0, entry),
@@ -16844,7 +17019,7 @@ async function buildGallerySnapshot(opts) {
16844
17019
  id: loaded.manifest.name,
16845
17020
  name: loaded.manifest.name,
16846
17021
  manifest: loaded.path,
16847
- lock: path32.resolve(opts.lockPath),
17022
+ lock: path33.resolve(opts.lockPath),
16848
17023
  root,
16849
17024
  provider: loaded.manifest.provider,
16850
17025
  account: null,
@@ -16870,7 +17045,7 @@ async function buildGallerySnapshot(opts) {
16870
17045
  return { snapshot, media };
16871
17046
  }
16872
17047
  async function buildWorkspaceGallerySnapshot(opts) {
16873
- const dir = path32.dirname(path32.resolve(opts.workspacePath));
17048
+ const dir = path33.dirname(path33.resolve(opts.workspacePath));
16874
17049
  const media = /* @__PURE__ */ new Map();
16875
17050
  const projects = [];
16876
17051
  const items = [];
@@ -16913,7 +17088,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
16913
17088
  id: project.id,
16914
17089
  name: loaded.manifest.name,
16915
17090
  manifest: manifestPath,
16916
- lock: path32.resolve(lockPath),
17091
+ lock: path33.resolve(lockPath),
16917
17092
  root: loaded.root,
16918
17093
  provider: loaded.manifest.provider,
16919
17094
  account: project.account ?? null,
@@ -16929,8 +17104,8 @@ async function buildWorkspaceGallerySnapshot(opts) {
16929
17104
  id: project.id,
16930
17105
  name: project.id,
16931
17106
  manifest: manifestPath,
16932
- lock: path32.resolve(lockPath),
16933
- root: path32.dirname(manifestPath),
17107
+ lock: path33.resolve(lockPath),
17108
+ root: path33.dirname(manifestPath),
16934
17109
  provider: project.provider,
16935
17110
  account: project.account ?? null,
16936
17111
  manifestSha256: null,
@@ -16957,7 +17132,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
16957
17132
  version: 1,
16958
17133
  generatedAt: (opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
16959
17134
  project: null,
16960
- workspace: { path: path32.resolve(opts.workspacePath), dir, projects },
17135
+ workspace: { path: path33.resolve(opts.workspacePath), dir, projects },
16961
17136
  filter,
16962
17137
  totals: {
16963
17138
  entries: projects.reduce((sum, project) => sum + project.entries, 0),
@@ -16974,7 +17149,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
16974
17149
  }
16975
17150
 
16976
17151
  // src/gallery/page.ts
16977
- import { existsSync as existsSync27, readFileSync as readFileSync5 } from "fs";
17152
+ import { existsSync as existsSync27, readFileSync as readFileSync6 } from "fs";
16978
17153
 
16979
17154
  // src/gallery/client-bundle.ts
16980
17155
  import { createRequire } from "module";
@@ -17014,8 +17189,8 @@ var client;
17014
17189
  function clientAssets() {
17015
17190
  const prebuilt = new URL("gallery.js", CLIENT_DIR);
17016
17191
  client ??= {
17017
- css: readFileSync5(new URL("gallery.css", CLIENT_DIR), "utf8"),
17018
- js: existsSync27(prebuilt) ? readFileSync5(prebuilt, "utf8") : bundleGalleryClient()
17192
+ css: readFileSync6(new URL("gallery.css", CLIENT_DIR), "utf8"),
17193
+ js: existsSync27(prebuilt) ? readFileSync6(prebuilt, "utf8") : bundleGalleryClient()
17019
17194
  };
17020
17195
  return client;
17021
17196
  }
@@ -17157,11 +17332,11 @@ var EDITOR_PIN = pin_default;
17157
17332
  var EDITOR_RELEASE_BASE = "https://github.com/gfargo/pixelkiln/releases/download";
17158
17333
 
17159
17334
  // src/editor/install.ts
17160
- import { createHash as createHash5 } from "crypto";
17335
+ import { createHash as createHash6 } from "crypto";
17161
17336
  import { existsSync as existsSync28 } from "fs";
17162
17337
  import { mkdir as mkdir11, readFile as readFile25, rename as rename10, rm as rm12, stat as stat4, writeFile as writeFile16 } from "fs/promises";
17163
- import os2 from "os";
17164
- import path33 from "path";
17338
+ import os3 from "os";
17339
+ import path34 from "path";
17165
17340
  var EditorInstallError = class extends Error {
17166
17341
  constructor(message6, file) {
17167
17342
  super(message6);
@@ -17172,15 +17347,15 @@ var EditorInstallError = class extends Error {
17172
17347
  };
17173
17348
  function editorToolsRoot() {
17174
17349
  const override = process.env.PIXELKILN_TOOLS_DIR?.trim();
17175
- if (override) return path33.resolve(override);
17176
- if (process.platform === "darwin") return path33.join(os2.homedir(), "Library", "Caches", "pixelkiln", "tools");
17350
+ if (override) return path34.resolve(override);
17351
+ if (process.platform === "darwin") return path34.join(os3.homedir(), "Library", "Caches", "pixelkiln", "tools");
17177
17352
  if (process.platform === "win32") {
17178
- return path33.join(process.env.LOCALAPPDATA ?? path33.join(os2.homedir(), "AppData", "Local"), "pixelkiln", "tools");
17353
+ return path34.join(process.env.LOCALAPPDATA ?? path34.join(os3.homedir(), "AppData", "Local"), "pixelkiln", "tools");
17179
17354
  }
17180
- return path33.join(process.env.XDG_CACHE_HOME ?? path33.join(os2.homedir(), ".cache"), "pixelkiln", "tools");
17355
+ return path34.join(process.env.XDG_CACHE_HOME ?? path34.join(os3.homedir(), ".cache"), "pixelkiln", "tools");
17181
17356
  }
17182
17357
  function editorDir(pin = EDITOR_PIN, root = editorToolsRoot()) {
17183
- return path33.join(root, "pixelorama", pin.release ?? "unpublished");
17358
+ return path34.join(root, "pixelorama", pin.release ?? "unpublished");
17184
17359
  }
17185
17360
  function editorBaseUrl(pin = EDITOR_PIN) {
17186
17361
  const override = process.env.PIXELKILN_EDITOR_URL?.trim();
@@ -17190,7 +17365,7 @@ function editorBaseUrl(pin = EDITOR_PIN) {
17190
17365
  }
17191
17366
  async function sha256Of(file) {
17192
17367
  try {
17193
- return createHash5("sha256").update(await readFile25(file)).digest("hex");
17368
+ return createHash6("sha256").update(await readFile25(file)).digest("hex");
17194
17369
  } catch {
17195
17370
  return null;
17196
17371
  }
@@ -17203,7 +17378,7 @@ async function editorStatus(opts = {}) {
17203
17378
  let totalBytes = 0;
17204
17379
  for (const [name, expected] of Object.entries(pin.files)) {
17205
17380
  totalBytes += expected.bytes;
17206
- const file = path33.join(dir, name);
17381
+ const file = path34.join(dir, name);
17207
17382
  const info = existsSync28(file) ? await stat4(file).catch(() => null) : null;
17208
17383
  if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
17209
17384
  installedBytes += expected.bytes;
@@ -17257,11 +17432,11 @@ async function installEditor(opts = {}) {
17257
17432
  if (bytes.length !== expected.bytes) {
17258
17433
  throw new EditorInstallError(`${name}: expected ${expected.bytes} bytes, got ${bytes.length}`, name);
17259
17434
  }
17260
- const digest2 = createHash5("sha256").update(bytes).digest("hex");
17435
+ const digest2 = createHash6("sha256").update(bytes).digest("hex");
17261
17436
  if (digest2 !== expected.sha256) {
17262
17437
  throw new EditorInstallError(`${name}: sha256 ${digest2.slice(0, 12)}\u2026 does not match the pinned ${expected.sha256.slice(0, 12)}\u2026`, name);
17263
17438
  }
17264
- const final = path33.join(dir, name);
17439
+ const final = path34.join(dir, name);
17265
17440
  const tmp = `${final}.${process.pid}.part`;
17266
17441
  try {
17267
17442
  await writeFile16(tmp, bytes);
@@ -17307,12 +17482,12 @@ var EDITOR_CONTENT_TYPES = {
17307
17482
  ".json": "application/json; charset=utf-8"
17308
17483
  };
17309
17484
  function editorContentType(name) {
17310
- return EDITOR_CONTENT_TYPES[path33.extname(name).toLowerCase()] ?? null;
17485
+ return EDITOR_CONTENT_TYPES[path34.extname(name).toLowerCase()] ?? null;
17311
17486
  }
17312
17487
 
17313
17488
  // src/gallery/editor.ts
17314
17489
  import { readFile as readFile26 } from "fs/promises";
17315
- import path34 from "path";
17490
+ import path35 from "path";
17316
17491
  function editorRoute(release, name = "index.html") {
17317
17492
  return `/editor/${encodeURIComponent(release)}/${encodeURIComponent(name)}`;
17318
17493
  }
@@ -17385,7 +17560,7 @@ function createGalleryEditorHandlers(opts = {}) {
17385
17560
  if (!status.installed) return null;
17386
17561
  let bytes;
17387
17562
  try {
17388
- bytes = await readFile26(path34.join(dir, name));
17563
+ bytes = await readFile26(path35.join(dir, name));
17389
17564
  } catch {
17390
17565
  verified = null;
17391
17566
  return null;
@@ -18078,7 +18253,7 @@ function createGenerateHandlers(opts) {
18078
18253
  // src/pipeline/salvage.ts
18079
18254
  import { readFile as readFile28 } from "fs/promises";
18080
18255
  import { existsSync as existsSync29 } from "fs";
18081
- import path35 from "path";
18256
+ import path36 from "path";
18082
18257
  function providerClaimId(provider, objectId) {
18083
18258
  return `${encodeURIComponent(provider)}:${encodeURIComponent(objectId)}`;
18084
18259
  }
@@ -18192,20 +18367,20 @@ function matchOrphanStyle(prompt, manifest) {
18192
18367
  return matchStyleByPattern(prompt, manifest);
18193
18368
  }
18194
18369
  async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, claimPaths) {
18195
- const own = path35.resolve(ownManifestPath);
18370
+ const own = path36.resolve(ownManifestPath);
18196
18371
  const siblingManifestPaths = [
18197
18372
  .../* @__PURE__ */ new Set([
18198
18373
  ...workspaceManifestPaths,
18199
- ...claimPaths.map((c) => path35.join(path35.dirname(path35.resolve(c)), "pixelkiln.manifest.json"))
18374
+ ...claimPaths.map((c) => path36.join(path36.dirname(path36.resolve(c)), "pixelkiln.manifest.json"))
18200
18375
  ])
18201
18376
  ];
18202
18377
  const siblings = [];
18203
18378
  for (const siblingManifestPath of siblingManifestPaths) {
18204
- if (path35.resolve(siblingManifestPath) === own) continue;
18379
+ if (path36.resolve(siblingManifestPath) === own) continue;
18205
18380
  if (!existsSync29(siblingManifestPath)) continue;
18206
18381
  try {
18207
18382
  const { manifest } = await loadManifest(siblingManifestPath);
18208
- siblings.push({ label: path35.basename(path35.dirname(siblingManifestPath)), manifest });
18383
+ siblings.push({ label: path36.basename(path36.dirname(siblingManifestPath)), manifest });
18209
18384
  } catch {
18210
18385
  }
18211
18386
  }
@@ -18326,7 +18501,7 @@ async function applyTags(provider, decisions, existing, opts = {}) {
18326
18501
 
18327
18502
  // src/pick/salvage-server.ts
18328
18503
  import { mkdir as mkdir12, writeFile as writeFile17, readFile as readFile29 } from "fs/promises";
18329
- import path36 from "path";
18504
+ import path37 from "path";
18330
18505
 
18331
18506
  // src/pick/salvage-sheet.ts
18332
18507
  var escapeHtml3 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
@@ -18492,7 +18667,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
18492
18667
  });
18493
18668
  const html = renderSalvageSheet(orphans, {
18494
18669
  styleId: ctx.styleId,
18495
- importDir: path36.relative(process.cwd(), ctx.importDir) || "."
18670
+ importDir: path37.relative(process.cwd(), ctx.importDir) || "."
18496
18671
  });
18497
18672
  const byId = new Map(orphans.map((o) => [o.id, o]));
18498
18673
  const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
@@ -18548,9 +18723,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
18548
18723
  if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
18549
18724
  decodePng(buf);
18550
18725
  const assetId = idFromPrompt(orphan.prompt, taken);
18551
- const rel = path36.join("_salvaged", `${assetId}.png`);
18552
- const outFile = path36.resolve(ctx.importDir, rel);
18553
- await mkdir12(path36.dirname(outFile), { recursive: true });
18726
+ const rel = path37.join("_salvaged", `${assetId}.png`);
18727
+ const outFile = path37.resolve(ctx.importDir, rel);
18728
+ await mkdir12(path37.dirname(outFile), { recursive: true });
18554
18729
  await writeFile17(outFile, buf);
18555
18730
  ctx.manifest.assets[assetId] = {
18556
18731
  prompt: orphan.prompt,
@@ -18583,7 +18758,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
18583
18758
  sourceUrl: durableSource,
18584
18759
  sourceUrls: durableSource ? [{ url: durableSource }] : [],
18585
18760
  outputs: [{
18586
- path: portableOutputPath(outFile, path36.dirname(ctx.manifestPath)),
18761
+ path: portableOutputPath(outFile, path37.dirname(ctx.manifestPath)),
18587
18762
  sha256: sha256(buf)
18588
18763
  }],
18589
18764
  submittedAt: orphan.createdAt,