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.cjs CHANGED
@@ -638,8 +638,16 @@ var RevisionDitheringSchema = import_zod.z.enum(["none", "2x2", "4x4", "8x8"]);
638
638
  var RevisionSchema = import_zod.z.object({
639
639
  /** What kind of controlled change the provider workflow performs. */
640
640
  mode: RevisionModeSchema,
641
- /** Parent asset id in the same style. */
641
+ /** Parent asset id. It lives in this asset's own style unless `fromStyle` names another. */
642
642
  from: import_zod.z.string().min(1),
643
+ /**
644
+ * The style the parent lives in, when it is not this asset's own. The
645
+ * parent is generated by that style's provider and model, and this
646
+ * revision starts from its output file, so a chain (generate with one
647
+ * model, clean up with another) is declared instead of restated by hand.
648
+ * Styles may depend on one another but not in a cycle.
649
+ */
650
+ fromStyle: import_zod.z.string().min(1).optional(),
643
651
  /** Manifest-relative black/white PNG. Required only for masked inpainting. */
644
652
  mask: import_zod.z.string().min(1).optional(),
645
653
  /** Provider-neutral edit strength. The active adapter must bind it explicitly. */
@@ -1500,6 +1508,7 @@ var LockEntrySchema = import_zod.z.object({
1500
1508
  revision: import_zod.z.object({
1501
1509
  mode: RevisionModeSchema,
1502
1510
  sourceAssetId: import_zod.z.string().min(1),
1511
+ sourceStyleId: import_zod.z.string().min(1).optional(),
1503
1512
  sourceSha256: import_zod.z.string().regex(/^[0-9a-f]{64}$/),
1504
1513
  maskSha256: import_zod.z.string().regex(/^[0-9a-f]{64}$/).optional(),
1505
1514
  strength: import_zod.z.number().min(0).max(1).optional(),
@@ -2141,9 +2150,9 @@ var PixelLabClient = class {
2141
2150
  apiKey;
2142
2151
  http;
2143
2152
  /** Retries transport failures, 429, and 5xx; see http.ts for the policy. */
2144
- async request(path37, init) {
2153
+ async request(path38, init) {
2145
2154
  const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
2146
- const res = await this.http(`${BASE}${path37}`, {
2155
+ const res = await this.http(`${BASE}${path38}`, {
2147
2156
  ...init,
2148
2157
  headers: {
2149
2158
  Authorization: auth,
@@ -2153,13 +2162,13 @@ var PixelLabClient = class {
2153
2162
  });
2154
2163
  const text = await res.text();
2155
2164
  if (!res.ok) {
2156
- throw new PixelLabError(`${init?.method ?? "GET"} ${path37} \u2192 ${res.status}`, res.status, text);
2165
+ throw new PixelLabError(`${init?.method ?? "GET"} ${path38} \u2192 ${res.status}`, res.status, text);
2157
2166
  }
2158
2167
  if (!text) return {};
2159
2168
  try {
2160
2169
  return JSON.parse(text);
2161
2170
  } catch {
2162
- throw new Error(`${init?.method ?? "GET"} ${path37} returned invalid JSON`);
2171
+ throw new Error(`${init?.method ?? "GET"} ${path38} returned invalid JSON`);
2163
2172
  }
2164
2173
  }
2165
2174
  async balance() {
@@ -2564,10 +2573,10 @@ var PixelLabClient = class {
2564
2573
  const palette = args.paletteSwatchBase64 ? { color_image: { type: "base64", base64: args.paletteSwatchBase64, format: "png" }, force_colors: true } : {};
2565
2574
  const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
2566
2575
  const south = args.reference?.south;
2567
- let path37;
2576
+ let path38;
2568
2577
  let body;
2569
2578
  if (args.mode === "v3") {
2570
- path37 = "/create-character-v3";
2579
+ path38 = "/create-character-v3";
2571
2580
  body = {
2572
2581
  description: args.description,
2573
2582
  image_size: imageSize,
@@ -2581,7 +2590,7 @@ var PixelLabClient = class {
2581
2590
  ...args.enhancePrompt ? { enhance_prompt: true } : {}
2582
2591
  };
2583
2592
  } else if (args.mode === "pro-flash") {
2584
- path37 = "/create-character-pro-flash";
2593
+ path38 = "/create-character-pro-flash";
2585
2594
  const traits = args.styleTraits;
2586
2595
  body = {
2587
2596
  description: args.description,
@@ -2605,7 +2614,7 @@ var PixelLabClient = class {
2605
2614
  } : {}
2606
2615
  };
2607
2616
  } else if (args.mode === "pro") {
2608
- path37 = "/create-character-pro";
2617
+ path38 = "/create-character-pro";
2609
2618
  body = {
2610
2619
  description: args.description,
2611
2620
  image_size: imageSize,
@@ -2619,7 +2628,7 @@ var PixelLabClient = class {
2619
2628
  ...!south && args.styleCharacterId ? { style_character_id: args.styleCharacterId } : {}
2620
2629
  };
2621
2630
  } else {
2622
- path37 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
2631
+ path38 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
2623
2632
  body = {
2624
2633
  description: args.description,
2625
2634
  image_size: imageSize,
@@ -2636,8 +2645,8 @@ var PixelLabClient = class {
2636
2645
  ...palette
2637
2646
  };
2638
2647
  }
2639
- const raw = await this.request(path37, { method: "POST", body: JSON.stringify(body) });
2640
- return validateResponse(CharacterSubmitSchema, raw, path37.slice(1));
2648
+ const raw = await this.request(path38, { method: "POST", body: JSON.stringify(body) });
2649
+ return validateResponse(CharacterSubmitSchema, raw, path38.slice(1));
2641
2650
  }
2642
2651
  /** A text edit of an existing character, applied to every direction and stored as a sibling. */
2643
2652
  async createCharacterState(args) {
@@ -3850,10 +3859,10 @@ var import_node_fs = require("fs");
3850
3859
  function sha256(data) {
3851
3860
  return (0, import_node_crypto.createHash)("sha256").update(data).digest("hex");
3852
3861
  }
3853
- async function sha256File(path37) {
3862
+ async function sha256File(path38) {
3854
3863
  return new Promise((resolve, reject) => {
3855
3864
  const hash = (0, import_node_crypto.createHash)("sha256");
3856
- const stream = (0, import_node_fs.createReadStream)(path37);
3865
+ const stream = (0, import_node_fs.createReadStream)(path38);
3857
3866
  stream.on("data", (chunk2) => hash.update(chunk2));
3858
3867
  stream.on("error", reject);
3859
3868
  stream.on("end", () => resolve(hash.digest("hex")));
@@ -3962,6 +3971,7 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
3962
3971
  revision: spec.revision ? {
3963
3972
  mode: spec.revision.mode,
3964
3973
  from: spec.revision.sourceAssetId,
3974
+ fromStyle: spec.revision.sourceStyleId,
3965
3975
  sourceSha256: spec.revision.sourceSha256,
3966
3976
  maskSha256: spec.revision.maskSha256,
3967
3977
  strength: spec.revision.strength,
@@ -6257,9 +6267,9 @@ var RetroDiffusionClient = class {
6257
6267
  if (!response.ok) throw new ProviderError("retrodiffusion", `Retro Diffusion download failed (${response.status})`, { status: response.status });
6258
6268
  return Buffer.from(await response.arrayBuffer());
6259
6269
  }
6260
- async call(path37, init = {}) {
6270
+ async call(path38, init = {}) {
6261
6271
  if (!this.token) throw new Error("RD_API_KEY is not set");
6262
- const response = await this.request(`${this.baseUrl}${path37}`, {
6272
+ const response = await this.request(`${this.baseUrl}${path38}`, {
6263
6273
  ...init,
6264
6274
  headers: {
6265
6275
  "Content-Type": "application/json",
@@ -6568,8 +6578,13 @@ function retroError(value) {
6568
6578
  }
6569
6579
 
6570
6580
  // src/providers/scenario.ts
6581
+ var import_node_crypto3 = require("crypto");
6582
+ var import_node_fs3 = require("fs");
6583
+ var import_node_os2 = __toESM(require("os"), 1);
6584
+ var import_node_path2 = __toESM(require("path"), 1);
6571
6585
  var DEFAULT_BASE_URL2 = "https://api.cloud.scenario.com/v1";
6572
6586
  var SOURCE_PROTOCOL2 = "scenario:";
6587
+ var SUBMIT_RETRIES = 8;
6573
6588
  var PROTECTED_PARAMETERS = /* @__PURE__ */ new Set([
6574
6589
  "dryRun",
6575
6590
  "height",
@@ -6616,7 +6631,11 @@ var ScenarioClient = class {
6616
6631
  async submit(modelId, body, projectId) {
6617
6632
  const response = await this.call(
6618
6633
  `/generate/custom/${encodeURIComponent(modelId)}`,
6619
- { method: "POST", body: JSON.stringify(body) },
6634
+ // A 429 here means this account's concurrent-job limit refused the
6635
+ // request, so nothing was accepted or billed; wait it out instead of
6636
+ // failing the asset. The default four retries give up after about half
6637
+ // a minute, before a running job has finished.
6638
+ { method: "POST", body: JSON.stringify(body), retries: SUBMIT_RETRIES },
6620
6639
  { projectId }
6621
6640
  );
6622
6641
  const job = response?.job;
@@ -6624,6 +6643,29 @@ var ScenarioClient = class {
6624
6643
  if (!jobId) throw new Error("Scenario did not return an async job id");
6625
6644
  return jobId;
6626
6645
  }
6646
+ /**
6647
+ * Uploads one image (POST /assets) and returns its asset id, which a model's
6648
+ * reference input takes. Free, and idempotent per image: the id is kept by
6649
+ * content hash so a rerun does not upload the same bytes again.
6650
+ */
6651
+ async uploadImage(image, projectId) {
6652
+ const bytes = Buffer.from(image.base64, "base64");
6653
+ const digest2 = sha256(bytes);
6654
+ const cached = readUploadCache()[uploadCacheKey(this.baseUrl, projectId, digest2)];
6655
+ if (cached) return cached;
6656
+ const response = await this.call(
6657
+ "/assets",
6658
+ {
6659
+ method: "POST",
6660
+ body: JSON.stringify({ image: image.base64, name: `pixelkiln-${digest2.slice(0, 12)}.${image.format === "png" ? "png" : "jpg"}` })
6661
+ },
6662
+ { projectId }
6663
+ );
6664
+ const id = response?.asset?.id;
6665
+ if (typeof id !== "string" || !id) throw new Error("Scenario did not return an uploaded asset id");
6666
+ writeUploadCache(uploadCacheKey(this.baseUrl, projectId, digest2), id);
6667
+ return id;
6668
+ }
6627
6669
  async job(id, projectId) {
6628
6670
  const response = await this.call(`/jobs/${encodeURIComponent(id)}`, {}, { projectId });
6629
6671
  const raw = response?.job;
@@ -6750,6 +6792,14 @@ var ScenarioProvider = class _ScenarioProvider {
6750
6792
  supports(generator) {
6751
6793
  return generator === "map";
6752
6794
  }
6795
+ /**
6796
+ * An edit is a generation with the parent as a reference image, which the
6797
+ * editing models (GPT Image, Gemini, FLUX) take. Masked inpainting and the
6798
+ * PixelLab-specific modes have no common Scenario input.
6799
+ */
6800
+ supportsRevision(mode2) {
6801
+ return mode2 === "image-to-image";
6802
+ }
6753
6803
  estimate(spec) {
6754
6804
  const options = scenarioOptions(spec);
6755
6805
  return {
@@ -6770,11 +6820,31 @@ var ScenarioProvider = class _ScenarioProvider {
6770
6820
  if (!Number.isInteger(count) || count < 1 || count > 4) {
6771
6821
  throw new Error("Scenario numOutputs must be a whole number from 1 to 4");
6772
6822
  }
6773
- if (spec.width < 128 || spec.width > 2048 || spec.width % 16 !== 0 || spec.height < 128 || spec.height > 2048 || spec.height % 16 !== 0) {
6774
- throw new Error("Scenario width and height must be multiples of 16 from 128 to 2048");
6823
+ if (!Number.isInteger(spec.width) || spec.width < 16 || spec.width > 4096 || !Number.isInteger(spec.height) || spec.height < 16 || spec.height > 4096) {
6824
+ throw new Error("Scenario width and height must be whole numbers from 16 to 4096");
6775
6825
  }
6776
- if (styleImages.length) {
6777
- throw new Error("Scenario styleImages are not supported yet; use a reusable Scenario model");
6826
+ const revision = spec.revision;
6827
+ if (revision) {
6828
+ if (revision.sourceMembers?.length) {
6829
+ throw new Error(
6830
+ `Scenario edits one image; ${revision.sourceAssetId} is a ${revision.sourceMembers.length}-member set`
6831
+ );
6832
+ }
6833
+ if (revision.strength != null) {
6834
+ throw new Error("Scenario image-to-image takes no strength; use the model's own parameters");
6835
+ }
6836
+ }
6837
+ const parameter = referenceParameter(options);
6838
+ if ((styleImages.length || revision) && Object.hasOwn(options.parameters ?? {}, parameter)) {
6839
+ throw new Error(
6840
+ `Scenario parameters.${parameter} is the reference-image input and is filled from styleImages and revisions`
6841
+ );
6842
+ }
6843
+ if (styleImages.length + (revision ? 1 : 0) > 1 && options.referenceArray === false) {
6844
+ throw new Error(`Scenario ${parameter} takes one image; use one style image or a revision, not both`);
6845
+ }
6846
+ if (options.referenceParameter != null && (typeof options.referenceParameter !== "string" || !options.referenceParameter.trim())) {
6847
+ throw new Error("Scenario referenceParameter must be a non-empty string");
6778
6848
  }
6779
6849
  if (options.projectId != null && (typeof options.projectId !== "string" || !options.projectId.trim())) {
6780
6850
  throw new Error("Scenario projectId must be a non-empty string");
@@ -6787,7 +6857,12 @@ var ScenarioProvider = class _ScenarioProvider {
6787
6857
  async submit(spec, styleImages) {
6788
6858
  this.validate(spec, styleImages);
6789
6859
  const options = scenarioOptions(spec);
6790
- const body = requestBody(spec, options);
6860
+ const references = [];
6861
+ if (spec.revision) references.push(readRevisionSource(spec));
6862
+ for (const image of styleImages) references.push({ base64: image.base64, format: image.format });
6863
+ const referenceAssetIds = [];
6864
+ for (const image of references) referenceAssetIds.push(await this.client.uploadImage(image, options.projectId));
6865
+ const body = requestBody(spec, options, referenceAssetIds);
6791
6866
  const quote = await this.client.quote(options.modelId, body, options.projectId);
6792
6867
  if (quote.total > options.maxComputeUnits + Number.EPSILON) {
6793
6868
  throw new Error(
@@ -6804,7 +6879,8 @@ var ScenarioProvider = class _ScenarioProvider {
6804
6879
  quotedComputeUnits: quote.total,
6805
6880
  creativeUnitsCost: quote.creativeUnitsCost,
6806
6881
  costDetails: quote.costDetails,
6807
- ipDetectionCost: quote.ipDetectionCost
6882
+ ipDetectionCost: quote.ipDetectionCost,
6883
+ ...referenceAssetIds.length ? { referenceAssetIds } : {}
6808
6884
  }
6809
6885
  };
6810
6886
  }
@@ -6826,7 +6902,13 @@ var ScenarioProvider = class _ScenarioProvider {
6826
6902
  }
6827
6903
  const assetIds = scenarioAssetIds(job);
6828
6904
  if (!assetIds.length) return { status: "failed", error: "Scenario job returned no asset IDs" };
6829
- const assets = await this.assets(assetIds, reference.projectId);
6905
+ let assets;
6906
+ try {
6907
+ assets = await this.assets(assetIds, reference.projectId);
6908
+ } catch (error) {
6909
+ if (error instanceof UnsupportedScenarioOutput) return { status: "failed", error: error.message };
6910
+ throw error;
6911
+ }
6830
6912
  if (assets.length > 1) {
6831
6913
  return {
6832
6914
  status: "review",
@@ -6881,6 +6963,32 @@ var ScenarioProvider = class _ScenarioProvider {
6881
6963
  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));
6882
6964
  }
6883
6965
  };
6966
+ function uploadCachePath() {
6967
+ return process.env.SCENARIO_UPLOAD_CACHE ?? import_node_path2.default.join(import_node_os2.default.tmpdir(), "pixelkiln-scenario", "uploads.json");
6968
+ }
6969
+ function uploadCacheKey(baseUrl, projectId, digest2) {
6970
+ return (0, import_node_crypto3.createHash)("sha256").update(`${baseUrl}
6971
+ ${projectId ?? ""}
6972
+ ${digest2}`).digest("hex");
6973
+ }
6974
+ function readUploadCache() {
6975
+ try {
6976
+ const value = JSON.parse((0, import_node_fs3.readFileSync)(uploadCachePath(), "utf8"));
6977
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
6978
+ } catch {
6979
+ return {};
6980
+ }
6981
+ }
6982
+ function writeUploadCache(key, assetId) {
6983
+ try {
6984
+ const file = uploadCachePath();
6985
+ (0, import_node_fs3.mkdirSync)(import_node_path2.default.dirname(file), { recursive: true });
6986
+ const tmp = `${file}.${process.pid}.tmp`;
6987
+ (0, import_node_fs3.writeFileSync)(tmp, JSON.stringify({ ...readUploadCache(), [key]: assetId }));
6988
+ (0, import_node_fs3.renameSync)(tmp, file);
6989
+ } catch {
6990
+ }
6991
+ }
6884
6992
  function scenarioOptions(spec) {
6885
6993
  return spec.providerOptions;
6886
6994
  }
@@ -6896,9 +7004,25 @@ function scenarioBaseUrl(value) {
6896
7004
  }
6897
7005
  return url.href.replace(/\/$/, "");
6898
7006
  }
6899
- function requestBody(spec, options) {
7007
+ function referenceParameter(options) {
7008
+ return options.referenceParameter?.trim() || "referenceImages";
7009
+ }
7010
+ function readRevisionSource(spec) {
7011
+ const revision = spec.revision;
7012
+ if (!revision.sourceFile || !revision.sourceSha256 || !revision.sourceFormat) {
7013
+ throw new Error(`${spec.styleId}/${spec.assetId}: revision source is not ready`);
7014
+ }
7015
+ const bytes = (0, import_node_fs3.readFileSync)(revision.sourceFile);
7016
+ if (sha256(bytes) !== revision.sourceSha256) {
7017
+ throw new Error(`${spec.styleId}/${spec.assetId}: revision source changed after the manifest was resolved`);
7018
+ }
7019
+ return { base64: bytes.toString("base64"), format: revision.sourceFormat };
7020
+ }
7021
+ function requestBody(spec, options, referenceAssetIds = []) {
7022
+ const parameter = referenceParameter(options);
6900
7023
  return {
6901
7024
  ...options.parameters ?? {},
7025
+ ...referenceAssetIds.length ? { [parameter]: options.referenceArray === false ? referenceAssetIds[0] : referenceAssetIds } : {},
6902
7026
  prompt: spec.prompt,
6903
7027
  width: spec.width,
6904
7028
  height: spec.height,
@@ -6988,9 +7112,11 @@ function scenarioAssetIds(job) {
6988
7112
  function assetDownloadUrl(asset) {
6989
7113
  return asset.originalFileUrl || asset.url;
6990
7114
  }
7115
+ var UnsupportedScenarioOutput = class extends Error {
7116
+ };
6991
7117
  function assertPngAsset(asset) {
6992
7118
  if (asset.originalMimeType && asset.originalMimeType !== MediaType.PNG) {
6993
- throw new Error(
7119
+ throw new UnsupportedScenarioOutput(
6994
7120
  `Scenario asset ${asset.id} is ${asset.originalMimeType}; PixelKiln's Scenario MVP requires PNG output`
6995
7121
  );
6996
7122
  }
@@ -7055,9 +7181,9 @@ function redactScenarioError(value, secrets) {
7055
7181
  }
7056
7182
 
7057
7183
  // src/providers/comfyui.ts
7058
- var import_node_crypto3 = require("crypto");
7184
+ var import_node_crypto4 = require("crypto");
7059
7185
  var import_promises = require("fs/promises");
7060
- var import_node_path2 = __toESM(require("path"), 1);
7186
+ var import_node_path3 = __toESM(require("path"), 1);
7061
7187
  var DEFAULT_BASE_URL3 = "http://127.0.0.1:8188";
7062
7188
  var BUILTIN_BINDINGS = /* @__PURE__ */ new Set([
7063
7189
  "prompt",
@@ -7084,7 +7210,7 @@ var ComfyUIClient = class {
7084
7210
  async submit(workflow) {
7085
7211
  const value = await this.json("prompt", {
7086
7212
  method: "POST",
7087
- body: JSON.stringify({ prompt: workflow, client_id: (0, import_node_crypto3.randomUUID)() })
7213
+ body: JSON.stringify({ prompt: workflow, client_id: (0, import_node_crypto4.randomUUID)() })
7088
7214
  });
7089
7215
  if (!isObject(value)) throw new Error("ComfyUI returned an invalid queue response");
7090
7216
  const promptId = value.prompt_id;
@@ -7194,7 +7320,7 @@ var ComfyUIProvider = class _ComfyUIProvider {
7194
7320
  }
7195
7321
  async resolveOptions(value, context) {
7196
7322
  const options = parseOptions(value);
7197
- const workflowPath = import_node_path2.default.resolve(context.root, options.workflowFile);
7323
+ const workflowPath = import_node_path3.default.resolve(context.root, options.workflowFile);
7198
7324
  let raw;
7199
7325
  try {
7200
7326
  raw = await (0, import_promises.readFile)(workflowPath, "utf8");
@@ -7249,7 +7375,7 @@ var ComfyUIProvider = class _ComfyUIProvider {
7249
7375
  if (typeof item !== "string") {
7250
7376
  throw new Error(`ComfyUI ${label} must be a manifest-relative PNG or JPEG path`);
7251
7377
  }
7252
- const file = import_node_path2.default.resolve(context.root, item);
7378
+ const file = import_node_path3.default.resolve(context.root, item);
7253
7379
  let bytes;
7254
7380
  try {
7255
7381
  bytes = await (0, import_promises.readFile)(file);
@@ -7961,7 +8087,7 @@ function canonical(value) {
7961
8087
  }
7962
8088
 
7963
8089
  // src/providers/fake.ts
7964
- var import_node_crypto4 = require("crypto");
8090
+ var import_node_crypto5 = require("crypto");
7965
8091
  var FAKE_PNG = Buffer.from(
7966
8092
  "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
7967
8093
  "base64"
@@ -8063,7 +8189,7 @@ var FakeProvider = class {
8063
8189
  }
8064
8190
  async download(url) {
8065
8191
  if (!url.startsWith("fake://")) throw new Error(`fake provider cannot download ${url}`);
8066
- const salt = (0, import_node_crypto4.createHash)("sha256").update(url).digest();
8192
+ const salt = (0, import_node_crypto5.createHash)("sha256").update(url).digest();
8067
8193
  return Buffer.concat([FAKE_PNG, salt.subarray(0, 4)]);
8068
8194
  }
8069
8195
  async balance() {
@@ -8211,8 +8337,8 @@ registerProvider({
8211
8337
 
8212
8338
  // src/manifest.ts
8213
8339
  var import_promises3 = require("fs/promises");
8214
- var import_node_fs5 = require("fs");
8215
- var import_node_path5 = __toESM(require("path"), 1);
8340
+ var import_node_fs6 = require("fs");
8341
+ var import_node_path6 = __toESM(require("path"), 1);
8216
8342
 
8217
8343
  // src/loop-directions.ts
8218
8344
  var SINGLE_OUTPUT_FIELDS = ["file", "cell", "remoteId", "source", "sourceByStyle", "outputRole"];
@@ -8300,15 +8426,15 @@ function isObject2(value) {
8300
8426
  }
8301
8427
 
8302
8428
  // src/outputs.ts
8303
- var import_node_fs4 = require("fs");
8304
- var import_node_path4 = __toESM(require("path"), 1);
8429
+ var import_node_fs5 = require("fs");
8430
+ var import_node_path5 = __toESM(require("path"), 1);
8305
8431
 
8306
8432
  // src/lock.ts
8307
8433
  var import_promises2 = require("fs/promises");
8308
- var import_node_fs3 = require("fs");
8309
- var import_node_path3 = __toESM(require("path"), 1);
8434
+ var import_node_fs4 = require("fs");
8435
+ var import_node_path4 = __toESM(require("path"), 1);
8310
8436
  async function loadLock(lockPath) {
8311
- if (!(0, import_node_fs3.existsSync)(lockPath)) return { version: 2, entries: {} };
8437
+ if (!(0, import_node_fs4.existsSync)(lockPath)) return { version: 2, entries: {} };
8312
8438
  try {
8313
8439
  return parseLock(JSON.parse(await (0, import_promises2.readFile)(lockPath, "utf8")));
8314
8440
  } catch (err) {
@@ -8323,7 +8449,7 @@ var saveQueues = /* @__PURE__ */ new Map();
8323
8449
  var dirtyPatches = /* @__PURE__ */ new WeakMap();
8324
8450
  var dirtyDeletes = /* @__PURE__ */ new WeakMap();
8325
8451
  function saveLock(lockPath, lock) {
8326
- const queueKey = import_node_path3.default.resolve(lockPath);
8452
+ const queueKey = import_node_path4.default.resolve(lockPath);
8327
8453
  const previous = saveQueues.get(queueKey) ?? Promise.resolve();
8328
8454
  const next = previous.catch(() => {
8329
8455
  }).then(() => writeLockNow(lockPath, lock));
@@ -8335,7 +8461,7 @@ function saveLock(lockPath, lock) {
8335
8461
  return next;
8336
8462
  }
8337
8463
  async function writeLockNow(lockPath, lock) {
8338
- await (0, import_promises2.mkdir)(import_node_path3.default.dirname(import_node_path3.default.resolve(lockPath)), { recursive: true });
8464
+ await (0, import_promises2.mkdir)(import_node_path4.default.dirname(import_node_path4.default.resolve(lockPath)), { recursive: true });
8339
8465
  const release = await acquireFileLock(lockPath);
8340
8466
  try {
8341
8467
  await writeLockWhileHeld(lockPath, lock);
@@ -8345,7 +8471,7 @@ async function writeLockNow(lockPath, lock) {
8345
8471
  }
8346
8472
  async function writeLockWhileHeld(file, lock) {
8347
8473
  let disk = { version: 2, entries: {} };
8348
- if ((0, import_node_fs3.existsSync)(file)) {
8474
+ if ((0, import_node_fs4.existsSync)(file)) {
8349
8475
  try {
8350
8476
  disk = parseLock(JSON.parse(await (0, import_promises2.readFile)(file, "utf8")));
8351
8477
  } catch (err) {
@@ -8480,22 +8606,22 @@ function totalSpend(lock, unit = "generations") {
8480
8606
 
8481
8607
  // src/outputs.ts
8482
8608
  function portableOutputPath(file, manifestDir) {
8483
- const root = import_node_path4.default.resolve(manifestDir);
8484
- const absolute = import_node_path4.default.isAbsolute(file) ? import_node_path4.default.normalize(file) : import_node_path4.default.resolve(root, file);
8485
- const relative = import_node_path4.default.relative(root, absolute);
8486
- const value = import_node_path4.default.isAbsolute(relative) ? absolute : relative;
8487
- return value.split(import_node_path4.default.sep).join("/");
8609
+ const root = import_node_path5.default.resolve(manifestDir);
8610
+ const absolute = import_node_path5.default.isAbsolute(file) ? import_node_path5.default.normalize(file) : import_node_path5.default.resolve(root, file);
8611
+ const relative = import_node_path5.default.relative(root, absolute);
8612
+ const value = import_node_path5.default.isAbsolute(relative) ? absolute : relative;
8613
+ return value.split(import_node_path5.default.sep).join("/");
8488
8614
  }
8489
8615
  function resolveOutputPath(recordedPath, manifestDir) {
8490
- if (import_node_path4.default.isAbsolute(recordedPath)) return import_node_path4.default.normalize(recordedPath);
8491
- if (import_node_path4.default.win32.isAbsolute(recordedPath)) return recordedPath;
8492
- return import_node_path4.default.resolve(manifestDir, recordedPath.split(/[\\/]/).join(import_node_path4.default.sep));
8616
+ if (import_node_path5.default.isAbsolute(recordedPath)) return import_node_path5.default.normalize(recordedPath);
8617
+ if (import_node_path5.default.win32.isAbsolute(recordedPath)) return recordedPath;
8618
+ return import_node_path5.default.resolve(manifestDir, recordedPath.split(/[\\/]/).join(import_node_path5.default.sep));
8493
8619
  }
8494
8620
  function expectedOutputPath(spec, role, index, total, mediaType) {
8495
8621
  return memberPath(spec.outFile, role, index, total, mediaType);
8496
8622
  }
8497
8623
  function memberPath(file, role, index, total, mediaType) {
8498
- const originalExt = import_node_path4.default.extname(file);
8624
+ const originalExt = import_node_path5.default.extname(file);
8499
8625
  const ext = mediaType ? mediaExtension(mediaType) : originalExt || ".png";
8500
8626
  const stem = originalExt ? file.slice(0, -originalExt.length) : file;
8501
8627
  if (total === 1) return `${stem}${ext}`;
@@ -8509,10 +8635,10 @@ function isFrameSetEntry(entry) {
8509
8635
  return entry.generator === "frames" && isMemberSetEntry(entry);
8510
8636
  }
8511
8637
  function sourceIsStem(source, entry, manifestDir) {
8512
- return isMemberSetEntry(entry) && !(0, import_node_fs4.existsSync)(import_node_path4.default.resolve(manifestDir, source));
8638
+ return isMemberSetEntry(entry) && !(0, import_node_fs5.existsSync)(import_node_path5.default.resolve(manifestDir, source));
8513
8639
  }
8514
8640
  function sourceOutputPath(source, entry, index, manifestDir) {
8515
- const absolute = import_node_path4.default.resolve(manifestDir, source);
8641
+ const absolute = import_node_path5.default.resolve(manifestDir, source);
8516
8642
  if (!sourceIsStem(source, entry, manifestDir)) return absolute;
8517
8643
  const output = entry.outputs[index];
8518
8644
  return memberPath(absolute, output.role, index, entry.outputs.length, output.mediaType);
@@ -8599,7 +8725,7 @@ function resolveSpecOutputs(spec, lock, manifestDir) {
8599
8725
  id: spec.assetId,
8600
8726
  index: 0,
8601
8727
  path: spec.outFile,
8602
- absolutePath: import_node_path4.default.resolve(spec.outFile),
8728
+ absolutePath: import_node_path5.default.resolve(spec.outFile),
8603
8729
  sha256: ""
8604
8730
  }];
8605
8731
  }
@@ -8615,8 +8741,8 @@ function frameSetFps(entry) {
8615
8741
 
8616
8742
  // src/manifest.ts
8617
8743
  async function loadManifest(manifestPath) {
8618
- const abs = import_node_path5.default.resolve(manifestPath);
8619
- if (!(0, import_node_fs5.existsSync)(abs)) throw new ProjectError(`No manifest at ${abs}`);
8744
+ const abs = import_node_path6.default.resolve(manifestPath);
8745
+ if (!(0, import_node_fs6.existsSync)(abs)) throw new ProjectError(`No manifest at ${abs}`);
8620
8746
  const expansion = expandLoopDirections(JSON.parse(await (0, import_promises3.readFile)(abs, "utf8")));
8621
8747
  if (expansion.issues.length) {
8622
8748
  throw new ProjectError(`Manifest at ${abs} is invalid:
@@ -8660,13 +8786,24 @@ ${issues}`);
8660
8786
  }
8661
8787
  }
8662
8788
  if (asset.revision) {
8663
- if (!parsed.data.assets[asset.revision.from]) {
8789
+ const parent = parsed.data.assets[asset.revision.from];
8790
+ if (!parent) {
8664
8791
  unknownReferences.push(
8665
8792
  `assets.${assetId}.revision.from: unknown asset "${asset.revision.from}"`
8666
8793
  );
8667
8794
  } else if (asset.revision.from === assetId) {
8668
8795
  unknownReferences.push(`assets.${assetId}.revision.from: an asset cannot revise itself`);
8669
8796
  }
8797
+ const fromStyle = asset.revision.fromStyle;
8798
+ if (fromStyle) {
8799
+ if (!styleIds.has(fromStyle)) {
8800
+ unknownReferences.push(`assets.${assetId}.revision.fromStyle: unknown style "${fromStyle}"`);
8801
+ } else if (parent && parent.styles.length && !parent.styles.includes(fromStyle)) {
8802
+ unknownReferences.push(
8803
+ `assets.${assetId}.revision.fromStyle: "${asset.revision.from}" is not in style "${fromStyle}"`
8804
+ );
8805
+ }
8806
+ }
8670
8807
  }
8671
8808
  for (const shape of ["state", "animation"]) {
8672
8809
  const of = asset[shape]?.of;
@@ -8708,7 +8845,7 @@ ${issues}`);
8708
8845
  ${unknownReferences.map((i) => ` ${i}`).join("\n")}`);
8709
8846
  }
8710
8847
  const loopFamilies = Object.keys(expansion.families).length ? { loopFamilies: expansion.families } : {};
8711
- return { manifest: parsed.data, root: import_node_path5.default.dirname(abs), path: abs, ...loopFamilies };
8848
+ return { manifest: parsed.data, root: import_node_path6.default.dirname(abs), path: abs, ...loopFamilies };
8712
8849
  }
8713
8850
  function parentAssetId(asset) {
8714
8851
  return asset?.revision?.from ?? asset?.state?.of ?? asset?.animation?.of ?? asset?.portrait?.of ?? asset?.outfit?.of;
@@ -8782,7 +8919,7 @@ async function resolveSpecs(loaded, filter) {
8782
8919
  return provider;
8783
8920
  };
8784
8921
  const specs = [];
8785
- const styleIds = Object.keys(manifest.styles).filter(
8922
+ const requestedStyleIds = Object.keys(manifest.styles).filter(
8786
8923
  (id) => !filter?.styles?.length || filter.styles.includes(id)
8787
8924
  );
8788
8925
  for (const unknownStyle of filter?.styles ?? []) {
@@ -8807,12 +8944,45 @@ async function resolveSpecs(loaded, filter) {
8807
8944
  current = parentAssetId(manifest.assets[current]);
8808
8945
  }
8809
8946
  }
8947
+ const styleDependencies = /* @__PURE__ */ new Map();
8948
+ for (const styleId of Object.keys(manifest.styles)) {
8949
+ const needed = /* @__PURE__ */ new Set();
8950
+ for (const assetId of resolutionAssetIds) {
8951
+ const asset = manifest.assets[assetId];
8952
+ if (asset?.styles.length && !asset.styles.includes(styleId)) continue;
8953
+ const fromStyle = asset?.revision?.fromStyle;
8954
+ if (fromStyle && fromStyle !== styleId) needed.add(fromStyle);
8955
+ }
8956
+ styleDependencies.set(styleId, needed);
8957
+ }
8958
+ const styleIds = [];
8959
+ const emitStyles = new Set(requestedStyleIds);
8960
+ {
8961
+ const placed = /* @__PURE__ */ new Set();
8962
+ const visiting = [];
8963
+ const place = (styleId) => {
8964
+ if (placed.has(styleId)) return;
8965
+ const cycleStart = visiting.indexOf(styleId);
8966
+ if (cycleStart >= 0) {
8967
+ throw new Error(
8968
+ `Styles revise each other in a cycle: ${[...visiting.slice(cycleStart), styleId].join(" -> ")}. revision.fromStyle must form a chain, not a loop.`
8969
+ );
8970
+ }
8971
+ visiting.push(styleId);
8972
+ for (const dependency of styleDependencies.get(styleId) ?? []) place(dependency);
8973
+ visiting.pop();
8974
+ placed.add(styleId);
8975
+ styleIds.push(styleId);
8976
+ };
8977
+ for (const styleId of requestedStyleIds) place(styleId);
8978
+ }
8979
+ const styleFinalizers = /* @__PURE__ */ new Map();
8810
8980
  const styleImageCache = /* @__PURE__ */ new Map();
8811
8981
  async function loadStyleImage(rel, what = "Style image") {
8812
- const abs = import_node_path5.default.resolve(root, rel);
8982
+ const abs = import_node_path6.default.resolve(root, rel);
8813
8983
  let hit = styleImageCache.get(abs);
8814
8984
  if (!hit) {
8815
- if (!(0, import_node_fs5.existsSync)(abs)) throw new Error(`${what} not found: ${abs}`);
8985
+ if (!(0, import_node_fs6.existsSync)(abs)) throw new Error(`${what} not found: ${abs}`);
8816
8986
  const buf = await (0, import_promises3.readFile)(abs);
8817
8987
  const metadata = imageMetadata(buf);
8818
8988
  if (!metadata) throw new Error(`${what} is not a readable PNG or JPEG: ${abs}`);
@@ -8839,7 +9009,7 @@ async function resolveSpecs(loaded, filter) {
8839
9009
  styleImageDimensions.push(loadedImage);
8840
9010
  }
8841
9011
  const resolvedImages = style.styleImages.map((image) => {
8842
- const hit = styleImageCache.get(import_node_path5.default.resolve(root, image.path));
9012
+ const hit = styleImageCache.get(import_node_path6.default.resolve(root, image.path));
8843
9013
  return { base64: hit.base64, width: hit.width, height: hit.height, format: hit.format };
8844
9014
  });
8845
9015
  const styleSpecs = /* @__PURE__ */ new Map();
@@ -8940,9 +9110,9 @@ async function resolveSpecs(loaded, filter) {
8940
9110
  } else {
8941
9111
  prompt = [style.promptPrefix, subject, style.promptSuffix].map((p) => p.trim()).filter(Boolean).join(", ");
8942
9112
  }
8943
- const relFile = asset.file ?? import_node_path5.default.join(asset.category ?? "", `${assetId}.png`);
8944
- const outFile = import_node_path5.default.resolve(root, style.outDir, relFile);
8945
- const qualityOutFile = style.quality ? import_node_path5.default.resolve(root, style.quality.outDir, pngPath(relFile)) : void 0;
9113
+ const relFile = asset.file ?? import_node_path6.default.join(asset.category ?? "", `${assetId}.png`);
9114
+ const outFile = import_node_path6.default.resolve(root, style.outDir, relFile);
9115
+ const qualityOutFile = style.quality ? import_node_path6.default.resolve(root, style.quality.outDir, pngPath(relFile)) : void 0;
8946
9116
  const tileSize = generator === "tiles" ? size : style.tileSize ?? 32;
8947
9117
  const tileVariations = tileFeatureOutputCount(generator === "tiles" ? style.tileFeature : void 0) ?? tileVariationCount(countNumberedDescriptions(prompt));
8948
9118
  const terrainTiles = generator === "terrain" ? terrainTileCount(style.terrainTransitionSize) : 0;
@@ -9037,7 +9207,7 @@ async function resolveSpecs(loaded, filter) {
9037
9207
  minGridConfidence: style.quality.minGridConfidence,
9038
9208
  ...style.quality.minTransparency == null ? {} : { minTransparency: style.quality.minTransparency },
9039
9209
  ...style.quality.fixerRevision ? { fixerRevision: style.quality.fixerRevision } : {},
9040
- ...style.quality.fixerPython ? { fixerPython: import_node_path5.default.resolve(root, style.quality.fixerPython) } : {},
9210
+ ...style.quality.fixerPython ? { fixerPython: import_node_path6.default.resolve(root, style.quality.fixerPython) } : {},
9041
9211
  ...style.quality.fps == null ? {} : { fps: style.quality.fps }
9042
9212
  }
9043
9213
  } : {},
@@ -9120,7 +9290,7 @@ async function resolveSpecs(loaded, filter) {
9120
9290
  const file = memberPath(anchorSpec.outFile, "south", 0, anchorSpec.character.directions, MediaType.PNG);
9121
9291
  resolved.character = {
9122
9292
  ...resolved.character,
9123
- styleAnchor: { assetId: anchorId, spec: anchorSpec, file, sha256: (0, import_node_fs5.existsSync)(file) ? await sha256File(file) : null }
9293
+ styleAnchor: { assetId: anchorId, spec: anchorSpec, file, sha256: (0, import_node_fs6.existsSync)(file) ? await sha256File(file) : null }
9124
9294
  };
9125
9295
  }
9126
9296
  const characterParent = asset.state?.of ?? asset.animation?.of ?? asset.portrait?.of;
@@ -9133,7 +9303,7 @@ async function resolveSpecs(loaded, filter) {
9133
9303
  parentAssetId: characterParent,
9134
9304
  parentSpec,
9135
9305
  parentFile,
9136
- parentSha256: (0, import_node_fs5.existsSync)(parentFile) ? await sha256File(parentFile) : null,
9306
+ parentSha256: (0, import_node_fs6.existsSync)(parentFile) ? await sha256File(parentFile) : null,
9137
9307
  // A state and its animations keep the engine and rotations of the base.
9138
9308
  mode: resolved.character.kind === "base" ? resolved.character.mode : parentSpec.character?.mode ?? resolved.character.mode,
9139
9309
  directions: resolved.character.kind === "base" ? resolved.character.directions : parentSpec.character?.directions ?? resolved.character.directions
@@ -9151,7 +9321,7 @@ async function resolveSpecs(loaded, filter) {
9151
9321
  parentAssetId: characterParent,
9152
9322
  parentSpec,
9153
9323
  parentFile,
9154
- parentSha256: (0, import_node_fs5.existsSync)(parentFile) ? await sha256File(parentFile) : null,
9324
+ parentSha256: (0, import_node_fs6.existsSync)(parentFile) ? await sha256File(parentFile) : null,
9155
9325
  // A state and its animations keep the base's own rotation count.
9156
9326
  directions: resolved.objectPro.kind === "base" ? resolved.objectPro.directions : parentSpec.objectPro.directions
9157
9327
  };
@@ -9173,7 +9343,7 @@ async function resolveSpecs(loaded, filter) {
9173
9343
  animation: sourceSpec.character.animation,
9174
9344
  outfit: {
9175
9345
  reference: {
9176
- path: import_node_path5.default.resolve(root, asset.outfit.reference),
9346
+ path: import_node_path6.default.resolve(root, asset.outfit.reference),
9177
9347
  sha256: referenceImage.hash,
9178
9348
  width: referenceImage.width,
9179
9349
  height: referenceImage.height,
@@ -9194,28 +9364,30 @@ async function resolveSpecs(loaded, filter) {
9194
9364
  `Provider "${activeProvider.id}" does not support ${asset.revision.mode} revisions`
9195
9365
  );
9196
9366
  }
9197
- const sourceSpec = await finalize(asset.revision.from);
9198
- const sourceStem = sourceSpec.quality?.outFile ?? (sourceSpec.source ? import_node_path5.default.resolve(root, sourceSpec.source) : sourceSpec.outFile);
9367
+ const sourceStyleId = asset.revision.fromStyle && asset.revision.fromStyle !== styleId ? asset.revision.fromStyle : void 0;
9368
+ const sourceSpec = sourceStyleId ? await styleFinalizers.get(sourceStyleId)(asset.revision.from) : await finalize(asset.revision.from);
9369
+ const sourceStem = sourceSpec.quality?.outFile ?? (sourceSpec.source ? import_node_path6.default.resolve(root, sourceSpec.source) : sourceSpec.outFile);
9199
9370
  const sourceMembers = await revisionSourceMembers(sourceStem, `revision source ${asset.revision.from}`);
9200
9371
  const sourceFile = sourceMembers?.[0]?.file ?? sourceStem;
9201
9372
  const sourceImage = sourceMembers ? { ...sourceMembers[0], hash: sha256(JSON.stringify(sourceMembers.map((member) => member.sha256))) } : await optionalRevisionImage(sourceFile, "revision source");
9202
9373
  const sourceFps = sourceSpec.character?.animation?.fps ?? sourceSpec.objectPro?.animation?.fps ?? sourceSpec.revision?.fps ?? sourceSpec.revision?.sourceFps;
9203
- const maskFile = asset.revision.mask ? import_node_path5.default.resolve(root, asset.revision.mask) : void 0;
9374
+ const maskFile = asset.revision.mask ? import_node_path6.default.resolve(root, asset.revision.mask) : void 0;
9204
9375
  const maskImage = maskFile ? await optionalRevisionImage(maskFile, "revision mask", "png") : null;
9205
9376
  if (sourceImage && maskImage && (sourceImage.width !== maskImage.width || sourceImage.height !== maskImage.height)) {
9206
9377
  throw new Error(
9207
9378
  `Revision mask for ${styleId}/${assetId} is ${maskImage.width}x${maskImage.height}; source ${asset.revision.from} is ${sourceImage.width}x${sourceImage.height}`
9208
9379
  );
9209
9380
  }
9210
- const paletteImageFile = asset.revision.paletteImage ? import_node_path5.default.resolve(root, asset.revision.paletteImage) : void 0;
9381
+ const paletteImageFile = asset.revision.paletteImage ? import_node_path6.default.resolve(root, asset.revision.paletteImage) : void 0;
9211
9382
  const paletteImage = paletteImageFile ? await optionalRevisionImage(paletteImageFile, "revision palette image", "png") : null;
9212
- const lastFrameFile = asset.revision.lastFrame ? import_node_path5.default.resolve(root, asset.revision.lastFrame) : void 0;
9383
+ const lastFrameFile = asset.revision.lastFrame ? import_node_path6.default.resolve(root, asset.revision.lastFrame) : void 0;
9213
9384
  const lastFrame = lastFrameFile ? await optionalRevisionImage(lastFrameFile, "revision last frame") : null;
9214
- const keypointsFile = asset.revision.keypointsFile ? import_node_path5.default.resolve(root, asset.revision.keypointsFile) : void 0;
9385
+ const keypointsFile = asset.revision.keypointsFile ? import_node_path6.default.resolve(root, asset.revision.keypointsFile) : void 0;
9215
9386
  const keypoints = keypointsFile ? await optionalSkeletonSet(keypointsFile) : null;
9216
9387
  resolved.revision = {
9217
9388
  mode: asset.revision.mode,
9218
9389
  sourceAssetId: asset.revision.from,
9390
+ ...sourceStyleId ? { sourceStyleId } : {},
9219
9391
  sourceFile,
9220
9392
  sourceSha256: sourceImage?.hash ?? null,
9221
9393
  sourceWidth: sourceImage?.width ?? null,
@@ -9337,6 +9509,8 @@ async function resolveSpecs(loaded, filter) {
9337
9509
  finalized.add(assetId);
9338
9510
  return resolved;
9339
9511
  };
9512
+ styleFinalizers.set(styleId, finalize);
9513
+ if (!emitStyles.has(styleId)) continue;
9340
9514
  for (const assetId of Object.keys(manifest.assets)) {
9341
9515
  if (!requestedAssetIds.has(assetId) || !styleSpecs.has(assetId)) continue;
9342
9516
  specs.push(await finalize(assetId));
@@ -9378,7 +9552,7 @@ async function resolveSpecs(loaded, filter) {
9378
9552
  return specs;
9379
9553
  }
9380
9554
  function pngPath(file) {
9381
- const extension = import_node_path5.default.extname(file);
9555
+ const extension = import_node_path6.default.extname(file);
9382
9556
  return extension ? `${file.slice(0, -extension.length)}.png` : `${file}.png`;
9383
9557
  }
9384
9558
  async function resolveMapScene(root, styleId, assetId, generator, scene, provider) {
@@ -9388,12 +9562,12 @@ async function resolveMapScene(root, styleId, assetId, generator, scene, provide
9388
9562
  if (!provider.supportsMapScene) {
9389
9563
  throw new Error(`assets.${assetId}.scene: provider "${provider.id}" cannot draw into a scene; PixelLab's /map-objects can`);
9390
9564
  }
9391
- const file = import_node_path5.default.resolve(root, scene.image);
9565
+ const file = import_node_path6.default.resolve(root, scene.image);
9392
9566
  const image = await optionalRevisionImage(file, "scene image", "png");
9393
9567
  const base = { file, sha256: image?.hash ?? null, width: image?.width ?? null, height: image?.height ?? null };
9394
9568
  const placement = scene.placement;
9395
9569
  if ("mask" in placement) {
9396
- const maskFile = import_node_path5.default.resolve(root, placement.mask);
9570
+ const maskFile = import_node_path6.default.resolve(root, placement.mask);
9397
9571
  const mask = await optionalRevisionImage(maskFile, "scene mask", "png");
9398
9572
  if (image && mask && (image.width !== mask.width || image.height !== mask.height)) {
9399
9573
  throw new Error(
@@ -9408,7 +9582,7 @@ async function resolveMapScene(root, styleId, assetId, generator, scene, provide
9408
9582
  return "oval" in placement ? { ...base, placement: { type: "oval", fraction: placement.oval } } : { ...base, placement: { type: "rectangle", fraction: placement.rectangle } };
9409
9583
  }
9410
9584
  async function optionalRevisionImage(file, label, requiredFormat) {
9411
- if (!(0, import_node_fs5.existsSync)(file)) return null;
9585
+ if (!(0, import_node_fs6.existsSync)(file)) return null;
9412
9586
  const bytes = await (0, import_promises3.readFile)(file);
9413
9587
  const metadata = imageMetadata(bytes);
9414
9588
  if (!metadata) throw new Error(`${label} is not a readable PNG or JPEG: ${file}`);
@@ -9418,7 +9592,7 @@ async function optionalRevisionImage(file, label, requiredFormat) {
9418
9592
  return { hash: sha256(bytes), ...metadata };
9419
9593
  }
9420
9594
  async function optionalSkeletonSet(file) {
9421
- if (!(0, import_node_fs5.existsSync)(file)) return null;
9595
+ if (!(0, import_node_fs6.existsSync)(file)) return null;
9422
9596
  const bytes = await (0, import_promises3.readFile)(file);
9423
9597
  let json;
9424
9598
  try {
@@ -9429,17 +9603,17 @@ async function optionalSkeletonSet(file) {
9429
9603
  return { hash: sha256(bytes), skeleton: parseSkeletonSet(json, file) };
9430
9604
  }
9431
9605
  async function revisionSourceMembers(stem, label) {
9432
- if ((0, import_node_fs5.existsSync)(stem)) return null;
9606
+ if ((0, import_node_fs6.existsSync)(stem)) return null;
9433
9607
  const at = (role) => memberPath(stem, role, 0, 2, MediaType.PNG);
9434
9608
  let roles = [];
9435
9609
  for (const set of [CHARACTER_DIRECTIONS_8, CHARACTER_DIRECTIONS_4]) {
9436
- if (set.every((direction) => (0, import_node_fs5.existsSync)(at(direction)))) {
9610
+ if (set.every((direction) => (0, import_node_fs6.existsSync)(at(direction)))) {
9437
9611
  roles = [...set];
9438
9612
  break;
9439
9613
  }
9440
9614
  }
9441
9615
  if (!roles.length) {
9442
- for (let index = 0; (0, import_node_fs5.existsSync)(at(`frame-${String(index).padStart(2, "0")}`)); index++) {
9616
+ for (let index = 0; (0, import_node_fs6.existsSync)(at(`frame-${String(index).padStart(2, "0")}`)); index++) {
9443
9617
  roles.push(`frame-${String(index).padStart(2, "0")}`);
9444
9618
  }
9445
9619
  }
@@ -9465,7 +9639,7 @@ async function resolveStyleImages(loaded, styleId) {
9465
9639
  if (!style) throw new Error(`Unknown style "${styleId}"`);
9466
9640
  const out = [];
9467
9641
  for (const img of style.styleImages) {
9468
- const file = import_node_path5.default.resolve(loaded.root, img.path);
9642
+ const file = import_node_path6.default.resolve(loaded.root, img.path);
9469
9643
  const buf = await (0, import_promises3.readFile)(file);
9470
9644
  const metadata = imageMetadata(buf);
9471
9645
  if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${file}`);
@@ -9496,7 +9670,7 @@ async function resolveCharacterShape(asset, style, kind, files) {
9496
9670
  };
9497
9671
  if (asset.concept) {
9498
9672
  const image = await files.load(asset.concept, "Concept image");
9499
- shape.concept = { path: import_node_path5.default.resolve(files.root, asset.concept), sha256: image.hash, width: image.width, height: image.height, format: image.format };
9673
+ shape.concept = { path: import_node_path6.default.resolve(files.root, asset.concept), sha256: image.hash, width: image.width, height: image.height, format: image.format };
9500
9674
  }
9501
9675
  if (asset.reference) {
9502
9676
  const byDirection = typeof asset.reference === "string" ? { south: asset.reference } : asset.reference;
@@ -9504,7 +9678,7 @@ async function resolveCharacterShape(asset, style, kind, files) {
9504
9678
  for (const [direction, rel] of Object.entries(byDirection)) {
9505
9679
  const image = await files.load(rel, `Reference sprite (${direction})`);
9506
9680
  shape.reference[direction] = {
9507
- path: import_node_path5.default.resolve(files.root, rel),
9681
+ path: import_node_path6.default.resolve(files.root, rel),
9508
9682
  sha256: image.hash,
9509
9683
  width: image.width,
9510
9684
  height: image.height,
@@ -9525,7 +9699,7 @@ async function resolveCharacterShape(asset, style, kind, files) {
9525
9699
  const animationMode = asset.animation.mode ?? (asset.animation.template ? "template" : "v3");
9526
9700
  const pose = async (rel, what) => {
9527
9701
  const image = await files.load(rel, what);
9528
- return { path: import_node_path5.default.resolve(files.root, rel), sha256: image.hash, width: image.width, height: image.height, format: image.format };
9702
+ return { path: import_node_path6.default.resolve(files.root, rel), sha256: image.hash, width: image.width, height: image.height, format: image.format };
9529
9703
  };
9530
9704
  shape.animation = {
9531
9705
  mode: animationMode,
@@ -9556,7 +9730,7 @@ async function resolveObjectProShape(assetId, asset, style, kind, files) {
9556
9730
  const south = typeof asset.reference === "string" ? asset.reference : asset.reference.south;
9557
9731
  if (south) {
9558
9732
  const image = await files.load(south, "Reference sprite");
9559
- shape.reference = { path: import_node_path5.default.resolve(files.root, south), sha256: image.hash, width: image.width, height: image.height, format: image.format };
9733
+ shape.reference = { path: import_node_path6.default.resolve(files.root, south), sha256: image.hash, width: image.width, height: image.height, format: image.format };
9560
9734
  }
9561
9735
  }
9562
9736
  if (asset.state) {
@@ -9584,7 +9758,7 @@ async function resolveObjectProShape(assetId, asset, style, kind, files) {
9584
9758
  }
9585
9759
  const pose = async (rel, what) => {
9586
9760
  const image = await files.load(rel, what);
9587
- return { path: import_node_path5.default.resolve(files.root, rel), sha256: image.hash, width: image.width, height: image.height, format: image.format };
9761
+ return { path: import_node_path6.default.resolve(files.root, rel), sha256: image.hash, width: image.width, height: image.height, format: image.format };
9588
9762
  };
9589
9763
  shape.animation = {
9590
9764
  mode: asset.animation.mode ?? "v3",
@@ -9604,11 +9778,11 @@ function characterDirections(directions) {
9604
9778
  }
9605
9779
 
9606
9780
  // src/project.ts
9607
- var import_node_path13 = __toESM(require("path"), 1);
9781
+ var import_node_path14 = __toESM(require("path"), 1);
9608
9782
 
9609
9783
  // src/env.ts
9610
- var import_node_fs6 = require("fs");
9611
- var import_node_path6 = __toESM(require("path"), 1);
9784
+ var import_node_fs7 = require("fs");
9785
+ var import_node_path7 = __toESM(require("path"), 1);
9612
9786
  function loadEnvFiles(dir) {
9613
9787
  const loaded = [];
9614
9788
  for (const [file, vars] of envFiles(dir)) {
@@ -9622,10 +9796,10 @@ function loadEnvFiles(dir) {
9622
9796
  function envFiles(dir) {
9623
9797
  const out = [];
9624
9798
  for (const name of [".env.local", ".env"]) {
9625
- const file = import_node_path6.default.join(dir, name);
9626
- if (!(0, import_node_fs6.existsSync)(file)) continue;
9799
+ const file = import_node_path7.default.join(dir, name);
9800
+ if (!(0, import_node_fs7.existsSync)(file)) continue;
9627
9801
  try {
9628
- out.push([file, parseEnv((0, import_node_fs6.readFileSync)(file, "utf8"))]);
9802
+ out.push([file, parseEnv((0, import_node_fs7.readFileSync)(file, "utf8"))]);
9629
9803
  } catch {
9630
9804
  }
9631
9805
  }
@@ -9651,12 +9825,12 @@ function parseEnv(contents) {
9651
9825
  }
9652
9826
 
9653
9827
  // src/pipeline/plan.ts
9654
- var import_node_fs9 = require("fs");
9655
- var import_node_path12 = __toESM(require("path"), 1);
9828
+ var import_node_fs10 = require("fs");
9829
+ var import_node_path13 = __toESM(require("path"), 1);
9656
9830
 
9657
9831
  // src/pipeline/mirror.ts
9658
9832
  var import_promises4 = require("fs/promises");
9659
- var import_node_path7 = __toESM(require("path"), 1);
9833
+ var import_node_path8 = __toESM(require("path"), 1);
9660
9834
 
9661
9835
  // src/palette.ts
9662
9836
  function normalizePalette(values) {
@@ -9795,7 +9969,7 @@ async function deriveMirror(spec, lock, lockPath, opts = {}) {
9795
9969
  for (const [index, member] of members.entries()) {
9796
9970
  const bytes = flipHorizontal(await (0, import_promises4.readFile)(member.file));
9797
9971
  const target = expectedOutputPath(spec, member.role, index, members.length, MediaType.PNG);
9798
- await (0, import_promises4.mkdir)(import_node_path7.default.dirname(target), { recursive: true });
9972
+ await (0, import_promises4.mkdir)(import_node_path8.default.dirname(target), { recursive: true });
9799
9973
  const tmp = `${target}.pixelkiln.tmp`;
9800
9974
  await (0, import_promises4.writeFile)(tmp, bytes);
9801
9975
  await (0, import_promises4.rename)(tmp, target);
@@ -9857,29 +10031,29 @@ async function deriveMirror(spec, lock, lockPath, opts = {}) {
9857
10031
  }
9858
10032
 
9859
10033
  // src/pipeline/quality-profile.ts
9860
- var import_node_fs7 = require("fs");
9861
- var import_node_path10 = __toESM(require("path"), 1);
10034
+ var import_node_fs8 = require("fs");
10035
+ var import_node_path11 = __toESM(require("path"), 1);
9862
10036
 
9863
10037
  // src/pipeline/refine.ts
9864
- var import_node_path9 = __toESM(require("path"), 1);
10038
+ var import_node_path10 = __toESM(require("path"), 1);
9865
10039
  var import_node_child_process2 = require("child_process");
9866
10040
  var import_promises6 = require("fs/promises");
9867
- var import_node_os2 = require("os");
10041
+ var import_node_os3 = require("os");
9868
10042
  var import_node_util = require("util");
9869
10043
 
9870
10044
  // src/artifacts.ts
9871
- var import_node_path8 = __toESM(require("path"), 1);
9872
- var import_node_crypto5 = require("crypto");
10045
+ var import_node_path9 = __toESM(require("path"), 1);
10046
+ var import_node_crypto6 = require("crypto");
9873
10047
  var import_promises5 = require("fs/promises");
9874
10048
  var activeTransactions = /* @__PURE__ */ new Set();
9875
10049
  function message(error) {
9876
10050
  return error instanceof Error ? error.message : String(error);
9877
10051
  }
9878
10052
  function digest(data) {
9879
- return (0, import_node_crypto5.createHash)("sha256").update(data).digest("hex");
10053
+ return (0, import_node_crypto6.createHash)("sha256").update(data).digest("hex");
9880
10054
  }
9881
10055
  function portableRelative(from, to) {
9882
- return import_node_path8.default.relative(from, import_node_path8.default.resolve(to)).split(import_node_path8.default.sep).join("/") || ".";
10056
+ return import_node_path9.default.relative(from, import_node_path9.default.resolve(to)).split(import_node_path9.default.sep).join("/") || ".";
9883
10057
  }
9884
10058
  function canonical2(value) {
9885
10059
  if (value === null || typeof value === "string" || typeof value === "boolean") return value;
@@ -9929,7 +10103,7 @@ function parseArtifactManifest(absolute, data) {
9929
10103
  return raw;
9930
10104
  }
9931
10105
  async function readArtifactBundleManifest(manifestPath) {
9932
- const absolute = import_node_path8.default.resolve(manifestPath);
10106
+ const absolute = import_node_path9.default.resolve(manifestPath);
9933
10107
  return parseArtifactManifest(absolute, await (0, import_promises5.readFile)(absolute, "utf8"));
9934
10108
  }
9935
10109
  async function readOptional(file) {
@@ -9953,8 +10127,8 @@ function processIsAlive(pid) {
9953
10127
  }
9954
10128
  }
9955
10129
  function validTemporaryPath(candidate, destination, type) {
9956
- return import_node_path8.default.dirname(candidate) === import_node_path8.default.dirname(destination) && import_node_path8.default.basename(candidate).startsWith(
9957
- `.${import_node_path8.default.basename(destination)}.pixelkiln-${type}-`
10130
+ return import_node_path9.default.dirname(candidate) === import_node_path9.default.dirname(destination) && import_node_path9.default.basename(candidate).startsWith(
10131
+ `.${import_node_path9.default.basename(destination)}.pixelkiln-${type}-`
9958
10132
  );
9959
10133
  }
9960
10134
  function parseTransaction(journal, data) {
@@ -9974,7 +10148,7 @@ async function removeTransactionFiles(journal) {
9974
10148
  await (0, import_promises5.rm)(transactionMarker(journal), { force: true });
9975
10149
  }
9976
10150
  async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
9977
- const journal = import_node_path8.default.resolve(recoveryFile);
10151
+ const journal = import_node_path9.default.resolve(recoveryFile);
9978
10152
  const bytes = await readOptional(journal);
9979
10153
  if (!bytes) {
9980
10154
  await (0, import_promises5.rm)(transactionMarker(journal), { force: true });
@@ -9990,7 +10164,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
9990
10164
  );
9991
10165
  }
9992
10166
  for (const entry of transaction.entries) {
9993
- if (typeof entry.destination !== "string" || typeof entry.stage !== "string" || typeof entry.sha256 !== "string" || entry.backup !== void 0 && typeof entry.backup !== "string" || !allowedDestinations.has(import_node_path8.default.resolve(entry.destination)) || !validTemporaryPath(entry.stage, entry.destination, "stage") || entry.backup !== void 0 && !validTemporaryPath(entry.backup, entry.destination, "backup")) {
10167
+ if (typeof entry.destination !== "string" || typeof entry.stage !== "string" || typeof entry.sha256 !== "string" || entry.backup !== void 0 && typeof entry.backup !== "string" || !allowedDestinations.has(import_node_path9.default.resolve(entry.destination)) || !validTemporaryPath(entry.stage, entry.destination, "stage") || entry.backup !== void 0 && !validTemporaryPath(entry.backup, entry.destination, "backup")) {
9994
10168
  throw new Error(
9995
10169
  `Refusing unsafe artifact recovery from ${journal}; its destinations or temporary paths do not match the current bundle.`
9996
10170
  );
@@ -10036,7 +10210,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
10036
10210
  await removeTransactionFiles(journal);
10037
10211
  }
10038
10212
  function createArtifactBundleManifest(manifestPath, outputs, provenance) {
10039
- const root = import_node_path8.default.dirname(import_node_path8.default.resolve(manifestPath));
10213
+ const root = import_node_path9.default.dirname(import_node_path9.default.resolve(manifestPath));
10040
10214
  const sources = provenance.sources.map((source) => ({
10041
10215
  ...source,
10042
10216
  path: portableRelative(root, source.path)
@@ -10066,11 +10240,11 @@ function withArtifactManifest(manifestPath, outputs, provenance) {
10066
10240
  ];
10067
10241
  }
10068
10242
  async function writeManagedArtifactBundle(manifestPath, outputs, provenance, options = {}) {
10069
- const absoluteManifest = import_node_path8.default.resolve(manifestPath);
10243
+ const absoluteManifest = import_node_path9.default.resolve(manifestPath);
10070
10244
  const recoveryFile = `${absoluteManifest}.transaction`;
10071
10245
  const allowedDestinations = /* @__PURE__ */ new Set([
10072
10246
  absoluteManifest,
10073
- ...outputs.map((output) => import_node_path8.default.resolve(output.path))
10247
+ ...outputs.map((output) => import_node_path9.default.resolve(output.path))
10074
10248
  ]);
10075
10249
  await recoverArtifactTransaction(recoveryFile, allowedDestinations);
10076
10250
  const existingManifestBytes = await readOptional(absoluteManifest);
@@ -10088,13 +10262,13 @@ async function writeManagedArtifactBundle(manifestPath, outputs, provenance, opt
10088
10262
  }
10089
10263
  }
10090
10264
  if (!options.force) {
10091
- const root = import_node_path8.default.dirname(absoluteManifest);
10265
+ const root = import_node_path9.default.dirname(absoluteManifest);
10092
10266
  const recorded = new Map(
10093
- previous?.outputs.map((output) => [import_node_path8.default.resolve(root, output.path), output.sha256]) ?? []
10267
+ previous?.outputs.map((output) => [import_node_path9.default.resolve(root, output.path), output.sha256]) ?? []
10094
10268
  );
10095
10269
  const conflicts = [];
10096
10270
  for (const output of outputs) {
10097
- const destination = import_node_path8.default.resolve(output.path);
10271
+ const destination = import_node_path9.default.resolve(output.path);
10098
10272
  const current = await readOptional(destination);
10099
10273
  if (!current || digest(current) === digest(output.data)) continue;
10100
10274
  const expected = recorded.get(destination);
@@ -10116,15 +10290,15 @@ Pass { force: true } (CLI: --force) to take ownership and replace the complete b
10116
10290
  );
10117
10291
  }
10118
10292
  async function verifyArtifactBundle(manifestPath) {
10119
- const absolute = import_node_path8.default.resolve(manifestPath);
10293
+ const absolute = import_node_path9.default.resolve(manifestPath);
10120
10294
  const raw = await readArtifactBundleManifest(absolute);
10121
- const root = import_node_path8.default.dirname(absolute);
10295
+ const root = import_node_path9.default.dirname(absolute);
10122
10296
  const changedSources = [];
10123
10297
  const changedOutputs = [];
10124
10298
  for (const source of raw.sources) {
10125
10299
  let actual = null;
10126
10300
  try {
10127
- actual = digest(await (0, import_promises5.readFile)(import_node_path8.default.resolve(root, source.path)));
10301
+ actual = digest(await (0, import_promises5.readFile)(import_node_path9.default.resolve(root, source.path)));
10128
10302
  } catch (error) {
10129
10303
  if (!isCode(error, "ENOENT")) throw error;
10130
10304
  }
@@ -10132,7 +10306,7 @@ async function verifyArtifactBundle(manifestPath) {
10132
10306
  }
10133
10307
  for (const output of raw.outputs) {
10134
10308
  try {
10135
- if (digest(await (0, import_promises5.readFile)(import_node_path8.default.resolve(root, output.path))) !== output.sha256) {
10309
+ if (digest(await (0, import_promises5.readFile)(import_node_path9.default.resolve(root, output.path))) !== output.sha256) {
10136
10310
  changedOutputs.push(output.path);
10137
10311
  }
10138
10312
  } catch (error) {
@@ -10188,7 +10362,7 @@ async function writeArtifactBundle(files, options = {}) {
10188
10362
  if (!files.length) throw new Error("An artifact bundle must contain at least one file.");
10189
10363
  const normalized = files.map((file) => {
10190
10364
  if (!file.path.trim()) throw new Error("Artifact paths cannot be empty.");
10191
- return { destination: import_node_path8.default.resolve(file.path), data: Buffer.from(file.data) };
10365
+ return { destination: import_node_path9.default.resolve(file.path), data: Buffer.from(file.data) };
10192
10366
  });
10193
10367
  const destinations = /* @__PURE__ */ new Set();
10194
10368
  for (const artifact of normalized) {
@@ -10216,23 +10390,23 @@ async function writeArtifactBundle(files, options = {}) {
10216
10390
  }
10217
10391
  }
10218
10392
  if (!changed.length) return { changed: [], unchanged };
10219
- const token = (0, import_node_crypto5.randomUUID)();
10393
+ const token = (0, import_node_crypto6.randomUUID)();
10220
10394
  for (const [index, artifact] of changed.entries()) {
10221
- const basename = import_node_path8.default.basename(artifact.destination);
10222
- artifact.stage = import_node_path8.default.join(
10223
- import_node_path8.default.dirname(artifact.destination),
10395
+ const basename = import_node_path9.default.basename(artifact.destination);
10396
+ artifact.stage = import_node_path9.default.join(
10397
+ import_node_path9.default.dirname(artifact.destination),
10224
10398
  `.${basename}.pixelkiln-stage-${token}-${index}`
10225
10399
  );
10226
10400
  if (artifact.existed) {
10227
- artifact.backup = import_node_path8.default.join(
10228
- import_node_path8.default.dirname(artifact.destination),
10401
+ artifact.backup = import_node_path9.default.join(
10402
+ import_node_path9.default.dirname(artifact.destination),
10229
10403
  `.${basename}.pixelkiln-backup-${token}-${index}`
10230
10404
  );
10231
10405
  }
10232
10406
  }
10233
- const journal = options.recoveryFile ? import_node_path8.default.resolve(options.recoveryFile) : null;
10407
+ const journal = options.recoveryFile ? import_node_path9.default.resolve(options.recoveryFile) : null;
10234
10408
  if (journal) {
10235
- await (0, import_promises5.mkdir)(import_node_path8.default.dirname(journal), { recursive: true });
10409
+ await (0, import_promises5.mkdir)(import_node_path9.default.dirname(journal), { recursive: true });
10236
10410
  const transaction = {
10237
10411
  format: "pixelkiln-artifact-transaction",
10238
10412
  version: 1,
@@ -10257,7 +10431,7 @@ async function writeArtifactBundle(files, options = {}) {
10257
10431
  }
10258
10432
  try {
10259
10433
  for (const [index, artifact] of changed.entries()) {
10260
- await (0, import_promises5.mkdir)(import_node_path8.default.dirname(artifact.destination), { recursive: true });
10434
+ await (0, import_promises5.mkdir)(import_node_path9.default.dirname(artifact.destination), { recursive: true });
10261
10435
  await options.beforeStage?.(artifact.destination, index);
10262
10436
  await (0, import_promises5.writeFile)(artifact.stage, artifact.data, { flag: "wx" });
10263
10437
  }
@@ -10523,10 +10697,10 @@ function sameFrameGrid(first, next) {
10523
10697
  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;
10524
10698
  }
10525
10699
  async function refineAsset(options) {
10526
- const source = import_node_path9.default.resolve(options.source);
10527
- const output = import_node_path9.default.resolve(options.output);
10700
+ const source = import_node_path10.default.resolve(options.source);
10701
+ const output = import_node_path10.default.resolve(options.output);
10528
10702
  if (source === output) throw new Error("Refinement source and output must be different files.");
10529
- const record = import_node_path9.default.resolve(qualityRecordPath(output));
10703
+ const record = import_node_path10.default.resolve(qualityRecordPath(output));
10530
10704
  const palette = normalizePalette(options.palette).map((color) => color.hex);
10531
10705
  const minGridConfidence = requireConfidence(options.minGridConfidence ?? "high");
10532
10706
  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.");
@@ -10549,8 +10723,8 @@ async function refineAsset(options) {
10549
10723
  );
10550
10724
  }
10551
10725
  }
10552
- const temp = await (0, import_promises6.mkdtemp)(import_node_path9.default.join((0, import_node_os2.tmpdir)(), "pixelkiln-refine-"));
10553
- const recovered = import_node_path9.default.join(temp, "native.png");
10726
+ const temp = await (0, import_promises6.mkdtemp)(import_node_path10.default.join((0, import_node_os3.tmpdir)(), "pixelkiln-refine-"));
10727
+ const recovered = import_node_path10.default.join(temp, "native.png");
10554
10728
  const fixerCommand = compatibilityCommand ?? options.fixerPython ?? process.env.PIXELKILN_PIXEL_FIXER_PYTHON ?? "python3";
10555
10729
  const args = compatibilityCommand ? [...options.fixerArgsPrefix ?? [], source, "--extract", recovered] : ["-c", PYTHON_FIXER_BRIDGE, source, recovered];
10556
10730
  try {
@@ -10635,18 +10809,18 @@ async function refineFrameSet(options) {
10635
10809
  if (roles.some((role) => !role.trim()) || new Set(roles).size !== roles.length) {
10636
10810
  throw new Error("Frame-set roles must be non-empty and unique.");
10637
10811
  }
10638
- const output = import_node_path9.default.resolve(options.output);
10639
- const record = import_node_path9.default.resolve(qualityRecordPath(output));
10812
+ const output = import_node_path10.default.resolve(options.output);
10813
+ const record = import_node_path10.default.resolve(qualityRecordPath(output));
10640
10814
  const palette = normalizeRefinementPalette(options.palette);
10641
10815
  const finalOutputs = options.sources.map((source, index) => qualityFrameOutputPath(output, source.role, index));
10642
10816
  if (new Set(finalOutputs).size !== finalOutputs.length) {
10643
10817
  throw new Error("Frame-set roles resolve to duplicate output filenames.");
10644
10818
  }
10645
- const sourceSet = new Set(options.sources.map((source) => import_node_path9.default.resolve(source.path)));
10819
+ const sourceSet = new Set(options.sources.map((source) => import_node_path10.default.resolve(source.path)));
10646
10820
  if (finalOutputs.some((file) => sourceSet.has(file))) {
10647
10821
  throw new Error("Frame-set refinement sources and outputs must be different files.");
10648
10822
  }
10649
- const temp = await (0, import_promises6.mkdtemp)(import_node_path9.default.join((0, import_node_os2.tmpdir)(), "pixelkiln-refine-frames-"));
10823
+ const temp = await (0, import_promises6.mkdtemp)(import_node_path10.default.join((0, import_node_os3.tmpdir)(), "pixelkiln-refine-frames-"));
10650
10824
  try {
10651
10825
  const frames = [];
10652
10826
  const outputs = [];
@@ -10654,8 +10828,8 @@ async function refineFrameSet(options) {
10654
10828
  const sourcePaths = [];
10655
10829
  let firstOptions = null;
10656
10830
  for (const [index, sourceEntry] of options.sources.entries()) {
10657
- const source = import_node_path9.default.resolve(sourceEntry.path);
10658
- const temporaryOutput = import_node_path9.default.join(temp, `${String(index).padStart(2, "0")}.png`);
10831
+ const source = import_node_path10.default.resolve(sourceEntry.path);
10832
+ const temporaryOutput = import_node_path10.default.join(temp, `${String(index).padStart(2, "0")}.png`);
10659
10833
  const refined = await refineAsset({
10660
10834
  source,
10661
10835
  output: temporaryOutput,
@@ -10730,7 +10904,7 @@ async function refineFrameSet(options) {
10730
10904
  );
10731
10905
  return {
10732
10906
  sources: sourcePaths,
10733
- outputs: outputs.map((item) => import_node_path9.default.resolve(item.path)),
10907
+ outputs: outputs.map((item) => import_node_path10.default.resolve(item.path)),
10734
10908
  record,
10735
10909
  frames,
10736
10910
  palette,
@@ -10741,12 +10915,12 @@ async function refineFrameSet(options) {
10741
10915
  }
10742
10916
  }
10743
10917
  async function checkQualityRecord(recordPath) {
10744
- const record = import_node_path9.default.resolve(recordPath);
10918
+ const record = import_node_path10.default.resolve(recordPath);
10745
10919
  const manifest = await readArtifactBundleManifest(record);
10746
10920
  const options = parseRecordOptions(manifest, record);
10747
10921
  const verification = await verifyArtifactBundle(record);
10748
- const sources = manifest.sources.map((source2) => import_node_path9.default.resolve(import_node_path9.default.dirname(record), source2.path));
10749
- const outputs = manifest.outputs.map((output2) => import_node_path9.default.resolve(import_node_path9.default.dirname(record), output2.path));
10922
+ const sources = manifest.sources.map((source2) => import_node_path10.default.resolve(import_node_path10.default.dirname(record), source2.path));
10923
+ const outputs = manifest.outputs.map((output2) => import_node_path10.default.resolve(import_node_path10.default.dirname(record), output2.path));
10750
10924
  const source = sources[0] ?? "";
10751
10925
  const output = outputs[0] ?? "";
10752
10926
  const expectedCount = options.frameSet?.count ?? 1;
@@ -10779,7 +10953,7 @@ async function checkQualityRecord(recordPath) {
10779
10953
  async function approveQualityRecord(recordPath, approval) {
10780
10954
  const reviewer = approval.reviewer.trim();
10781
10955
  if (!reviewer) throw new Error("A human reviewer name is required.");
10782
- const record = import_node_path9.default.resolve(recordPath);
10956
+ const record = import_node_path10.default.resolve(recordPath);
10783
10957
  const manifest = await readArtifactBundleManifest(record);
10784
10958
  const options = parseRecordOptions(manifest, record);
10785
10959
  const verification = await verifyArtifactBundle(record);
@@ -10800,14 +10974,14 @@ async function approveQualityRecord(recordPath, approval) {
10800
10974
  `A refinement record must contain exactly ${expectedCount} source(s) and output(s).`
10801
10975
  );
10802
10976
  }
10803
- const root = import_node_path9.default.dirname(record);
10977
+ const root = import_node_path10.default.dirname(record);
10804
10978
  const outputs = await Promise.all(manifest.outputs.map(async (entry) => {
10805
- const outputPath = import_node_path9.default.resolve(root, entry.path);
10979
+ const outputPath = import_node_path10.default.resolve(root, entry.path);
10806
10980
  return { path: outputPath, data: await (0, import_promises6.readFile)(outputPath) };
10807
10981
  }));
10808
10982
  const sources = manifest.sources.map((source) => ({
10809
10983
  ...source,
10810
- path: import_node_path9.default.resolve(root, source.path)
10984
+ path: import_node_path10.default.resolve(root, source.path)
10811
10985
  }));
10812
10986
  const approved = {
10813
10987
  ...options,
@@ -10839,8 +11013,8 @@ async function qualitySource(spec, lock) {
10839
11013
  if (spec.generator === "frames") {
10840
11014
  return { items: null, reason: "frame-set quality requires ordered downloaded provider outputs" };
10841
11015
  }
10842
- const source = import_node_path10.default.resolve(spec.root, spec.source);
10843
- return (0, import_node_fs7.existsSync)(source) ? { items: [{ role: "source", path: source }] } : { items: null, reason: `declared source is missing: ${spec.source}` };
11016
+ const source = import_node_path11.default.resolve(spec.root, spec.source);
11017
+ return (0, import_node_fs8.existsSync)(source) ? { items: [{ role: "source", path: source }] } : { items: null, reason: `declared source is missing: ${spec.source}` };
10844
11018
  }
10845
11019
  const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
10846
11020
  if (!entry) return { items: null, reason: "raw provider output is not in the lockfile" };
@@ -10869,7 +11043,7 @@ async function qualitySource(spec, lock) {
10869
11043
  }
10870
11044
  if (role) roles.add(role);
10871
11045
  const source = currentEntryOutputPath(entry, spec, index);
10872
- if (!(0, import_node_fs7.existsSync)(source)) return { items: null, reason: `raw provider output is missing: ${source}` };
11046
+ if (!(0, import_node_fs8.existsSync)(source)) return { items: null, reason: `raw provider output is missing: ${source}` };
10873
11047
  if (await sha256File(source) !== output.sha256) {
10874
11048
  return { items: null, reason: "raw provider output was modified after download" };
10875
11049
  }
@@ -10888,7 +11062,7 @@ async function inspectQualityProfile(spec, lock) {
10888
11062
  }
10889
11063
  const sourcePaths = source.items.map((item) => item.path);
10890
11064
  const expectedOutputs = spec.generator === "frames" ? source.items.map((item, index) => qualityFrameOutputPath(output, item.role, index)) : [output];
10891
- if (!(0, import_node_fs7.existsSync)(record)) {
11065
+ if (!(0, import_node_fs8.existsSync)(record)) {
10892
11066
  return {
10893
11067
  key,
10894
11068
  state: "needs-refinement",
@@ -10906,7 +11080,7 @@ async function inspectQualityProfile(spec, lock) {
10906
11080
  const expectedRevision = spec.quality.fixerRevision ?? PIXEL_ART_FIXER_REVISION;
10907
11081
  const expectedTransparency = spec.quality.minTransparency ?? null;
10908
11082
  const expectedFrameSet = spec.generator === "frames" ? { fps: spec.quality.fps ?? 12, count: source.items.length, roles: source.items.map((item) => item.role) } : null;
10909
- const contractChanged = JSON.stringify(check.sources.map((item) => import_node_path10.default.resolve(item))) !== JSON.stringify(sourcePaths.map((item) => import_node_path10.default.resolve(item))) || JSON.stringify(check.outputs.map((item) => import_node_path10.default.resolve(item))) !== JSON.stringify(expectedOutputs.map((item) => import_node_path10.default.resolve(item))) || JSON.stringify(check.options.palette.colors) !== JSON.stringify(expectedPalette) || check.options.nativeGrid.revision !== expectedRevision || check.options.audit.thresholds.minGridConfidence !== spec.quality.minGridConfidence || check.options.audit.thresholds.minTransparency !== expectedTransparency || (expectedFrameSet ? !check.options.frameSet || check.options.frameSet.fps !== expectedFrameSet.fps || check.options.frameSet.count !== expectedFrameSet.count || JSON.stringify(check.options.frameSet.roles) !== JSON.stringify(expectedFrameSet.roles) : check.options.frameSet !== void 0);
11083
+ const contractChanged = JSON.stringify(check.sources.map((item) => import_node_path11.default.resolve(item))) !== JSON.stringify(sourcePaths.map((item) => import_node_path11.default.resolve(item))) || JSON.stringify(check.outputs.map((item) => import_node_path11.default.resolve(item))) !== JSON.stringify(expectedOutputs.map((item) => import_node_path11.default.resolve(item))) || JSON.stringify(check.options.palette.colors) !== JSON.stringify(expectedPalette) || check.options.nativeGrid.revision !== expectedRevision || check.options.audit.thresholds.minGridConfidence !== spec.quality.minGridConfidence || check.options.audit.thresholds.minTransparency !== expectedTransparency || (expectedFrameSet ? !check.options.frameSet || check.options.frameSet.fps !== expectedFrameSet.fps || check.options.frameSet.count !== expectedFrameSet.count || JSON.stringify(check.options.frameSet.roles) !== JSON.stringify(expectedFrameSet.roles) : check.options.frameSet !== void 0);
10910
11084
  if (contractChanged) {
10911
11085
  return {
10912
11086
  key,
@@ -11069,8 +11243,8 @@ async function requireApprovedQualitySources(specs, lock, assetIds, outputRoles)
11069
11243
  }
11070
11244
 
11071
11245
  // src/pipeline/revision.ts
11072
- var import_node_fs8 = require("fs");
11073
- var import_node_path11 = __toESM(require("path"), 1);
11246
+ var import_node_fs9 = require("fs");
11247
+ var import_node_path12 = __toESM(require("path"), 1);
11074
11248
  async function inspectRevisionReadiness(spec, lock) {
11075
11249
  if (spec.mirror) return inspectMirror(spec, lock, /* @__PURE__ */ new Set());
11076
11250
  if (spec.character?.kind === "outfit") return inspectOutfit(spec, lock, /* @__PURE__ */ new Set());
@@ -11086,7 +11260,7 @@ async function inspectStyleAnchor(spec, lock, seen) {
11086
11260
  const label = `style anchor ${spec.styleId}/${anchor.assetId}`;
11087
11261
  const dependency = await inspectParent(anchor.spec, lock, seen);
11088
11262
  if (!dependency.ready) return { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
11089
- if (!anchor.sha256 || !(0, import_node_fs8.existsSync)(anchor.file)) return { ready: false, reason: `${label} has no generated south-facing file yet` };
11263
+ if (!anchor.sha256 || !(0, import_node_fs9.existsSync)(anchor.file)) return { ready: false, reason: `${label} has no generated south-facing file yet` };
11090
11264
  if (await sha256File(anchor.file) !== anchor.sha256) {
11091
11265
  return { ready: false, reason: `${label} changed after the manifest was resolved` };
11092
11266
  }
@@ -11100,7 +11274,7 @@ async function inspectScene(spec) {
11100
11274
  const files = [{ label: "scene image", file: scene.file, sha256: scene.sha256 }];
11101
11275
  if (scene.placement.type === "mask") files.push({ label: "scene mask", file: scene.placement.file, sha256: scene.placement.sha256 });
11102
11276
  for (const { label, file, sha256: sha2562 } of files) {
11103
- if (!sha2562 || !(0, import_node_fs8.existsSync)(file)) return { ready: false, reason: `${label} does not exist yet: ${file}` };
11277
+ if (!sha2562 || !(0, import_node_fs9.existsSync)(file)) return { ready: false, reason: `${label} does not exist yet: ${file}` };
11104
11278
  if (await sha256File(file) !== sha2562) return { ready: false, reason: `${label} changed after the manifest was resolved` };
11105
11279
  }
11106
11280
  return { ready: true, reason: "scene is current" };
@@ -11117,7 +11291,7 @@ async function inspectOutfit(spec, lock, seen) {
11117
11291
  const label = `outfit source ${spec.styleId}/${character.parentAssetId}`;
11118
11292
  const dependency = await inspectParent(character.parentSpec, lock, seen);
11119
11293
  if (!dependency.ready) return { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
11120
- if (!(0, import_node_fs8.existsSync)(outfit.reference.path)) {
11294
+ if (!(0, import_node_fs9.existsSync)(outfit.reference.path)) {
11121
11295
  return { ready: false, reason: `outfit reference is missing: ${outfit.reference.path}` };
11122
11296
  }
11123
11297
  if (await sha256File(outfit.reference.path) !== outfit.reference.sha256) {
@@ -11131,7 +11305,7 @@ async function inspectCharacterParent(spec, lock, seen) {
11131
11305
  const label = `${character.kind} parent ${spec.styleId}/${character.parentAssetId}`;
11132
11306
  const dependency = await inspectParent(parentSpec, lock, seen);
11133
11307
  if (!dependency.ready) return { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
11134
- if (!character.parentSha256 || !character.parentFile || !(0, import_node_fs8.existsSync)(character.parentFile)) {
11308
+ if (!character.parentSha256 || !character.parentFile || !(0, import_node_fs9.existsSync)(character.parentFile)) {
11135
11309
  return { ready: false, reason: `${label} has no generated south-facing file yet` };
11136
11310
  }
11137
11311
  if (await sha256File(character.parentFile) !== character.parentSha256) {
@@ -11149,7 +11323,7 @@ async function inspectObjectProParent(spec, lock, seen) {
11149
11323
  const label = `${object.kind} parent ${spec.styleId}/${object.parentAssetId}`;
11150
11324
  const dependency = await inspectParent(parentSpec, lock, seen);
11151
11325
  if (!dependency.ready) return { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
11152
- if (!object.parentSha256 || !object.parentFile || !(0, import_node_fs8.existsSync)(object.parentFile)) {
11326
+ if (!object.parentSha256 || !object.parentFile || !(0, import_node_fs9.existsSync)(object.parentFile)) {
11153
11327
  return { ready: false, reason: `${label} has no generated south-facing file yet` };
11154
11328
  }
11155
11329
  if (await sha256File(object.parentFile) !== object.parentSha256) {
@@ -11168,14 +11342,14 @@ async function inspectRevision(spec, lock, seen) {
11168
11342
  if (!dependency.ready) {
11169
11343
  return {
11170
11344
  ready: false,
11171
- reason: `revision source ${spec.styleId}/${revision.sourceAssetId} is not ready: ${dependency.reason}`
11345
+ reason: `revision source ${revision.sourceStyleId ?? spec.styleId}/${revision.sourceAssetId} is not ready: ${dependency.reason}`
11172
11346
  };
11173
11347
  }
11174
11348
  if (revision.sourceMembers) {
11175
11349
  const members = await inspectSourceMembers(spec, lock);
11176
11350
  if (!members.ready) return members;
11177
11351
  } else {
11178
- if (!revision.sourceSha256 || !(0, import_node_fs8.existsSync)(revision.sourceFile)) {
11352
+ if (!revision.sourceSha256 || !(0, import_node_fs9.existsSync)(revision.sourceFile)) {
11179
11353
  return { ready: false, reason: `revision source is missing: ${revision.sourceFile}` };
11180
11354
  }
11181
11355
  if (await sha256File(revision.sourceFile) !== revision.sourceSha256) {
@@ -11183,7 +11357,7 @@ async function inspectRevision(spec, lock, seen) {
11183
11357
  }
11184
11358
  }
11185
11359
  if (revision.maskFile) {
11186
- if (!revision.maskSha256 || !(0, import_node_fs8.existsSync)(revision.maskFile)) {
11360
+ if (!revision.maskSha256 || !(0, import_node_fs9.existsSync)(revision.maskFile)) {
11187
11361
  return { ready: false, reason: `revision mask is missing: ${revision.maskFile}` };
11188
11362
  }
11189
11363
  if (await sha256File(revision.maskFile) !== revision.maskSha256) {
@@ -11191,7 +11365,7 @@ async function inspectRevision(spec, lock, seen) {
11191
11365
  }
11192
11366
  }
11193
11367
  if (revision.lastFrameFile) {
11194
- if (!revision.lastFrameSha256 || !(0, import_node_fs8.existsSync)(revision.lastFrameFile)) {
11368
+ if (!revision.lastFrameSha256 || !(0, import_node_fs9.existsSync)(revision.lastFrameFile)) {
11195
11369
  return { ready: false, reason: `revision last frame is missing: ${revision.lastFrameFile}` };
11196
11370
  }
11197
11371
  if (await sha256File(revision.lastFrameFile) !== revision.lastFrameSha256) {
@@ -11204,7 +11378,7 @@ async function inspectSourceMembers(spec, lock) {
11204
11378
  const revision = spec.revision;
11205
11379
  const members = revision.sourceMembers;
11206
11380
  for (const member of members) {
11207
- if (!(0, import_node_fs8.existsSync)(member.file)) return { ready: false, reason: `revision source member is missing: ${member.file}` };
11381
+ if (!(0, import_node_fs9.existsSync)(member.file)) return { ready: false, reason: `revision source member is missing: ${member.file}` };
11208
11382
  if (await sha256File(member.file) !== member.sha256) {
11209
11383
  return { ready: false, reason: `revision source member changed after the manifest was resolved: ${member.file}` };
11210
11384
  }
@@ -11213,8 +11387,8 @@ async function inspectSourceMembers(spec, lock) {
11213
11387
  if (parent.source) return { ready: true, reason: "declared source set is present" };
11214
11388
  const entry = lock.entries[lockKey(parent.styleId, parent.assetId)];
11215
11389
  if (!entry) return { ready: false, reason: "revision source is not in the lockfile" };
11216
- const recorded = entry.outputs.map((_, index) => import_node_path11.default.resolve(currentEntryOutputPath(entry, parent, index))).sort();
11217
- const found = members.map((member) => import_node_path11.default.resolve(member.file)).sort();
11390
+ const recorded = entry.outputs.map((_, index) => import_node_path12.default.resolve(currentEntryOutputPath(entry, parent, index))).sort();
11391
+ const found = members.map((member) => import_node_path12.default.resolve(member.file)).sort();
11218
11392
  if (recorded.length !== found.length || recorded.some((file, index) => file !== found[index])) {
11219
11393
  return {
11220
11394
  ready: false,
@@ -11263,8 +11437,8 @@ async function inspectParent(spec, lock, seen) {
11263
11437
  };
11264
11438
  }
11265
11439
  if (spec.source) {
11266
- const source = import_node_path11.default.resolve(spec.root, spec.source);
11267
- return (0, import_node_fs8.existsSync)(source) ? { ready: true, reason: "declared source is present" } : { ready: false, reason: `declared source is missing: ${spec.source}` };
11440
+ const source = import_node_path12.default.resolve(spec.root, spec.source);
11441
+ return (0, import_node_fs9.existsSync)(source) ? { ready: true, reason: "declared source is present" } : { ready: false, reason: `declared source is missing: ${spec.source}` };
11268
11442
  }
11269
11443
  const entry = lock.entries[key];
11270
11444
  if (!entry) return { ready: false, reason: "parent is not in the lockfile" };
@@ -11278,7 +11452,7 @@ async function inspectParent(spec, lock, seen) {
11278
11452
  for (let index = 0; index < entry.outputs.length; index++) {
11279
11453
  const output = entry.outputs[index];
11280
11454
  const file = currentEntryOutputPath(entry, spec, index);
11281
- if (!(0, import_node_fs8.existsSync)(file)) return { ready: false, reason: `parent output is missing: ${file}` };
11455
+ if (!(0, import_node_fs9.existsSync)(file)) return { ready: false, reason: `parent output is missing: ${file}` };
11282
11456
  if (await sha256File(file) !== output.sha256) {
11283
11457
  return { ready: false, reason: `parent output was modified: ${file}` };
11284
11458
  }
@@ -11342,7 +11516,7 @@ async function buildPlan(specs, lock, opts = {}) {
11342
11516
  state = "missing";
11343
11517
  reason = "--force";
11344
11518
  } else if (!entry && spec.source && spec.generator !== "frames") {
11345
- if ((0, import_node_fs9.existsSync)(import_node_path12.default.resolve(spec.root, spec.source))) {
11519
+ if ((0, import_node_fs10.existsSync)(import_node_path13.default.resolve(spec.root, spec.source))) {
11346
11520
  state = "ok";
11347
11521
  reason = `placed from ${spec.source}; not generated`;
11348
11522
  } else {
@@ -11350,7 +11524,7 @@ async function buildPlan(specs, lock, opts = {}) {
11350
11524
  reason = `declared source is missing: ${spec.source}`;
11351
11525
  }
11352
11526
  } else if (!entry) {
11353
- if ((0, import_node_fs9.existsSync)(spec.outFile)) {
11527
+ if ((0, import_node_fs10.existsSync)(spec.outFile)) {
11354
11528
  state = "untracked";
11355
11529
  reason = "file on disk, no provenance recorded";
11356
11530
  } else {
@@ -11363,7 +11537,7 @@ async function buildPlan(specs, lock, opts = {}) {
11363
11537
  } else if (spec.mirror && !mirrorCurrent(entry, lock.entries[lockKey(spec.styleId, spec.mirror.sourceAssetId)])) {
11364
11538
  state = "stale";
11365
11539
  reason = `mirror source ${spec.mirror.sourceAssetId} changed; flip it again (no generation cost)`;
11366
- } else if (spec.mirror && entry.status === "downloaded" && entry.outputs.some((_output, index) => !(0, import_node_fs9.existsSync)(currentEntryOutputPath(entry, spec, index)))) {
11540
+ } else if (spec.mirror && entry.status === "downloaded" && entry.outputs.some((_output, index) => !(0, import_node_fs10.existsSync)(currentEntryOutputPath(entry, spec, index)))) {
11367
11541
  state = "stale";
11368
11542
  reason = "mirror output missing on disk; flip it again (no generation cost)";
11369
11543
  } else if (spec.character?.kind === "outfit" && entry.outfit?.sourceSha256 !== mirrorSourceHash(lock.entries[lockKey(spec.styleId, spec.character.parentAssetId)] ?? { outputs: [] })) {
@@ -11391,9 +11565,9 @@ async function buildPlan(specs, lock, opts = {}) {
11391
11565
  } else if (entry.status !== "downloaded") {
11392
11566
  state = "in-flight";
11393
11567
  reason = `awaiting ${entry.status}`;
11394
- } else if (entry.outputs.length === 0 || entry.outputs.some((_output, index) => !(0, import_node_fs9.existsSync)(currentEntryOutputPath(entry, spec, index)))) {
11568
+ } else if (entry.outputs.length === 0 || entry.outputs.some((_output, index) => !(0, import_node_fs10.existsSync)(currentEntryOutputPath(entry, spec, index)))) {
11395
11569
  state = "orphaned";
11396
- reason = entry.outputs.length === 0 ? "no recorded output files" : `missing on disk: ${entry.outputs.filter((_output, index) => !(0, import_node_fs9.existsSync)(currentEntryOutputPath(entry, spec, index))).map((o) => o.role ?? "asset").join(", ")}`;
11570
+ reason = entry.outputs.length === 0 ? "no recorded output files" : `missing on disk: ${entry.outputs.filter((_output, index) => !(0, import_node_fs10.existsSync)(currentEntryOutputPath(entry, spec, index))).map((o) => o.role ?? "asset").join(", ")}`;
11397
11571
  } else if (await anyOutputModified(entry, spec)) {
11398
11572
  state = "orphaned";
11399
11573
  reason = "output modified since download";
@@ -11484,13 +11658,13 @@ function summarize(plan) {
11484
11658
 
11485
11659
  // src/project.ts
11486
11660
  async function openProject(manifestPath = "pixelkiln.manifest.json", opts = {}) {
11487
- const absolute = import_node_path13.default.resolve(manifestPath);
11488
- const root = import_node_path13.default.dirname(absolute);
11661
+ const absolute = import_node_path14.default.resolve(manifestPath);
11662
+ const root = import_node_path14.default.dirname(absolute);
11489
11663
  if (opts.env !== false) {
11490
11664
  loadEnvFiles(root);
11491
- if (import_node_path13.default.resolve(process.cwd()) !== root) loadEnvFiles(process.cwd());
11665
+ if (import_node_path14.default.resolve(process.cwd()) !== root) loadEnvFiles(process.cwd());
11492
11666
  }
11493
- const lockPath = import_node_path13.default.resolve(opts.lockPath ?? import_node_path13.default.join(root, "pixelkiln.lock.json"));
11667
+ const lockPath = import_node_path14.default.resolve(opts.lockPath ?? import_node_path14.default.join(root, "pixelkiln.lock.json"));
11494
11668
  const loaded = await loadManifest(absolute);
11495
11669
  const specs = await resolveSpecs(loaded, { styles: opts.styles ?? [], assets: opts.assets ?? [] });
11496
11670
  const lock = await loadLock(lockPath);
@@ -11557,8 +11731,8 @@ function shouldPersistSourceUrl(value) {
11557
11731
  }
11558
11732
 
11559
11733
  // src/recipes.ts
11560
- var import_node_path14 = __toESM(require("path"), 1);
11561
- var import_node_fs10 = require("fs");
11734
+ var import_node_path15 = __toESM(require("path"), 1);
11735
+ var import_node_fs11 = require("fs");
11562
11736
  var import_promises7 = require("fs/promises");
11563
11737
  var import_node_url = require("url");
11564
11738
  var import_zod4 = require("zod");
@@ -11568,7 +11742,7 @@ var VERSION_RE = /^\d+\.\d+\.\d+$/;
11568
11742
  var RECIPE_FILE = "pixelkiln.recipe.json";
11569
11743
  var RECIPE_DIR_TOKEN = "{{recipeDir}}";
11570
11744
  var PortablePathSchema = import_zod4.z.string().min(1).refine((value) => {
11571
- if (value.includes("\\") || import_node_path14.default.posix.isAbsolute(value)) return false;
11745
+ if (value.includes("\\") || import_node_path15.default.posix.isAbsolute(value)) return false;
11572
11746
  const parts = value.split("/");
11573
11747
  return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
11574
11748
  }, "expected a portable relative path without '.', '..', or backslashes");
@@ -11744,7 +11918,7 @@ function message3(error) {
11744
11918
  return error instanceof Error ? error.message : String(error);
11745
11919
  }
11746
11920
  async function readRecipeFile(recipePath, bundled) {
11747
- const absolute = import_node_path14.default.resolve(recipePath);
11921
+ const absolute = import_node_path15.default.resolve(recipePath);
11748
11922
  let raw;
11749
11923
  try {
11750
11924
  raw = JSON.parse(await (0, import_promises7.readFile)(absolute, "utf8"));
@@ -11758,13 +11932,13 @@ async function readRecipeFile(recipePath, bundled) {
11758
11932
  ` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
11759
11933
  );
11760
11934
  }
11761
- return { recipe: parsed.data, authored: raw, path: absolute, dir: import_node_path14.default.dirname(absolute), bundled };
11935
+ return { recipe: parsed.data, authored: raw, path: absolute, dir: import_node_path15.default.dirname(absolute), bundled };
11762
11936
  }
11763
11937
  async function walkRecipeFiles(root) {
11764
- if (!(0, import_node_fs10.existsSync)(root)) return [];
11938
+ if (!(0, import_node_fs11.existsSync)(root)) return [];
11765
11939
  const out = [];
11766
11940
  for (const entry of await (0, import_promises7.readdir)(root, { withFileTypes: true })) {
11767
- const resolved = import_node_path14.default.join(root, entry.name);
11941
+ const resolved = import_node_path15.default.join(root, entry.name);
11768
11942
  if (entry.isDirectory()) out.push(...await walkRecipeFiles(resolved));
11769
11943
  else if (entry.isFile() && entry.name === RECIPE_FILE) out.push(resolved);
11770
11944
  }
@@ -11775,7 +11949,7 @@ function bundledRecipeRoot() {
11775
11949
  }
11776
11950
  async function listBundledRecipes(root = bundledRecipeRoot()) {
11777
11951
  const recipes = await Promise.all(
11778
- (await walkRecipeFiles(import_node_path14.default.resolve(root))).map((file) => readRecipeFile(file, true))
11952
+ (await walkRecipeFiles(import_node_path15.default.resolve(root))).map((file) => readRecipeFile(file, true))
11779
11953
  );
11780
11954
  return recipes.sort(
11781
11955
  (a, b) => a.recipe.id.localeCompare(b.recipe.id) || compareVersions(b.recipe.version, a.recipe.version)
@@ -11798,9 +11972,9 @@ function parseSelector(selector) {
11798
11972
  return { id: match[1], version: match[2] };
11799
11973
  }
11800
11974
  async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
11801
- const local = import_node_path14.default.resolve(target);
11802
- if ((0, import_node_fs10.existsSync)(local)) {
11803
- const recipePath = import_node_path14.default.basename(local) === RECIPE_FILE ? local : import_node_path14.default.join(local, RECIPE_FILE);
11975
+ const local = import_node_path15.default.resolve(target);
11976
+ if ((0, import_node_fs11.existsSync)(local)) {
11977
+ const recipePath = import_node_path15.default.basename(local) === RECIPE_FILE ? local : import_node_path15.default.join(local, RECIPE_FILE);
11804
11978
  return readRecipeFile(recipePath, false);
11805
11979
  }
11806
11980
  if (target.startsWith(".") || target.startsWith("/") || target.includes("\\")) {
@@ -11814,8 +11988,8 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
11814
11988
  return matches[0];
11815
11989
  }
11816
11990
  async function verifyFile(root, file) {
11817
- const absolute = import_node_path14.default.resolve(root, ...file.path.split("/"));
11818
- if (!(0, import_node_fs10.existsSync)(absolute)) {
11991
+ const absolute = import_node_path15.default.resolve(root, ...file.path.split("/"));
11992
+ if (!(0, import_node_fs11.existsSync)(absolute)) {
11819
11993
  return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
11820
11994
  }
11821
11995
  const actualSha256 = await sha256File(absolute);
@@ -11830,7 +12004,7 @@ async function verifyRecipe(target, options = {}) {
11830
12004
  const loaded = await resolveRecipe(target, options.bundledRoot);
11831
12005
  const actualIntegrity = recipeDigest(loaded.authored);
11832
12006
  const files = await Promise.all(loaded.recipe.files.map((file) => verifyFile(loaded.dir, file)));
11833
- const modelRoot = options.modelRoot ? import_node_path14.default.resolve(options.modelRoot) : null;
12007
+ const modelRoot = options.modelRoot ? import_node_path15.default.resolve(options.modelRoot) : null;
11834
12008
  const models = modelRoot ? await Promise.all(loaded.recipe.models.map(async (model) => ({
11835
12009
  ...await verifyFile(modelRoot, model),
11836
12010
  source: model.source,
@@ -11864,7 +12038,7 @@ async function verifyRecipe(target, options = {}) {
11864
12038
  };
11865
12039
  }
11866
12040
  function renderStyle(style, destination, cwd) {
11867
- const portableDir = import_node_path14.default.relative(cwd, destination).split(import_node_path14.default.sep).join("/") || ".";
12041
+ const portableDir = import_node_path15.default.relative(cwd, destination).split(import_node_path15.default.sep).join("/") || ".";
11868
12042
  const replace = (value) => {
11869
12043
  if (typeof value === "string") return value.replaceAll(RECIPE_DIR_TOKEN, portableDir);
11870
12044
  if (Array.isArray(value)) return value.map(replace);
@@ -11876,24 +12050,24 @@ function renderStyle(style, destination, cwd) {
11876
12050
  return replace(style);
11877
12051
  }
11878
12052
  async function installRecipe(target, options = {}) {
11879
- const cwd = import_node_path14.default.resolve(options.cwd ?? process.cwd());
12053
+ const cwd = import_node_path15.default.resolve(options.cwd ?? process.cwd());
11880
12054
  const loaded = await resolveRecipe(target, options.bundledRoot);
11881
12055
  const verification = await verifyRecipe(loaded.path);
11882
12056
  if (!verification.ok) {
11883
12057
  throw new Error(`Recipe ${loaded.recipe.id}@${loaded.recipe.version} failed integrity verification.`);
11884
12058
  }
11885
- const destination = options.out ? import_node_path14.default.resolve(cwd, options.out) : import_node_path14.default.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
12059
+ const destination = options.out ? import_node_path15.default.resolve(cwd, options.out) : import_node_path15.default.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
11886
12060
  const sources = [
11887
12061
  { path: RECIPE_FILE, source: loaded.path },
11888
- ...loaded.recipe.files.map((file) => ({ path: file.path, source: import_node_path14.default.join(loaded.dir, ...file.path.split("/")) }))
12062
+ ...loaded.recipe.files.map((file) => ({ path: file.path, source: import_node_path15.default.join(loaded.dir, ...file.path.split("/")) }))
11889
12063
  ];
11890
12064
  const files = await Promise.all(sources.map(async (file) => ({
11891
- path: import_node_path14.default.join(destination, ...file.path.split("/")),
12065
+ path: import_node_path15.default.join(destination, ...file.path.split("/")),
11892
12066
  data: await (0, import_promises7.readFile)(file.source)
11893
12067
  })));
11894
12068
  if (!options.force) {
11895
12069
  for (const file of files) {
11896
- if (!(0, import_node_fs10.existsSync)(file.path)) continue;
12070
+ if (!(0, import_node_fs11.existsSync)(file.path)) continue;
11897
12071
  const current = await (0, import_promises7.readFile)(file.path);
11898
12072
  if (!current.equals(file.data)) {
11899
12073
  throw new OverwriteRefusedError(
@@ -11919,8 +12093,8 @@ async function installRecipe(target, options = {}) {
11919
12093
 
11920
12094
  // src/pipeline/audit.ts
11921
12095
  var import_promises8 = require("fs/promises");
11922
- var import_node_fs11 = require("fs");
11923
- var import_node_path15 = __toESM(require("path"), 1);
12096
+ var import_node_fs12 = require("fs");
12097
+ var import_node_path16 = __toESM(require("path"), 1);
11924
12098
  function paletteDistance(asset, reference) {
11925
12099
  if (!asset.length || !reference.length) return 0;
11926
12100
  let total = 0;
@@ -11956,7 +12130,7 @@ async function auditStyle(loaded, specs, styleId, lock) {
11956
12130
  const unreadable = [];
11957
12131
  for (const spec of mine) {
11958
12132
  for (const output of resolveSpecOutputs(spec, lock, loaded.root)) {
11959
- if (!(0, import_node_fs11.existsSync)(output.absolutePath)) {
12133
+ if (!(0, import_node_fs12.existsSync)(output.absolutePath)) {
11960
12134
  missing.push(output.id);
11961
12135
  continue;
11962
12136
  }
@@ -11985,8 +12159,8 @@ async function auditStyle(loaded, specs, styleId, lock) {
11985
12159
  let referenceFromStyleImages = false;
11986
12160
  const refPalettes = [];
11987
12161
  for (const img of style.styleImages) {
11988
- const abs = import_node_path15.default.resolve(loaded.root, img.path);
11989
- if (!(0, import_node_fs11.existsSync)(abs)) continue;
12162
+ const abs = import_node_path16.default.resolve(loaded.root, img.path);
12163
+ if (!(0, import_node_fs12.existsSync)(abs)) continue;
11990
12164
  try {
11991
12165
  refPalettes.push(extractPalette(decodePng(await (0, import_promises8.readFile)(abs)), 12));
11992
12166
  } catch {
@@ -12064,19 +12238,19 @@ function hex(c) {
12064
12238
  }
12065
12239
 
12066
12240
  // src/pixellab-utils.ts
12067
- var import_node_fs12 = require("fs");
12241
+ var import_node_fs13 = require("fs");
12068
12242
  var import_promises9 = require("fs/promises");
12069
- var import_node_path16 = __toESM(require("path"), 1);
12243
+ var import_node_path17 = __toESM(require("path"), 1);
12070
12244
  var UNZOOM_MIN_SIDE = 256;
12071
12245
  var UNZOOM_MAX_AREA = 2048 * 2048;
12072
12246
  function defaultUnzoomOut(input) {
12073
- const ext = import_node_path16.default.extname(input);
12247
+ const ext = import_node_path17.default.extname(input);
12074
12248
  return `${ext ? input.slice(0, -ext.length) : input}.unzoomed.png`;
12075
12249
  }
12076
12250
  async function unzoomFile(client2, input, opts = {}) {
12077
- const out = import_node_path16.default.resolve(opts.out ?? defaultUnzoomOut(input));
12078
- if (import_node_path16.default.resolve(input) === out) throw new Error("unzoom will not overwrite its own input; pass a different --out");
12079
- if ((0, import_node_fs12.existsSync)(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
12251
+ const out = import_node_path17.default.resolve(opts.out ?? defaultUnzoomOut(input));
12252
+ if (import_node_path17.default.resolve(input) === out) throw new Error("unzoom will not overwrite its own input; pass a different --out");
12253
+ if ((0, import_node_fs13.existsSync)(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
12080
12254
  const bytes = await (0, import_promises9.readFile)(input);
12081
12255
  const metadata = imageMetadata(bytes);
12082
12256
  if (!metadata) throw new Error(`${input} is not a readable PNG or JPEG`);
@@ -12090,7 +12264,7 @@ async function unzoomFile(client2, input, opts = {}) {
12090
12264
  throw new Error(`${input} is ${width}x${height}; PixelLab's unzoom takes at most 2048x2048 worth of pixels`);
12091
12265
  }
12092
12266
  const res = await client2.unzoom({ image: { base64: bytes.toString("base64"), format }, quantize: opts.quantize });
12093
- await (0, import_promises9.mkdir)(import_node_path16.default.dirname(out), { recursive: true });
12267
+ await (0, import_promises9.mkdir)(import_node_path17.default.dirname(out), { recursive: true });
12094
12268
  await (0, import_promises9.writeFile)(out, res.png);
12095
12269
  return { out, original: res.originalSize, unzoomed: res.unzoomedSize, zoomFactor: res.zoomFactor, usage: res.usage };
12096
12270
  }
@@ -12098,14 +12272,14 @@ var FONT_WEIGHTS = ["Bold", "Regular"];
12098
12272
  var FONT_GLYPH_SIZES = [8, 16, 32, 64];
12099
12273
  var FONT_COST_GENERATIONS = 25;
12100
12274
  function fontOutputPaths(out) {
12101
- const base = import_node_path16.default.resolve(out.replace(/\.(ttf|png)$/i, ""));
12275
+ const base = import_node_path17.default.resolve(out.replace(/\.(ttf|png)$/i, ""));
12102
12276
  return { ttf: `${base}.ttf`, atlas: `${base}.png` };
12103
12277
  }
12104
12278
  async function generateFont(client2, opts) {
12105
12279
  const { ttf, atlas } = fontOutputPaths(opts.out);
12106
12280
  if (!opts.force) {
12107
12281
  for (const file of [ttf, atlas]) {
12108
- if ((0, import_node_fs12.existsSync)(file)) throw new Error(`${file} already exists; pass --force to replace it`);
12282
+ if ((0, import_node_fs13.existsSync)(file)) throw new Error(`${file} already exists; pass --force to replace it`);
12109
12283
  }
12110
12284
  }
12111
12285
  const sleep3 = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
@@ -12132,7 +12306,7 @@ async function generateFont(client2, opts) {
12132
12306
  client2.download(job.download_ttf_url),
12133
12307
  client2.download(job.download_atlas_url)
12134
12308
  ]);
12135
- await (0, import_promises9.mkdir)(import_node_path16.default.dirname(ttf), { recursive: true });
12309
+ await (0, import_promises9.mkdir)(import_node_path17.default.dirname(ttf), { recursive: true });
12136
12310
  await (0, import_promises9.writeFile)(ttf, ttfBytes);
12137
12311
  await (0, import_promises9.writeFile)(atlas, atlasBytes);
12138
12312
  return { jobId, ttf, atlas, glyphPx: job.glyph_px ?? null, usage: job.usage };
@@ -12147,15 +12321,15 @@ async function generateFont(client2, opts) {
12147
12321
  }
12148
12322
 
12149
12323
  // src/pipeline/quality-regression.ts
12150
- var import_node_path17 = __toESM(require("path"), 1);
12151
- var import_node_fs13 = require("fs");
12324
+ var import_node_path18 = __toESM(require("path"), 1);
12325
+ var import_node_fs14 = require("fs");
12152
12326
  var import_promises10 = require("fs/promises");
12153
12327
  var import_zod5 = require("zod");
12154
12328
  var SHA256_RE2 = /^[0-9a-f]{64}$/;
12155
12329
  var HEX_RE = /^#[0-9a-f]{6}$/;
12156
12330
  var MAX_REDMEAN_DISTANCE = 765;
12157
12331
  var QualityPathSchema = import_zod5.z.string().min(1).refine(
12158
- (value) => !import_node_path17.default.posix.isAbsolute(value) && !value.includes("\\"),
12332
+ (value) => !import_node_path18.default.posix.isAbsolute(value) && !value.includes("\\"),
12159
12333
  "expected a portable relative path"
12160
12334
  );
12161
12335
  var DEFAULT_QUALITY_TOLERANCES = {
@@ -12302,7 +12476,7 @@ function measureDecodedImageQuality(png, digest2) {
12302
12476
  };
12303
12477
  }
12304
12478
  async function measureImageQuality(file) {
12305
- const absolute = import_node_path17.default.resolve(file);
12479
+ const absolute = import_node_path18.default.resolve(file);
12306
12480
  let bytes;
12307
12481
  try {
12308
12482
  bytes = await (0, import_promises10.readFile)(absolute);
@@ -12324,11 +12498,11 @@ function resolveQualityInputs(raw, inputsFilePath) {
12324
12498
  }
12325
12499
  const ids = parsed.data.map((entry) => entry.id);
12326
12500
  if (new Set(ids).size !== ids.length) throw new Error("--inputs contains a duplicate quality case id.");
12327
- const root = import_node_path17.default.dirname(import_node_path17.default.resolve(inputsFilePath));
12501
+ const root = import_node_path18.default.dirname(import_node_path18.default.resolve(inputsFilePath));
12328
12502
  return parsed.data.map((entry) => ({
12329
12503
  id: entry.id,
12330
- file: import_node_path17.default.resolve(root, entry.path),
12331
- ...entry.record ? { record: import_node_path17.default.resolve(root, entry.record) } : {},
12504
+ file: import_node_path18.default.resolve(root, entry.path),
12505
+ ...entry.record ? { record: import_node_path18.default.resolve(root, entry.record) } : {},
12332
12506
  tolerances: QualityToleranceSchema.parse({
12333
12507
  ...DEFAULT_QUALITY_TOLERANCES,
12334
12508
  ...entry.tolerances
@@ -12336,7 +12510,7 @@ function resolveQualityInputs(raw, inputsFilePath) {
12336
12510
  }));
12337
12511
  }
12338
12512
  function portableRelative2(from, to) {
12339
- return import_node_path17.default.relative(from, import_node_path17.default.resolve(to)).split(import_node_path17.default.sep).join("/") || ".";
12513
+ return import_node_path18.default.relative(from, import_node_path18.default.resolve(to)).split(import_node_path18.default.sep).join("/") || ".";
12340
12514
  }
12341
12515
  async function verifyRecordForImage(recordPath, imagePath) {
12342
12516
  const verification = await verifyArtifactBundle(recordPath);
@@ -12351,20 +12525,20 @@ async function verifyRecordForImage(recordPath, imagePath) {
12351
12525
  if (record.kind !== "refine" || options === null || typeof options !== "object" || options.schema !== "pixelkiln-quality") {
12352
12526
  throw new Error(`Quality record ${recordPath} is not a PixelKiln refinement record.`);
12353
12527
  }
12354
- const outputs = record.outputs.map((output) => import_node_path17.default.resolve(import_node_path17.default.dirname(recordPath), output.path));
12355
- if (!outputs.includes(import_node_path17.default.resolve(imagePath))) {
12528
+ const outputs = record.outputs.map((output) => import_node_path18.default.resolve(import_node_path18.default.dirname(recordPath), output.path));
12529
+ if (!outputs.includes(import_node_path18.default.resolve(imagePath))) {
12356
12530
  throw new Error(`Quality record ${recordPath} does not own ${imagePath}.`);
12357
12531
  }
12358
12532
  }
12359
12533
  async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
12360
12534
  if (!inputs.length) throw new Error("Cannot snapshot an empty quality baseline.");
12361
- const absolute = import_node_path17.default.resolve(baselinePath);
12535
+ const absolute = import_node_path18.default.resolve(baselinePath);
12362
12536
  for (const input of inputs) {
12363
- if (import_node_path17.default.resolve(input.file) === absolute || input.record && import_node_path17.default.resolve(input.record) === absolute) {
12537
+ if (import_node_path18.default.resolve(input.file) === absolute || input.record && import_node_path18.default.resolve(input.record) === absolute) {
12364
12538
  throw new Error(`Quality baseline output would overwrite an input: ${absolute}.`);
12365
12539
  }
12366
12540
  }
12367
- const root = import_node_path17.default.dirname(absolute);
12541
+ const root = import_node_path18.default.dirname(absolute);
12368
12542
  const cases = [];
12369
12543
  for (const input of [...inputs].sort((a, b) => a.id.localeCompare(b.id))) {
12370
12544
  const expected = await measureImageQuality(input.file);
@@ -12391,7 +12565,7 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
12391
12565
  cases
12392
12566
  });
12393
12567
  const data = Buffer.from(JSON.stringify(baseline, null, 2) + "\n");
12394
- if ((0, import_node_fs13.existsSync)(absolute) && !options.force) {
12568
+ if ((0, import_node_fs14.existsSync)(absolute) && !options.force) {
12395
12569
  const current = await (0, import_promises10.readFile)(absolute);
12396
12570
  if (!current.equals(data)) {
12397
12571
  throw new OverwriteRefusedError(
@@ -12404,7 +12578,7 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
12404
12578
  return { baseline, path: absolute, changed: result.changed.length > 0 };
12405
12579
  }
12406
12580
  async function readQualityBaseline(baselinePath) {
12407
- const absolute = import_node_path17.default.resolve(baselinePath);
12581
+ const absolute = import_node_path18.default.resolve(baselinePath);
12408
12582
  let raw;
12409
12583
  try {
12410
12584
  raw = JSON.parse(await (0, import_promises10.readFile)(absolute, "utf8"));
@@ -12477,12 +12651,12 @@ function regressionViolations(expected, actual, tolerances) {
12477
12651
  return { violations, warnings };
12478
12652
  }
12479
12653
  async function checkQualityBaseline(baselinePath) {
12480
- const absolute = import_node_path17.default.resolve(baselinePath);
12654
+ const absolute = import_node_path18.default.resolve(baselinePath);
12481
12655
  const baseline = await readQualityBaseline(absolute);
12482
- const root = import_node_path17.default.dirname(absolute);
12656
+ const root = import_node_path18.default.dirname(absolute);
12483
12657
  const cases = [];
12484
12658
  for (const entry of baseline.cases) {
12485
- const file = import_node_path17.default.resolve(root, entry.file);
12659
+ const file = import_node_path18.default.resolve(root, entry.file);
12486
12660
  let actual = null;
12487
12661
  const violations = [];
12488
12662
  const warnings = [];
@@ -12495,7 +12669,7 @@ async function checkQualityBaseline(baselinePath) {
12495
12669
  violations.push(message4(error));
12496
12670
  }
12497
12671
  if (entry.record) {
12498
- const recordPath = import_node_path17.default.resolve(root, entry.record.path);
12672
+ const recordPath = import_node_path18.default.resolve(root, entry.record.path);
12499
12673
  try {
12500
12674
  const recordHash = await sha256File(recordPath);
12501
12675
  if (recordHash !== entry.record.sha256) violations.push("quality record hash changed");
@@ -12531,14 +12705,14 @@ async function checkQualityBaseline(baselinePath) {
12531
12705
  }
12532
12706
 
12533
12707
  // src/pipeline/cache-health.ts
12534
- var import_node_fs17 = require("fs");
12708
+ var import_node_fs18 = require("fs");
12535
12709
  var import_promises13 = require("fs/promises");
12536
- var import_node_path21 = __toESM(require("path"), 1);
12710
+ var import_node_path22 = __toESM(require("path"), 1);
12537
12711
 
12538
12712
  // src/cache.ts
12539
12713
  var import_promises11 = require("fs/promises");
12540
- var import_node_fs14 = require("fs");
12541
- var import_node_path18 = __toESM(require("path"), 1);
12714
+ var import_node_fs15 = require("fs");
12715
+ var import_node_path19 = __toESM(require("path"), 1);
12542
12716
  var import_zod6 = require("zod");
12543
12717
  var HashCacheSchema = import_zod6.z.object({
12544
12718
  version: import_zod6.z.literal(1),
@@ -12552,10 +12726,10 @@ function isSha256Hash(value) {
12552
12726
  function parseCache(value) {
12553
12727
  return HashCacheSchema.parse(value);
12554
12728
  }
12555
- async function loadCache(path37) {
12556
- if (!(0, import_node_fs14.existsSync)(path37)) return { version: 1, hashes: {} };
12729
+ async function loadCache(path38) {
12730
+ if (!(0, import_node_fs15.existsSync)(path38)) return { version: 1, hashes: {} };
12557
12731
  try {
12558
- const parsed = HashCacheSchema.safeParse(JSON.parse(await (0, import_promises11.readFile)(path37, "utf8")));
12732
+ const parsed = HashCacheSchema.safeParse(JSON.parse(await (0, import_promises11.readFile)(path38, "utf8")));
12559
12733
  if (!parsed.success) return { version: 1, hashes: {} };
12560
12734
  return {
12561
12735
  version: 1,
@@ -12567,18 +12741,18 @@ async function loadCache(path37) {
12567
12741
  return { version: 1, hashes: {} };
12568
12742
  }
12569
12743
  }
12570
- async function saveCache(path37, cache) {
12744
+ async function saveCache(path38, cache) {
12571
12745
  const sorted = {};
12572
12746
  for (const key of Object.keys(cache.hashes).sort()) {
12573
12747
  const hash = cache.hashes[key];
12574
12748
  if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
12575
12749
  sorted[key] = hash;
12576
12750
  }
12577
- await (0, import_promises11.mkdir)(import_node_path18.default.dirname(import_node_path18.default.resolve(path37)), { recursive: true });
12578
- const tmp = `${path37}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
12751
+ await (0, import_promises11.mkdir)(import_node_path19.default.dirname(import_node_path19.default.resolve(path38)), { recursive: true });
12752
+ const tmp = `${path38}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
12579
12753
  try {
12580
12754
  await (0, import_promises11.writeFile)(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
12581
- await (0, import_promises11.rename)(tmp, path37);
12755
+ await (0, import_promises11.rename)(tmp, path38);
12582
12756
  } finally {
12583
12757
  await (0, import_promises11.rm)(tmp, { force: true });
12584
12758
  }
@@ -12602,20 +12776,20 @@ function cachePathFor(lockPath) {
12602
12776
  }
12603
12777
 
12604
12778
  // src/pipeline/history.ts
12605
- var import_node_fs16 = require("fs");
12606
- var import_node_path20 = __toESM(require("path"), 1);
12779
+ var import_node_fs17 = require("fs");
12780
+ var import_node_path21 = __toESM(require("path"), 1);
12607
12781
 
12608
12782
  // src/pipeline/fetch.ts
12609
- var import_node_fs15 = require("fs");
12783
+ var import_node_fs16 = require("fs");
12610
12784
  var import_promises12 = require("fs/promises");
12611
- var import_node_path19 = __toESM(require("path"), 1);
12785
+ var import_node_path20 = __toESM(require("path"), 1);
12612
12786
  async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12613
12787
  const log = opts.onProgress ?? (() => {
12614
12788
  });
12615
12789
  const result = { downloaded: 0, skipped: 0, failed: 0, ...opts.refresh ? { unchanged: 0 } : {} };
12616
12790
  const specByKey = new Map(specs.map((s) => [lockKey(s.styleId, s.assetId), s]));
12617
12791
  normalizeLockOutputPaths(lock, specs);
12618
- const cacheDir = opts.cacheDir === false ? null : import_node_path19.default.resolve(opts.cacheDir ?? import_node_path19.default.join(import_node_path19.default.dirname(lockPath), ".pixelkiln", "cache"));
12792
+ const cacheDir = opts.cacheDir === false ? null : import_node_path20.default.resolve(opts.cacheDir ?? import_node_path20.default.join(import_node_path20.default.dirname(lockPath), ".pixelkiln", "cache"));
12619
12793
  const pending = Object.entries(lock.entries).filter(([key, e]) => {
12620
12794
  if (e.provider !== provider.id || !specByKey.has(key)) return false;
12621
12795
  if (specByKey.get(key)?.mirror) return false;
@@ -12626,15 +12800,15 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12626
12800
  if (spec && e.specHash === spec.specHash && e.outputs.length && !postprocessCurrent(e, spec)) {
12627
12801
  return Boolean(opts.force) || e.outputs.every((output) => {
12628
12802
  const file = resolveOutputPath(output.path, spec.root);
12629
- return (0, import_node_fs15.existsSync)(file) && sha256((0, import_node_fs15.readFileSync)(file)) === output.sha256;
12803
+ return (0, import_node_fs16.existsSync)(file) && sha256((0, import_node_fs16.readFileSync)(file)) === output.sha256;
12630
12804
  });
12631
12805
  }
12632
12806
  if (!opts.repair) return false;
12633
12807
  if (e.outputs.length === 0 || !spec) return true;
12634
12808
  return e.outputs.some((output) => {
12635
12809
  const file = resolveOutputPath(output.path, spec.root);
12636
- if (!(0, import_node_fs15.existsSync)(file)) return true;
12637
- const current = sha256((0, import_node_fs15.readFileSync)(file));
12810
+ if (!(0, import_node_fs16.existsSync)(file)) return true;
12811
+ const current = sha256((0, import_node_fs16.readFileSync)(file));
12638
12812
  if (current === output.sha256) return false;
12639
12813
  return Boolean(opts.force) || (e.supersededOutputs ?? []).some((old) => old.sha256 === current);
12640
12814
  });
@@ -12717,14 +12891,14 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12717
12891
  if (!source.url) throw new Error(`no durable source URL to refresh ${source.role ?? "asset"} from`);
12718
12892
  fresh = await provider.download(source.url);
12719
12893
  if (recorded && sha256(fresh) === recordedRaw && !reapply) {
12720
- log(` current ${import_node_path19.default.relative(process.cwd(), target)}`);
12894
+ log(` current ${import_node_path20.default.relative(process.cwd(), target)}`);
12721
12895
  outputs.push({ ...recorded, path: portableOutputPath(target, spec.root) });
12722
12896
  continue;
12723
12897
  }
12724
12898
  }
12725
12899
  let raw = null;
12726
12900
  let final = null;
12727
- if ((0, import_node_fs15.existsSync)(target)) {
12901
+ if ((0, import_node_fs16.existsSync)(target)) {
12728
12902
  const currentHash = await sha256File(target);
12729
12903
  if (recorded && currentHash === recorded.sha256) {
12730
12904
  if (!fresh && !reapply) {
@@ -12741,9 +12915,9 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12741
12915
  }
12742
12916
  if (reapply && !fresh) {
12743
12917
  if (!recorded.raw) raw = await (0, import_promises12.readFile)(target);
12744
- log(` reapply ${import_node_path19.default.relative(process.cwd(), target)}`);
12918
+ log(` reapply ${import_node_path20.default.relative(process.cwd(), target)}`);
12745
12919
  } else {
12746
- log(` changed ${import_node_path19.default.relative(process.cwd(), target)} (upstream)`);
12920
+ log(` changed ${import_node_path20.default.relative(process.cwd(), target)} (upstream)`);
12747
12921
  }
12748
12922
  } else {
12749
12923
  const superseded = (entry.supersededOutputs ?? []).find(
@@ -12771,7 +12945,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12771
12945
  }
12772
12946
  }
12773
12947
  log(
12774
- ` replace ${import_node_path19.default.relative(process.cwd(), target)}` + (opts.force ? " (--force)" : " (previous tracked generation)")
12948
+ ` replace ${import_node_path20.default.relative(process.cwd(), target)}` + (opts.force ? " (--force)" : " (previous tracked generation)")
12775
12949
  );
12776
12950
  }
12777
12951
  }
@@ -12780,7 +12954,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12780
12954
  } else if (!raw && recorded && cacheDir) {
12781
12955
  if (!reapply) final = await readCachedMedia(cacheDir, recorded.sha256, expectedMediaType);
12782
12956
  if (!final && recordedRaw) raw = await readCachedMedia(cacheDir, recordedRaw, expectedMediaType);
12783
- if (final || raw) log(` cached ${import_node_path19.default.relative(process.cwd(), target)}`);
12957
+ if (final || raw) log(` cached ${import_node_path20.default.relative(process.cwd(), target)}`);
12784
12958
  }
12785
12959
  if (!final && !raw) {
12786
12960
  if (!source.url) {
@@ -12807,11 +12981,11 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12807
12981
  buf = processed.bytes;
12808
12982
  if (processed.changed) {
12809
12983
  rawHash = sha256(raw);
12810
- log(` snapped ${import_node_path19.default.relative(process.cwd(), target)} to ${wanted.palette.colors.length} colours`);
12984
+ log(` snapped ${import_node_path20.default.relative(process.cwd(), target)} to ${wanted.palette.colors.length} colours`);
12811
12985
  }
12812
12986
  }
12813
12987
  if (cacheDir) await cacheMedia(cacheDir, buf, mediaType);
12814
- await (0, import_promises12.mkdir)(import_node_path19.default.dirname(target), { recursive: true });
12988
+ await (0, import_promises12.mkdir)(import_node_path20.default.dirname(target), { recursive: true });
12815
12989
  const tmp = `${target}.pixelkiln.tmp`;
12816
12990
  await (0, import_promises12.writeFile)(tmp, buf);
12817
12991
  outputs.push({
@@ -12825,7 +12999,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12825
12999
  await saveLock(lockPath, lock);
12826
13000
  await (0, import_promises12.rename)(tmp, target);
12827
13001
  wrote = true;
12828
- log(` wrote ${import_node_path19.default.relative(process.cwd(), target)}`);
13002
+ log(` wrote ${import_node_path20.default.relative(process.cwd(), target)}`);
12829
13003
  }
12830
13004
  if (opts.refresh && !wrote) {
12831
13005
  if (rotated) {
@@ -12869,8 +13043,8 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12869
13043
  return result;
12870
13044
  }
12871
13045
  async function readCachedMedia(cacheDir, hash, mediaType) {
12872
- const file = import_node_path19.default.join(cacheDir, cacheFileName(hash, mediaType));
12873
- if (!(0, import_node_fs15.existsSync)(file)) return null;
13046
+ const file = import_node_path20.default.join(cacheDir, cacheFileName(hash, mediaType));
13047
+ if (!(0, import_node_fs16.existsSync)(file)) return null;
12874
13048
  try {
12875
13049
  const buf = await (0, import_promises12.readFile)(file);
12876
13050
  if (sha256(buf) !== hash) return null;
@@ -12883,7 +13057,7 @@ async function readCachedMedia(cacheDir, hash, mediaType) {
12883
13057
  async function cacheMedia(cacheDir, buf, mediaType, knownHash) {
12884
13058
  const hash = knownHash ?? sha256(buf);
12885
13059
  validateMedia(buf, mediaType);
12886
- const file = import_node_path19.default.join(cacheDir, cacheFileName(hash, mediaType));
13060
+ const file = import_node_path20.default.join(cacheDir, cacheFileName(hash, mediaType));
12887
13061
  if (await readCachedMedia(cacheDir, hash, mediaType)) return;
12888
13062
  await (0, import_promises12.mkdir)(cacheDir, { recursive: true });
12889
13063
  const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
@@ -12891,7 +13065,7 @@ async function cacheMedia(cacheDir, buf, mediaType, knownHash) {
12891
13065
  try {
12892
13066
  await (0, import_promises12.rename)(tmp, file);
12893
13067
  } catch (err) {
12894
- if (!(0, import_node_fs15.existsSync)(file)) throw err;
13068
+ if (!(0, import_node_fs16.existsSync)(file)) throw err;
12895
13069
  } finally {
12896
13070
  await (0, import_promises12.rm)(tmp, { force: true });
12897
13071
  }
@@ -13010,10 +13184,10 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
13010
13184
  }
13011
13185
  for (let i = 0; i < entry.outputs.length; i++) {
13012
13186
  const file = currentEntryOutputPath(entry, spec, i);
13013
- if (!(0, import_node_fs16.existsSync)(file)) continue;
13187
+ if (!(0, import_node_fs17.existsSync)(file)) continue;
13014
13188
  if (await sha256File(file) !== entry.outputs[i].sha256 && !opts.force) {
13015
13189
  throw new OverwriteRefusedError(
13016
- `${import_node_path20.default.relative(process.cwd(), file)} was changed after download; keep it with pixelkiln edit, or pass --force to replace it`,
13190
+ `${import_node_path21.default.relative(process.cwd(), file)} was changed after download; keep it with pixelkiln edit, or pass --force to replace it`,
13017
13191
  [file]
13018
13192
  );
13019
13193
  }
@@ -13065,11 +13239,11 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
13065
13239
 
13066
13240
  // src/pipeline/cache-health.ts
13067
13241
  async function inspectCaches(lock, lockPath, options = {}) {
13068
- if (options.prune && !(0, import_node_fs17.existsSync)(lockPath)) {
13069
- throw new Error(`Refusing to prune without an existing lockfile at ${import_node_path21.default.resolve(lockPath)}`);
13242
+ if (options.prune && !(0, import_node_fs18.existsSync)(lockPath)) {
13243
+ throw new Error(`Refusing to prune without an existing lockfile at ${import_node_path22.default.resolve(lockPath)}`);
13070
13244
  }
13071
- const contentDir = import_node_path21.default.resolve(import_node_path21.default.dirname(lockPath), ".pixelkiln", "cache");
13072
- const remotePath = import_node_path21.default.resolve(cachePathFor(lockPath));
13245
+ const contentDir = import_node_path22.default.resolve(import_node_path22.default.dirname(lockPath), ".pixelkiln", "cache");
13246
+ const remotePath = import_node_path22.default.resolve(cachePathFor(lockPath));
13073
13247
  const referenced = referencedHashes(lock);
13074
13248
  let content = await inspectContentCache(contentDir, referenced);
13075
13249
  let remoteHashes = await inspectRemoteHashCache(remotePath);
@@ -13081,7 +13255,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
13081
13255
  ]);
13082
13256
  for (const name of names) {
13083
13257
  try {
13084
- await (0, import_promises13.rm)(import_node_path21.default.join(contentDir, name), { force: true });
13258
+ await (0, import_promises13.rm)(import_node_path22.default.join(contentDir, name), { force: true });
13085
13259
  removed.contentFiles++;
13086
13260
  } catch {
13087
13261
  }
@@ -13108,7 +13282,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
13108
13282
  async function inspectContentCache(contentDir, referenced) {
13109
13283
  const report = {
13110
13284
  path: contentDir,
13111
- exists: (0, import_node_fs17.existsSync)(contentDir),
13285
+ exists: (0, import_node_fs18.existsSync)(contentDir),
13112
13286
  files: 0,
13113
13287
  bytes: 0,
13114
13288
  valid: 0,
@@ -13129,7 +13303,7 @@ async function inspectContentCache(contentDir, referenced) {
13129
13303
  continue;
13130
13304
  }
13131
13305
  report.files++;
13132
- const file = import_node_path21.default.join(contentDir, entry.name);
13306
+ const file = import_node_path22.default.join(contentDir, entry.name);
13133
13307
  const mediaType = mediaTypeFromExtension(entry.name);
13134
13308
  const expected = mediaType ? entry.name.slice(0, -4) : "";
13135
13309
  let bytes;
@@ -13172,7 +13346,7 @@ async function inspectContentCache(contentDir, referenced) {
13172
13346
  async function inspectRemoteHashCache(remotePath) {
13173
13347
  const report = {
13174
13348
  path: remotePath,
13175
- exists: (0, import_node_fs17.existsSync)(remotePath),
13349
+ exists: (0, import_node_fs18.existsSync)(remotePath),
13176
13350
  entries: 0,
13177
13351
  valid: 0,
13178
13352
  invalidIds: [],
@@ -13196,20 +13370,20 @@ async function inspectRemoteHashCache(remotePath) {
13196
13370
  }
13197
13371
 
13198
13372
  // src/pipeline/pack.ts
13199
- var import_node_fs18 = require("fs");
13200
- var import_node_path22 = __toESM(require("path"), 1);
13373
+ var import_node_fs19 = require("fs");
13374
+ var import_node_path23 = __toESM(require("path"), 1);
13201
13375
  var DEFAULT_FRAME_SET_FPS = 12;
13202
13376
  function resolvePackInputs(raw, inputsFilePath) {
13203
13377
  if (!Array.isArray(raw) || !raw.length) {
13204
13378
  throw new Error("--inputs must be a non-empty JSON array of { id, path }");
13205
13379
  }
13206
- const baseDir = import_node_path22.default.dirname(import_node_path22.default.resolve(inputsFilePath));
13380
+ const baseDir = import_node_path23.default.dirname(import_node_path23.default.resolve(inputsFilePath));
13207
13381
  return raw.map((entry, i) => {
13208
13382
  const e = entry;
13209
13383
  if (typeof e.id !== "string" || typeof e.path !== "string") {
13210
13384
  throw new Error(`--inputs[${i}] needs string "id" and "path"`);
13211
13385
  }
13212
- return { id: e.id, path: import_node_path22.default.resolve(baseDir, e.path) };
13386
+ return { id: e.id, path: import_node_path23.default.resolve(baseDir, e.path) };
13213
13387
  });
13214
13388
  }
13215
13389
  function packSprites(inputs, options = {}) {
@@ -13230,7 +13404,7 @@ function packSprites(inputs, options = {}) {
13230
13404
  for (const input of ordered) {
13231
13405
  let sourceHash = null;
13232
13406
  try {
13233
- const bytes = (0, import_node_fs18.readFileSync)(input.path);
13407
+ const bytes = (0, import_node_fs19.readFileSync)(input.path);
13234
13408
  sourceHash = sha256(bytes);
13235
13409
  const png = decodePng(bytes);
13236
13410
  sprites.push({ id: input.id, width: png.width, height: png.height, pixels: png.pixels });
@@ -13293,7 +13467,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
13293
13467
  }
13294
13468
  const inputs2 = Object.entries(options.sourceOverrides).map(([id, source]) => ({
13295
13469
  id,
13296
- path: import_node_path22.default.resolve(source)
13470
+ path: import_node_path23.default.resolve(source)
13297
13471
  }));
13298
13472
  if (!inputs2.length) throw new Error(`No approved quality outputs for style "${styleId}".`);
13299
13473
  const packed2 = packSprites(inputs2, options);
@@ -13317,7 +13491,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
13317
13491
  let outputs = resolveEntryOutputs(entry, id, manifestDir);
13318
13492
  const source = sources[id];
13319
13493
  if (source && outputs.length <= 1) {
13320
- inputs.push({ id, path: import_node_path22.default.resolve(manifestDir, source) });
13494
+ inputs.push({ id, path: import_node_path23.default.resolve(manifestDir, source) });
13321
13495
  continue;
13322
13496
  }
13323
13497
  if (source && sourceIsStem(source, entry, manifestDir)) {
@@ -13351,7 +13525,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
13351
13525
  }
13352
13526
  }
13353
13527
  for (const [id, source] of Object.entries(sources)) {
13354
- if (!locked.has(id)) inputs.push({ id, path: import_node_path22.default.resolve(manifestDir, source) });
13528
+ if (!locked.has(id)) inputs.push({ id, path: import_node_path23.default.resolve(manifestDir, source) });
13355
13529
  }
13356
13530
  if (!inputs.length) {
13357
13531
  throw new Error(`No readable sprites for style "${styleId}"; ${noOutput.length} skipped.`);
@@ -13388,7 +13562,7 @@ function mountSprites(placements, options) {
13388
13562
  for (const p of placements) {
13389
13563
  let sourceHash = null;
13390
13564
  try {
13391
- const bytes = (0, import_node_fs18.readFileSync)(p.path);
13565
+ const bytes = (0, import_node_fs19.readFileSync)(p.path);
13392
13566
  sourceHash = sha256(bytes);
13393
13567
  const png = decodePng(bytes);
13394
13568
  if (png.width > cellWidth || png.height > cellHeight) {
@@ -13459,7 +13633,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
13459
13633
  const source = sources[id];
13460
13634
  const entry = lock.entries[`${styleId}/${id}`];
13461
13635
  if (source && !(entry && sourceIsStem(source, entry, manifestDir))) {
13462
- placements.push({ id, path: import_node_path22.default.resolve(manifestDir, source), cell });
13636
+ placements.push({ id, path: import_node_path23.default.resolve(manifestDir, source), cell });
13463
13637
  continue;
13464
13638
  }
13465
13639
  if (source && entry) {
@@ -13492,7 +13666,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
13492
13666
  `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}`
13493
13667
  );
13494
13668
  }
13495
- const basePng = mount.base ? (0, import_node_fs18.readFileSync)(import_node_path22.default.resolve(manifestDir, mount.base)) : void 0;
13669
+ const basePng = mount.base ? (0, import_node_fs19.readFileSync)(import_node_path23.default.resolve(manifestDir, mount.base)) : void 0;
13496
13670
  const mounted = mountSprites(placements, {
13497
13671
  cellWidth: mount.cellWidth,
13498
13672
  cellHeight: mount.cellHeight,
@@ -13502,7 +13676,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
13502
13676
  ...mounted,
13503
13677
  sources: mount.base ? [{
13504
13678
  id: "$base",
13505
- path: import_node_path22.default.resolve(manifestDir, mount.base),
13679
+ path: import_node_path23.default.resolve(manifestDir, mount.base),
13506
13680
  sha256: sha256(basePng),
13507
13681
  included: true
13508
13682
  }, ...mounted.sources] : mounted.sources,
@@ -14079,6 +14253,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
14079
14253
  revision: spec.revision ? {
14080
14254
  mode: spec.revision.mode,
14081
14255
  sourceAssetId: spec.revision.sourceAssetId,
14256
+ ...spec.revision.sourceStyleId ? { sourceStyleId: spec.revision.sourceStyleId } : {},
14082
14257
  sourceSha256: spec.revision.sourceSha256,
14083
14258
  ...spec.revision.maskSha256 ? { maskSha256: spec.revision.maskSha256 } : {},
14084
14259
  ...spec.revision.strength == null ? {} : { strength: spec.revision.strength },
@@ -14312,9 +14487,9 @@ async function poll(provider, lock, lockPath, opts = {}) {
14312
14487
  }
14313
14488
 
14314
14489
  // src/pipeline/doctor.ts
14315
- var import_node_fs19 = require("fs");
14490
+ var import_node_fs20 = require("fs");
14316
14491
  var import_promises14 = require("fs/promises");
14317
- var import_node_path23 = __toESM(require("path"), 1);
14492
+ var import_node_path24 = __toESM(require("path"), 1);
14318
14493
  async function doctor(loaded, specs, lock, lockPath, opts = {}) {
14319
14494
  const checks = [];
14320
14495
  const add = (id, level, message6) => checks.push({ id, level, message: message6 });
@@ -14325,14 +14500,14 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
14325
14500
  );
14326
14501
  add("lock", "ok", `${Object.keys(lock.entries).length} valid v2 lock entr(ies)`);
14327
14502
  const dirs = [...new Set(specs.flatMap((spec) => [
14328
- import_node_path23.default.dirname(spec.outFile),
14329
- ...spec.quality ? [import_node_path23.default.dirname(spec.quality.outFile)] : []
14503
+ import_node_path24.default.dirname(spec.outFile),
14504
+ ...spec.quality ? [import_node_path24.default.dirname(spec.quality.outFile)] : []
14330
14505
  ]))];
14331
14506
  const unwritable = [];
14332
14507
  for (const dir of dirs) {
14333
14508
  const ancestor = await nearestExistingDirectory(dir);
14334
14509
  try {
14335
- await (0, import_promises14.access)(ancestor, import_node_fs19.constants.W_OK);
14510
+ await (0, import_promises14.access)(ancestor, import_node_fs20.constants.W_OK);
14336
14511
  } catch {
14337
14512
  unwritable.push(dir);
14338
14513
  }
@@ -14349,7 +14524,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
14349
14524
  for (let index = 0; index < entry.outputs.length; index++) {
14350
14525
  const output = entry.outputs[index];
14351
14526
  const spec = specByKey.get(key);
14352
- const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, import_node_path23.default.dirname(lockPath));
14527
+ const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, import_node_path24.default.dirname(lockPath));
14353
14528
  const owner = outputOwners.get(absolute);
14354
14529
  if (owner && owner !== key) duplicateOutputs.push(`${output.path} (${owner}, ${key})`);
14355
14530
  outputOwners.set(absolute, key);
@@ -14364,7 +14539,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
14364
14539
  } else {
14365
14540
  add("lock-outputs", "ok", `${outputOwners.size} recorded output path(s) are uniquely owned`);
14366
14541
  }
14367
- const cacheDir = import_node_path23.default.join(import_node_path23.default.dirname(lockPath), ".pixelkiln", "cache");
14542
+ const cacheDir = import_node_path24.default.join(import_node_path24.default.dirname(lockPath), ".pixelkiln", "cache");
14368
14543
  const stranded = [];
14369
14544
  const invalidState = [];
14370
14545
  const staleMapUrls = [];
@@ -14380,11 +14555,11 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
14380
14555
  const hasSource = Boolean(entry.sourceUrl || entry.sourceUrls?.length);
14381
14556
  let hasCache = false;
14382
14557
  for (const output of entry.outputs) {
14383
- const cached = import_node_path23.default.join(
14558
+ const cached = import_node_path24.default.join(
14384
14559
  cacheDir,
14385
14560
  cacheFileName(output.sha256, output.mediaType ?? MediaType.PNG)
14386
14561
  );
14387
- if ((0, import_node_fs19.existsSync)(cached) && await sha256File(cached) === output.sha256) {
14562
+ if ((0, import_node_fs20.existsSync)(cached) && await sha256File(cached) === output.sha256) {
14388
14563
  hasCache = true;
14389
14564
  break;
14390
14565
  }
@@ -14480,16 +14655,16 @@ async function nearestExistingDirectory(start) {
14480
14655
  if ((await (0, import_promises14.stat)(current)).isDirectory()) return current;
14481
14656
  } catch {
14482
14657
  }
14483
- const parent = import_node_path23.default.dirname(current);
14658
+ const parent = import_node_path24.default.dirname(current);
14484
14659
  if (parent === current) return current;
14485
14660
  current = parent;
14486
14661
  }
14487
14662
  }
14488
14663
 
14489
14664
  // src/pipeline/adopt.ts
14490
- var import_node_fs20 = require("fs");
14665
+ var import_node_fs21 = require("fs");
14491
14666
  var import_promises15 = require("fs/promises");
14492
- var import_node_path24 = __toESM(require("path"), 1);
14667
+ var import_node_path25 = __toESM(require("path"), 1);
14493
14668
  async function adopt(provider, specs, lock, lockPath, opts = {}) {
14494
14669
  const log = opts.onProgress ?? (() => {
14495
14670
  });
@@ -14500,7 +14675,7 @@ async function adopt(provider, specs, lock, lockPath, opts = {}) {
14500
14675
  const localByHash = /* @__PURE__ */ new Map();
14501
14676
  const unmatchedLocal = [];
14502
14677
  for (const spec of specs) {
14503
- if (!(0, import_node_fs20.existsSync)(spec.outFile)) {
14678
+ if (!(0, import_node_fs21.existsSync)(spec.outFile)) {
14504
14679
  unmatchedLocal.push(`${spec.styleId}/${spec.assetId} (no file at ${spec.outFile})`);
14505
14680
  continue;
14506
14681
  }
@@ -14622,13 +14797,13 @@ async function tagAdopted(provider, specs, lock, opts = {}) {
14622
14797
  }
14623
14798
 
14624
14799
  // src/pick/server.ts
14625
- var import_node_fs22 = require("fs");
14800
+ var import_node_fs23 = require("fs");
14626
14801
  var import_promises18 = require("fs/promises");
14627
14802
 
14628
14803
  // src/pipeline/init.ts
14629
14804
  var import_promises16 = require("fs/promises");
14630
- var import_node_fs21 = require("fs");
14631
- var import_node_path25 = __toESM(require("path"), 1);
14805
+ var import_node_fs22 = require("fs");
14806
+ var import_node_path26 = __toESM(require("path"), 1);
14632
14807
  function pngSize(buf) {
14633
14808
  if (buf.length < 24) return null;
14634
14809
  const isPng = buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834;
@@ -14642,7 +14817,7 @@ function slugify(value) {
14642
14817
  async function walk(dir, exclude) {
14643
14818
  const out = [];
14644
14819
  for (const entry of await (0, import_promises16.readdir)(dir, { withFileTypes: true })) {
14645
- const full = import_node_path25.default.join(dir, entry.name);
14820
+ const full = import_node_path26.default.join(dir, entry.name);
14646
14821
  if (exclude.some((x) => full.includes(x))) continue;
14647
14822
  if (entry.isDirectory()) out.push(...await walk(full, exclude));
14648
14823
  else if (entry.name.toLowerCase().endsWith(".png")) out.push(full);
@@ -14661,9 +14836,9 @@ async function scanAssets(root, opts = {}) {
14661
14836
  skipped.push(`${file} (not a readable PNG)`);
14662
14837
  continue;
14663
14838
  }
14664
- const rel = import_node_path25.default.relative(root, file);
14665
- const category = import_node_path25.default.dirname(rel) === "." ? "" : import_node_path25.default.dirname(rel);
14666
- let id = slugify(import_node_path25.default.basename(rel));
14839
+ const rel = import_node_path26.default.relative(root, file);
14840
+ const category = import_node_path26.default.dirname(rel) === "." ? "" : import_node_path26.default.dirname(rel);
14841
+ let id = slugify(import_node_path26.default.basename(rel));
14667
14842
  if (seen.has(id)) {
14668
14843
  const n = seen.get(id) + 1;
14669
14844
  seen.set(id, n);
@@ -15212,7 +15387,7 @@ async function currentArt(entry, spec) {
15212
15387
  const previous = primaryOutput({ ...entry, outputs: entry.supersededOutputs });
15213
15388
  if (!previous) return null;
15214
15389
  const file = resolveOutputPath(previous.path, spec.root);
15215
- if (!(0, import_node_fs22.existsSync)(file)) return null;
15390
+ if (!(0, import_node_fs23.existsSync)(file)) return null;
15216
15391
  const contentType = previous.mediaType ?? mediaTypeFromExtension(file);
15217
15392
  if (!contentType) return null;
15218
15393
  let width = entry.width;
@@ -15386,15 +15561,15 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
15386
15561
  }
15387
15562
 
15388
15563
  // src/gallery/snapshot.ts
15389
- var import_node_crypto6 = require("crypto");
15390
- var import_node_fs28 = require("fs");
15564
+ var import_node_crypto7 = require("crypto");
15565
+ var import_node_fs29 = require("fs");
15391
15566
  var import_promises26 = require("fs/promises");
15392
- var import_node_path33 = __toESM(require("path"), 1);
15567
+ var import_node_path34 = __toESM(require("path"), 1);
15393
15568
 
15394
15569
  // src/workspace.ts
15395
15570
  var import_promises19 = require("fs/promises");
15396
- var import_node_fs23 = require("fs");
15397
- var import_node_path26 = __toESM(require("path"), 1);
15571
+ var import_node_fs24 = require("fs");
15572
+ var import_node_path27 = __toESM(require("path"), 1);
15398
15573
  var import_zod7 = require("zod");
15399
15574
  var WorkspaceProjectSchema = import_zod7.z.object({
15400
15575
  id: import_zod7.z.string().min(1),
@@ -15419,7 +15594,7 @@ ${parsed.error.issues.slice(0, 5).map((i) => ` ${i.path.join(".")}: ${i.message
15419
15594
  );
15420
15595
  }
15421
15596
  async function loadWorkspace(workspacePath) {
15422
- if (!(0, import_node_fs23.existsSync)(workspacePath)) return { version: 1, projects: [] };
15597
+ if (!(0, import_node_fs24.existsSync)(workspacePath)) return { version: 1, projects: [] };
15423
15598
  let raw;
15424
15599
  try {
15425
15600
  raw = JSON.parse(await (0, import_promises19.readFile)(workspacePath, "utf8"));
@@ -15436,7 +15611,7 @@ async function saveWorkspace(workspacePath, ws) {
15436
15611
  version: 1,
15437
15612
  projects: [...ws.projects].sort((a, b) => a.id.localeCompare(b.id))
15438
15613
  };
15439
- await (0, import_promises19.mkdir)(import_node_path26.default.dirname(import_node_path26.default.resolve(workspacePath)), { recursive: true });
15614
+ await (0, import_promises19.mkdir)(import_node_path27.default.dirname(import_node_path27.default.resolve(workspacePath)), { recursive: true });
15440
15615
  const tmp = `${workspacePath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
15441
15616
  try {
15442
15617
  await (0, import_promises19.writeFile)(tmp, JSON.stringify(sorted, null, 2) + "\n");
@@ -15446,12 +15621,12 @@ async function saveWorkspace(workspacePath, ws) {
15446
15621
  }
15447
15622
  }
15448
15623
  function toPortablePath(dir, absolute) {
15449
- return import_node_path26.default.relative(dir, absolute).split(import_node_path26.default.sep).join("/");
15624
+ return import_node_path27.default.relative(dir, absolute).split(import_node_path27.default.sep).join("/");
15450
15625
  }
15451
15626
  function resolveProject(dir, project) {
15452
15627
  return {
15453
- manifestPath: import_node_path26.default.resolve(dir, project.manifest.split("/").join(import_node_path26.default.sep)),
15454
- lockPath: import_node_path26.default.resolve(dir, project.lock.split("/").join(import_node_path26.default.sep))
15628
+ manifestPath: import_node_path27.default.resolve(dir, project.manifest.split("/").join(import_node_path27.default.sep)),
15629
+ lockPath: import_node_path27.default.resolve(dir, project.lock.split("/").join(import_node_path27.default.sep))
15455
15630
  };
15456
15631
  }
15457
15632
  function validateWorkspace(ws, dir) {
@@ -15464,21 +15639,21 @@ function validateWorkspace(ws, dir) {
15464
15639
  const { manifestPath, lockPath } = resolveProject(dir, project);
15465
15640
  lockOwners.set(lockPath, [...lockOwners.get(lockPath) ?? [], project.id]);
15466
15641
  manifestOwners.set(manifestPath, [...manifestOwners.get(manifestPath) ?? [], project.id]);
15467
- if (import_node_path26.default.isAbsolute(project.manifest) || import_node_path26.default.isAbsolute(project.lock)) {
15642
+ if (import_node_path27.default.isAbsolute(project.manifest) || import_node_path27.default.isAbsolute(project.lock)) {
15468
15643
  diagnostics.push({
15469
15644
  id: "absolute-path",
15470
15645
  level: "warning",
15471
15646
  message: `project "${project.id}" stores an absolute path; the catalog will not resolve correctly if this tree is cloned or moved elsewhere`
15472
15647
  });
15473
15648
  }
15474
- if (!(0, import_node_fs23.existsSync)(manifestPath)) {
15649
+ if (!(0, import_node_fs24.existsSync)(manifestPath)) {
15475
15650
  diagnostics.push({
15476
15651
  id: "missing-manifest",
15477
15652
  level: "error",
15478
15653
  message: `project "${project.id}" manifest not found: ${manifestPath}`
15479
15654
  });
15480
15655
  }
15481
- if (!(0, import_node_fs23.existsSync)(lockPath)) {
15656
+ if (!(0, import_node_fs24.existsSync)(lockPath)) {
15482
15657
  diagnostics.push({
15483
15658
  id: "missing-lock",
15484
15659
  level: "error",
@@ -15526,15 +15701,15 @@ function validateWorkspace(ws, dir) {
15526
15701
 
15527
15702
  // src/gallery/edit.ts
15528
15703
  var import_promises25 = require("fs/promises");
15529
- var import_node_path32 = __toESM(require("path"), 1);
15704
+ var import_node_path33 = __toESM(require("path"), 1);
15530
15705
  var import_zod13 = require("zod");
15531
15706
 
15532
15707
  // src/manifest-edit.ts
15533
15708
  var import_promises20 = require("fs/promises");
15534
- var import_node_path27 = __toESM(require("path"), 1);
15709
+ var import_node_path28 = __toESM(require("path"), 1);
15535
15710
  var import_zod8 = require("zod");
15536
15711
  var HexSha = import_zod8.z.string().regex(/^[0-9a-f]{64}$/);
15537
- var ProjectFileSchema = import_zod8.z.string().min(1).refine((file) => !import_node_path27.default.isAbsolute(file) && !file.split(/[\\/]/).includes(".."), "must be a path inside the project");
15712
+ var ProjectFileSchema = import_zod8.z.string().min(1).refine((file) => !import_node_path28.default.isAbsolute(file) && !file.split(/[\\/]/).includes(".."), "must be a path inside the project");
15538
15713
  var AssetPatchSchema = import_zod8.z.object({
15539
15714
  prompt: import_zod8.z.string().optional(),
15540
15715
  /** Per-style override for the style being viewed; null removes it. */
@@ -15953,7 +16128,7 @@ function applyEdit(raw, edit) {
15953
16128
  }
15954
16129
  }
15955
16130
  async function withCandidate(manifestPath, edit, use, unchanged) {
15956
- const absolute = import_node_path27.default.resolve(manifestPath);
16131
+ const absolute = import_node_path28.default.resolve(manifestPath);
15957
16132
  const original = await (0, import_promises20.readFile)(absolute, "utf8");
15958
16133
  if (sha256(original) !== edit.expectedSha256) throw new ManifestDriftError(absolute);
15959
16134
  let raw;
@@ -15967,9 +16142,9 @@ async function withCandidate(manifestPath, edit, use, unchanged) {
15967
16142
  applyEdit(raw, edit);
15968
16143
  const next = serializeLike(original, raw);
15969
16144
  if (next === original) return unchanged(absolute, original);
15970
- const tmp = import_node_path27.default.join(
15971
- import_node_path27.default.dirname(absolute),
15972
- `.${import_node_path27.default.basename(absolute)}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`
16145
+ const tmp = import_node_path28.default.join(
16146
+ import_node_path28.default.dirname(absolute),
16147
+ `.${import_node_path28.default.basename(absolute)}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`
15973
16148
  );
15974
16149
  try {
15975
16150
  await (0, import_promises20.writeFile)(tmp, next);
@@ -15998,18 +16173,18 @@ async function applyManifestEdit(manifestPath, edit) {
15998
16173
  }
15999
16174
 
16000
16175
  // src/pipeline/hand-edit.ts
16001
- var import_node_fs24 = require("fs");
16176
+ var import_node_fs25 = require("fs");
16002
16177
  var import_promises21 = require("fs/promises");
16003
- var import_node_path28 = __toESM(require("path"), 1);
16178
+ var import_node_path29 = __toESM(require("path"), 1);
16004
16179
  var import_zod9 = require("zod");
16005
16180
  var HAND_EDIT_DIR = "edits";
16006
16181
  function handEditPath(loaded, spec) {
16007
16182
  const style = loaded.manifest.styles[spec.styleId];
16008
- const outDir = import_node_path28.default.resolve(loaded.root, style?.outDir ?? ".");
16009
- const relative = import_node_path28.default.relative(outDir, spec.outFile);
16010
- const inside = relative && !relative.startsWith("..") && !import_node_path28.default.isAbsolute(relative);
16011
- const file = inside ? relative : import_node_path28.default.basename(spec.outFile);
16012
- return import_node_path28.default.join(outDir, HAND_EDIT_DIR, file.replace(/\.gif$/i, ".png"));
16183
+ const outDir = import_node_path29.default.resolve(loaded.root, style?.outDir ?? ".");
16184
+ const relative = import_node_path29.default.relative(outDir, spec.outFile);
16185
+ const inside = relative && !relative.startsWith("..") && !import_node_path29.default.isAbsolute(relative);
16186
+ const file = inside ? relative : import_node_path29.default.basename(spec.outFile);
16187
+ return import_node_path29.default.join(outDir, HAND_EDIT_DIR, file.replace(/\.gif$/i, ".png"));
16013
16188
  }
16014
16189
  function handEditBases(spec, lock) {
16015
16190
  const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
@@ -16022,10 +16197,10 @@ function handEditBases(spec, lock) {
16022
16197
  }));
16023
16198
  if (members.some((member) => member.mediaType && member.mediaType !== "image/png")) return null;
16024
16199
  if (entry.outputs.length === 1 && !primaryOutput(entry)) return null;
16025
- if (!members.every((member) => (0, import_node_fs24.existsSync)(member.path))) return null;
16200
+ if (!members.every((member) => (0, import_node_fs25.existsSync)(member.path))) return null;
16026
16201
  return members.map(({ role, path: file }) => ({ role, path: file }));
16027
16202
  }
16028
- return (0, import_node_fs24.existsSync)(spec.outFile) ? [{ role: null, path: spec.outFile }] : null;
16203
+ return (0, import_node_fs25.existsSync)(spec.outFile) ? [{ role: null, path: spec.outFile }] : null;
16029
16204
  }
16030
16205
  function handEditBase(spec, lock) {
16031
16206
  const bases = handEditBases(spec, lock);
@@ -16055,18 +16230,18 @@ async function startHandEdit(loaded, lock, spec, opts = {}) {
16055
16230
  `${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`
16056
16231
  );
16057
16232
  }
16058
- const existing = spec.source ? import_node_path28.default.resolve(loaded.root, spec.source) : null;
16233
+ const existing = spec.source ? import_node_path29.default.resolve(loaded.root, spec.source) : null;
16059
16234
  const editPath = existing ?? handEditPath(loaded, spec);
16060
16235
  const members = handEditMembers(loaded, spec, lock, editPath);
16061
16236
  let created = false;
16062
16237
  for (const member of members) {
16063
- if ((0, import_node_fs24.existsSync)(member.path)) continue;
16238
+ if ((0, import_node_fs25.existsSync)(member.path)) continue;
16064
16239
  if (!member.base) {
16065
16240
  throw new ManifestEditError(
16066
16241
  `${spec.styleId}/${spec.assetId} has no generated PNG on disk to start from; generate or restore it first`
16067
16242
  );
16068
16243
  }
16069
- await (0, import_promises21.mkdir)(import_node_path28.default.dirname(member.path), { recursive: true });
16244
+ await (0, import_promises21.mkdir)(import_node_path29.default.dirname(member.path), { recursive: true });
16070
16245
  await (0, import_promises21.copyFile)(member.base, member.path);
16071
16246
  created = true;
16072
16247
  }
@@ -16204,7 +16379,7 @@ async function saveHandEdit(loaded, lock, spec, input) {
16204
16379
  editor: input.editor,
16205
16380
  protocol: input.protocol,
16206
16381
  savedAt: (input.now ?? /* @__PURE__ */ new Date()).toISOString(),
16207
- project: projectPath ? import_node_path28.default.basename(projectPath) : null,
16382
+ project: projectPath ? import_node_path29.default.basename(projectPath) : null,
16208
16383
  outputs: incoming.map((frame, index) => ({
16209
16384
  role: frame.role,
16210
16385
  basedOn: basedOnHash(entry, index),
@@ -16246,9 +16421,9 @@ function openInEditor(file, editor = process.env.PIXELKILN_EDITOR) {
16246
16421
  }
16247
16422
 
16248
16423
  // src/gallery/upload.ts
16249
- var import_node_fs25 = require("fs");
16424
+ var import_node_fs26 = require("fs");
16250
16425
  var import_promises22 = require("fs/promises");
16251
- var import_node_path29 = __toESM(require("path"), 1);
16426
+ var import_node_path30 = __toESM(require("path"), 1);
16252
16427
  var import_zod10 = require("zod");
16253
16428
  var MAX_UPLOAD_BYTES = 8 * 1024 * 1024;
16254
16429
  var UploadImageSchema = import_zod10.z.object({
@@ -16261,12 +16436,12 @@ var UploadImageSchema = import_zod10.z.object({
16261
16436
  overwrite: import_zod10.z.boolean().optional()
16262
16437
  }).strict();
16263
16438
  async function saveProjectImage(root, request) {
16264
- const file = import_node_path29.default.resolve(root, request.path);
16265
- const relative = import_node_path29.default.relative(root, file);
16266
- if (relative.startsWith("..") || import_node_path29.default.isAbsolute(relative)) {
16439
+ const file = import_node_path30.default.resolve(root, request.path);
16440
+ const relative = import_node_path30.default.relative(root, file);
16441
+ if (relative.startsWith("..") || import_node_path30.default.isAbsolute(relative)) {
16267
16442
  throw new ManifestEditError("an upload must stay inside the project");
16268
16443
  }
16269
- const extension = import_node_path29.default.extname(file).toLowerCase();
16444
+ const extension = import_node_path30.default.extname(file).toLowerCase();
16270
16445
  const expected = extension === ".png" ? "png" : extension === ".jpg" || extension === ".jpeg" ? "jpeg" : null;
16271
16446
  if (!expected) throw new ManifestEditError("an upload must be a .png, .jpg, or .jpeg path");
16272
16447
  const bytes = Buffer.from(request.base64, "base64");
@@ -16278,18 +16453,18 @@ async function saveProjectImage(root, request) {
16278
16453
  if (metadata.format !== expected) {
16279
16454
  throw new ManifestEditError(`the upload is a ${metadata.format.toUpperCase()}; name it ${metadata.format === "png" ? ".png" : ".jpg"}`);
16280
16455
  }
16281
- if ((0, import_node_fs25.existsSync)(file) && !request.overwrite) {
16282
- throw new ManifestEditError(`${relative.split(import_node_path29.default.sep).join("/")} already exists; choose another name, or replace it`);
16456
+ if ((0, import_node_fs26.existsSync)(file) && !request.overwrite) {
16457
+ throw new ManifestEditError(`${relative.split(import_node_path30.default.sep).join("/")} already exists; choose another name, or replace it`);
16283
16458
  }
16284
- await (0, import_promises22.mkdir)(import_node_path29.default.dirname(file), { recursive: true });
16459
+ await (0, import_promises22.mkdir)(import_node_path30.default.dirname(file), { recursive: true });
16285
16460
  await (0, import_promises22.writeFile)(file, bytes);
16286
- return relative.split(import_node_path29.default.sep).join("/");
16461
+ return relative.split(import_node_path30.default.sep).join("/");
16287
16462
  }
16288
16463
 
16289
16464
  // src/gallery/image-revision.ts
16290
- var import_node_fs26 = require("fs");
16465
+ var import_node_fs27 = require("fs");
16291
16466
  var import_promises23 = require("fs/promises");
16292
- var import_node_path30 = __toESM(require("path"), 1);
16467
+ var import_node_path31 = __toESM(require("path"), 1);
16293
16468
  var import_zod11 = require("zod");
16294
16469
  var InpaintRevisionRequestSchema = import_zod11.z.object({
16295
16470
  action: import_zod11.z.literal("create-inpaint-revision"),
@@ -16308,15 +16483,15 @@ var InpaintRevisionRequestSchema = import_zod11.z.object({
16308
16483
  overwrite: import_zod11.z.boolean().optional()
16309
16484
  }).strict();
16310
16485
  async function createInpaintRevision(manifestPath, request) {
16311
- const root = import_node_path30.default.dirname(import_node_path30.default.resolve(manifestPath));
16312
- if (import_node_path30.default.extname(request.maskPath).toLowerCase() !== ".png") {
16486
+ const root = import_node_path31.default.dirname(import_node_path31.default.resolve(manifestPath));
16487
+ if (import_node_path31.default.extname(request.maskPath).toLowerCase() !== ".png") {
16313
16488
  throw new ManifestEditError("the mask must be a .png path inside the project");
16314
16489
  }
16315
16490
  const bytes = Buffer.from(request.maskBase64, "base64");
16316
16491
  const metadata = imageMetadata(bytes);
16317
16492
  if (!metadata || metadata.format !== "png") throw new ManifestEditError("the mask is not a readable PNG");
16318
- const file = import_node_path30.default.resolve(root, request.maskPath);
16319
- const previous = (0, import_node_fs26.existsSync)(file) ? await (0, import_promises23.readFile)(file) : null;
16493
+ const file = import_node_path31.default.resolve(root, request.maskPath);
16494
+ const previous = (0, import_node_fs27.existsSync)(file) ? await (0, import_promises23.readFile)(file) : null;
16320
16495
  const mask = await saveProjectImage(root, {
16321
16496
  action: "upload-image",
16322
16497
  path: request.maskPath,
@@ -16343,9 +16518,9 @@ async function createInpaintRevision(manifestPath, request) {
16343
16518
  }
16344
16519
 
16345
16520
  // src/gallery/skeleton.ts
16346
- var import_node_fs27 = require("fs");
16521
+ var import_node_fs28 = require("fs");
16347
16522
  var import_promises24 = require("fs/promises");
16348
- var import_node_path31 = __toESM(require("path"), 1);
16523
+ var import_node_path32 = __toESM(require("path"), 1);
16349
16524
  var import_zod12 = require("zod");
16350
16525
  var SkeletonEstimateRequestSchema = import_zod12.z.object({
16351
16526
  project: import_zod12.z.string().min(1).optional(),
@@ -16383,20 +16558,20 @@ var SkeletonKeypointsUpdateSchema = import_zod12.z.object({
16383
16558
  set: SkeletonSetSchema
16384
16559
  }).strict();
16385
16560
  async function createSkeletonAnimation(manifestPath, request) {
16386
- const root = import_node_path31.default.dirname(import_node_path31.default.resolve(manifestPath));
16387
- const file = import_node_path31.default.resolve(root, request.keypointsFile);
16388
- const relative = import_node_path31.default.relative(root, file);
16389
- if (!request.keypointsFile.endsWith(".json") || relative.startsWith("..") || import_node_path31.default.isAbsolute(relative)) {
16561
+ const root = import_node_path32.default.dirname(import_node_path32.default.resolve(manifestPath));
16562
+ const file = import_node_path32.default.resolve(root, request.keypointsFile);
16563
+ const relative = import_node_path32.default.relative(root, file);
16564
+ if (!request.keypointsFile.endsWith(".json") || relative.startsWith("..") || import_node_path32.default.isAbsolute(relative)) {
16390
16565
  throw new ManifestEditError("the keypoints file must be a .json path inside the project");
16391
16566
  }
16392
- const existed = (0, import_node_fs27.existsSync)(file);
16567
+ const existed = (0, import_node_fs28.existsSync)(file);
16393
16568
  let previous = null;
16394
16569
  if (request.set) {
16395
16570
  if (existed && !request.overwrite) {
16396
16571
  throw new ManifestEditError(`${request.keypointsFile} already exists; choose another path, or replace it`);
16397
16572
  }
16398
16573
  if (existed) previous = await (0, import_promises24.readFile)(file);
16399
- await (0, import_promises24.mkdir)(import_node_path31.default.dirname(file), { recursive: true });
16574
+ await (0, import_promises24.mkdir)(import_node_path32.default.dirname(file), { recursive: true });
16400
16575
  await (0, import_promises24.writeFile)(file, JSON.stringify(request.set, null, 2) + "\n");
16401
16576
  } else if (!existed) {
16402
16577
  throw new ManifestEditError(`${request.keypointsFile} does not exist; paste poses or estimate them from the sprite`);
@@ -16412,7 +16587,7 @@ async function createSkeletonAnimation(manifestPath, request) {
16412
16587
  revision: {
16413
16588
  mode: "animate-skeleton",
16414
16589
  from: request.from,
16415
- keypointsFile: relative.split(import_node_path31.default.sep).join("/"),
16590
+ keypointsFile: relative.split(import_node_path32.default.sep).join("/"),
16416
16591
  direction: request.direction,
16417
16592
  ...request.description ? { description: request.description } : {},
16418
16593
  ...request.skeletonTemplate ? { skeletonTemplate: request.skeletonTemplate } : {}
@@ -16433,19 +16608,19 @@ async function updateSkeletonKeypoints(ctx, request) {
16433
16608
  if (!spec) throw new ManifestEditError(`"${key}" is not declared by the manifest`);
16434
16609
  const file = spec.revision?.mode === "animate-skeleton" ? spec.revision.keypointsFile : void 0;
16435
16610
  if (!file) throw new ManifestEditError(`${key} is not an animate-skeleton revision`);
16436
- const relative = import_node_path31.default.relative(ctx.loaded.root, file);
16437
- if (relative.startsWith("..") || import_node_path31.default.isAbsolute(relative)) {
16611
+ const relative = import_node_path32.default.relative(ctx.loaded.root, file);
16612
+ if (relative.startsWith("..") || import_node_path32.default.isAbsolute(relative)) {
16438
16613
  throw new ManifestEditError(`${key}'s keypoints file is outside the project; edit it by hand`);
16439
16614
  }
16440
- const current = (0, import_node_fs27.existsSync)(file) ? sha256(await (0, import_promises24.readFile)(file)) : null;
16615
+ const current = (0, import_node_fs28.existsSync)(file) ? sha256(await (0, import_promises24.readFile)(file)) : null;
16441
16616
  if (current !== request.expectedSha256) {
16442
16617
  throw Object.assign(
16443
- new Error(`${relative.split(import_node_path31.default.sep).join("/")} changed on disk since this page loaded it; press Refresh and edit again`),
16618
+ new Error(`${relative.split(import_node_path32.default.sep).join("/")} changed on disk since this page loaded it; press Refresh and edit again`),
16444
16619
  { status: 409 }
16445
16620
  );
16446
16621
  }
16447
16622
  await (0, import_promises24.writeFile)(file, JSON.stringify(request.set, null, 2) + "\n");
16448
- return relative.split(import_node_path31.default.sep).join("/");
16623
+ return relative.split(import_node_path32.default.sep).join("/");
16449
16624
  }
16450
16625
 
16451
16626
  // src/gallery/edit.ts
@@ -16499,7 +16674,7 @@ function createGalleryEditHandler(opts) {
16499
16674
  const history = /* @__PURE__ */ new Map();
16500
16675
  const manifestOf = async (project) => {
16501
16676
  try {
16502
- return import_node_path32.default.resolve(await opts.manifestFor(typeof project === "string" ? project : void 0));
16677
+ return import_node_path33.default.resolve(await opts.manifestFor(typeof project === "string" ? project : void 0));
16503
16678
  } catch {
16504
16679
  return null;
16505
16680
  }
@@ -16522,7 +16697,7 @@ function createGalleryEditHandler(opts) {
16522
16697
  await (0, import_promises25.writeFile)(tmp, last.before);
16523
16698
  await (0, import_promises25.rename)(tmp, manifestPath2);
16524
16699
  stack.pop();
16525
- log(` manifest edit undone in ${import_node_path32.default.relative(process.cwd(), manifestPath2) || manifestPath2}`);
16700
+ log(` manifest edit undone in ${import_node_path33.default.relative(process.cwd(), manifestPath2) || manifestPath2}`);
16526
16701
  return opts.reload();
16527
16702
  }
16528
16703
  const manifestPath = await manifestOf(body?.project);
@@ -16584,7 +16759,7 @@ function createEditApplier(opts, log) {
16584
16759
  } catch (error) {
16585
16760
  throw new ManifestEditError(error instanceof Error ? error.message : String(error));
16586
16761
  }
16587
- const written = await saveProjectImage(import_node_path32.default.dirname(import_node_path32.default.resolve(manifestPath2)), upload.data);
16762
+ const written = await saveProjectImage(import_node_path33.default.dirname(import_node_path33.default.resolve(manifestPath2)), upload.data);
16588
16763
  log(` image added: ${written}`);
16589
16764
  return opts.reload();
16590
16765
  }
@@ -16651,7 +16826,7 @@ function createEditApplier(opts, log) {
16651
16826
  }
16652
16827
  const result = await applyManifestEdit(manifestPath, parsed.data);
16653
16828
  if (result.changed) {
16654
- const relative = import_node_path32.default.relative(process.cwd(), result.manifestPath);
16829
+ const relative = import_node_path33.default.relative(process.cwd(), result.manifestPath);
16655
16830
  const shown = !relative || relative.startsWith("..") ? result.manifestPath : relative;
16656
16831
  log(` manifest edited: ${describeEdit(parsed.data)} in ${shown}`);
16657
16832
  }
@@ -16679,7 +16854,7 @@ function describeEdit(edit) {
16679
16854
  // src/gallery/snapshot.ts
16680
16855
  var PROJECT_FILE_TYPE = "application/octet-stream";
16681
16856
  function galleryMediaId(absolutePath) {
16682
- return (0, import_node_crypto6.createHash)("sha256").update(import_node_path33.default.resolve(absolutePath)).digest("hex").slice(0, 24);
16857
+ return (0, import_node_crypto7.createHash)("sha256").update(import_node_path34.default.resolve(absolutePath)).digest("hex").slice(0, 24);
16683
16858
  }
16684
16859
  function galleryMediaRoute(id) {
16685
16860
  return `/media/${id}`;
@@ -16698,9 +16873,9 @@ function describeHistory(media, entry, cacheDir) {
16698
16873
  return (entry.history ?? []).map((generation, i) => {
16699
16874
  const outputs = generation.outputs.map((output) => {
16700
16875
  const mediaType = output.mediaType ?? MediaType.PNG;
16701
- const file = import_node_path33.default.join(cacheDir, cacheFileName(output.sha256, mediaType));
16876
+ const file = import_node_path34.default.join(cacheDir, cacheFileName(output.sha256, mediaType));
16702
16877
  let url = null;
16703
- if ((0, import_node_fs28.existsSync)(file)) {
16878
+ if ((0, import_node_fs29.existsSync)(file)) {
16704
16879
  const id = galleryMediaId(file);
16705
16880
  media.set(id, { path: file, contentType: mediaType });
16706
16881
  url = `${galleryMediaRoute(id)}?v=${output.sha256.slice(0, 24)}`;
@@ -16830,10 +17005,10 @@ async function describeOutput(media, root, absolutePath, recorded = {}) {
16830
17005
  async function describeQuality(media, root, inspection) {
16831
17006
  const outputPaths = inspection.outputs ?? [inspection.output];
16832
17007
  const outputs = await Promise.all(
16833
- outputPaths.map((file) => describeOutput(media, root, import_node_path33.default.resolve(root, file)))
17008
+ outputPaths.map((file) => describeOutput(media, root, import_node_path34.default.resolve(root, file)))
16834
17009
  );
16835
- const recordPath = import_node_path33.default.resolve(root, inspection.record);
16836
- const recordExists = (0, import_node_fs28.existsSync)(recordPath);
17010
+ const recordPath = import_node_path34.default.resolve(root, inspection.record);
17011
+ const recordExists = (0, import_node_fs29.existsSync)(recordPath);
16837
17012
  let options = null;
16838
17013
  let check = null;
16839
17014
  if (recordExists) {
@@ -16852,7 +17027,7 @@ async function describeQuality(media, root, inspection) {
16852
17027
  return {
16853
17028
  state: inspection.state,
16854
17029
  reason: inspection.reason,
16855
- output: portableOutputPath(import_node_path33.default.resolve(root, inspection.output), root),
17030
+ output: portableOutputPath(import_node_path34.default.resolve(root, inspection.output), root),
16856
17031
  record: portableOutputPath(recordPath, root),
16857
17032
  recordExists,
16858
17033
  outputs,
@@ -16892,7 +17067,7 @@ async function buildGallerySnapshot(opts) {
16892
17067
  const items = [];
16893
17068
  const manifestText = await (0, import_promises26.readFile)(loaded.path, "utf8");
16894
17069
  const manifestSha256 = sha256(manifestText);
16895
- const cacheDir = import_node_path33.default.resolve(import_node_path33.default.dirname(opts.lockPath), ".pixelkiln", "cache");
17070
+ const cacheDir = import_node_path34.default.resolve(import_node_path34.default.dirname(opts.lockPath), ".pixelkiln", "cache");
16896
17071
  let rawStyles = {};
16897
17072
  try {
16898
17073
  const raw = JSON.parse(manifestText);
@@ -16916,12 +17091,12 @@ async function buildGallerySnapshot(opts) {
16916
17091
  )
16917
17092
  );
16918
17093
  if (spec.source) {
16919
- const editPath = import_node_path33.default.resolve(root, spec.source);
17094
+ const editPath = import_node_path34.default.resolve(root, spec.source);
16920
17095
  const memberCount = sourceIsStem(spec.source, entry, root) ? entry.outputs.length : 1;
16921
17096
  const editShas = await Promise.all(
16922
17097
  Array.from({ length: memberCount }, async (_, index) => {
16923
17098
  const file = sourceOutputPath(spec.source, entry, index, root);
16924
- return { file, sha256: (0, import_node_fs28.existsSync)(file) ? await sha256File(file) : null };
17099
+ return { file, sha256: (0, import_node_fs29.existsSync)(file) ? await sha256File(file) : null };
16925
17100
  })
16926
17101
  );
16927
17102
  edits = await Promise.all(editShas.map(({ file, sha256: editSha }) => describeOutput(media, root, file, { sha256: editSha })));
@@ -16929,7 +17104,7 @@ async function buildGallerySnapshot(opts) {
16929
17104
  const companion = await readHandEditCompanion(editPath);
16930
17105
  if (companion) {
16931
17106
  const projectPath = handEditProjectPath(editPath);
16932
- const project = companion.project && (0, import_node_fs28.existsSync)(projectPath) ? await fileInfo(projectPath) : null;
17107
+ const project = companion.project && (0, import_node_fs29.existsSync)(projectPath) ? await fileInfo(projectPath) : null;
16933
17108
  let projectUrl = null;
16934
17109
  if (project) {
16935
17110
  const id = galleryMediaId(projectPath);
@@ -16965,8 +17140,8 @@ async function buildGallerySnapshot(opts) {
16965
17140
  editStatus = edits.some((member) => !member.exists) ? "missing" : editChanged.every((changed) => !changed) ? "same" : regenerated ? "regenerated-since" : "edited";
16966
17141
  }
16967
17142
  } else if (spec.source) {
16968
- outputs = [await describeOutput(media, root, import_node_path33.default.resolve(root, spec.source))];
16969
- } else if (!entry && (0, import_node_fs28.existsSync)(spec.outFile)) {
17143
+ outputs = [await describeOutput(media, root, import_node_path34.default.resolve(root, spec.source))];
17144
+ } else if (!entry && (0, import_node_fs29.existsSync)(spec.outFile)) {
16970
17145
  outputs = [await describeOutput(media, root, spec.outFile)];
16971
17146
  } else {
16972
17147
  outputs = [];
@@ -17006,7 +17181,7 @@ async function buildGallerySnapshot(opts) {
17006
17181
  recordedSpecHash: entry?.specHash ?? null,
17007
17182
  currentSpecHash: spec.specHash,
17008
17183
  revision: entry?.revision ?? null,
17009
- revisionParentKey: spec.revision ? lockKey(spec.styleId, spec.revision.sourceAssetId) : entry?.revision ? lockKey(spec.styleId, entry.revision.sourceAssetId) : null,
17184
+ revisionParentKey: spec.revision ? lockKey(spec.revision.sourceStyleId ?? spec.styleId, spec.revision.sourceAssetId) : entry?.revision ? lockKey(entry.revision.sourceStyleId ?? spec.styleId, entry.revision.sourceAssetId) : null,
17010
17185
  mirrorOfKey: spec.mirror ? lockKey(spec.styleId, spec.mirror.sourceAssetId) : entry?.mirror ? lockKey(spec.styleId, entry.mirror.sourceAssetId) : null,
17011
17186
  skeleton: spec.revision?.mode === "animate-skeleton" && spec.revision.keypointsFile ? {
17012
17187
  keypointsFile: portableOutputPath(spec.revision.keypointsFile, root),
@@ -17076,7 +17251,7 @@ async function buildGallerySnapshot(opts) {
17076
17251
  recordedSpecHash: entry.specHash,
17077
17252
  currentSpecHash: null,
17078
17253
  revision: entry.revision,
17079
- revisionParentKey: entry.revision ? lockKey(entry.styleId, entry.revision.sourceAssetId) : null,
17254
+ revisionParentKey: entry.revision ? lockKey(entry.revision.sourceStyleId ?? entry.styleId, entry.revision.sourceAssetId) : null,
17080
17255
  mirrorOfKey: entry.mirror ? lockKey(entry.styleId, entry.mirror.sourceAssetId) : null,
17081
17256
  skeleton: null,
17082
17257
  character: describeCharacter(void 0, entry),
@@ -17169,7 +17344,7 @@ async function buildGallerySnapshot(opts) {
17169
17344
  id: loaded.manifest.name,
17170
17345
  name: loaded.manifest.name,
17171
17346
  manifest: loaded.path,
17172
- lock: import_node_path33.default.resolve(opts.lockPath),
17347
+ lock: import_node_path34.default.resolve(opts.lockPath),
17173
17348
  root,
17174
17349
  provider: loaded.manifest.provider,
17175
17350
  account: null,
@@ -17195,7 +17370,7 @@ async function buildGallerySnapshot(opts) {
17195
17370
  return { snapshot, media };
17196
17371
  }
17197
17372
  async function buildWorkspaceGallerySnapshot(opts) {
17198
- const dir = import_node_path33.default.dirname(import_node_path33.default.resolve(opts.workspacePath));
17373
+ const dir = import_node_path34.default.dirname(import_node_path34.default.resolve(opts.workspacePath));
17199
17374
  const media = /* @__PURE__ */ new Map();
17200
17375
  const projects = [];
17201
17376
  const items = [];
@@ -17238,7 +17413,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
17238
17413
  id: project.id,
17239
17414
  name: loaded.manifest.name,
17240
17415
  manifest: manifestPath,
17241
- lock: import_node_path33.default.resolve(lockPath),
17416
+ lock: import_node_path34.default.resolve(lockPath),
17242
17417
  root: loaded.root,
17243
17418
  provider: loaded.manifest.provider,
17244
17419
  account: project.account ?? null,
@@ -17254,8 +17429,8 @@ async function buildWorkspaceGallerySnapshot(opts) {
17254
17429
  id: project.id,
17255
17430
  name: project.id,
17256
17431
  manifest: manifestPath,
17257
- lock: import_node_path33.default.resolve(lockPath),
17258
- root: import_node_path33.default.dirname(manifestPath),
17432
+ lock: import_node_path34.default.resolve(lockPath),
17433
+ root: import_node_path34.default.dirname(manifestPath),
17259
17434
  provider: project.provider,
17260
17435
  account: project.account ?? null,
17261
17436
  manifestSha256: null,
@@ -17282,7 +17457,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
17282
17457
  version: 1,
17283
17458
  generatedAt: (opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
17284
17459
  project: null,
17285
- workspace: { path: import_node_path33.default.resolve(opts.workspacePath), dir, projects },
17460
+ workspace: { path: import_node_path34.default.resolve(opts.workspacePath), dir, projects },
17286
17461
  filter,
17287
17462
  totals: {
17288
17463
  entries: projects.reduce((sum, project) => sum + project.entries, 0),
@@ -17299,7 +17474,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
17299
17474
  }
17300
17475
 
17301
17476
  // src/gallery/page.ts
17302
- var import_node_fs29 = require("fs");
17477
+ var import_node_fs30 = require("fs");
17303
17478
 
17304
17479
  // src/gallery/client-bundle.ts
17305
17480
  var import_node_module = require("module");
@@ -17339,8 +17514,8 @@ var client;
17339
17514
  function clientAssets() {
17340
17515
  const prebuilt = new URL("gallery.js", CLIENT_DIR);
17341
17516
  client ??= {
17342
- css: (0, import_node_fs29.readFileSync)(new URL("gallery.css", CLIENT_DIR), "utf8"),
17343
- js: (0, import_node_fs29.existsSync)(prebuilt) ? (0, import_node_fs29.readFileSync)(prebuilt, "utf8") : bundleGalleryClient()
17517
+ css: (0, import_node_fs30.readFileSync)(new URL("gallery.css", CLIENT_DIR), "utf8"),
17518
+ js: (0, import_node_fs30.existsSync)(prebuilt) ? (0, import_node_fs30.readFileSync)(prebuilt, "utf8") : bundleGalleryClient()
17344
17519
  };
17345
17520
  return client;
17346
17521
  }
@@ -17482,11 +17657,11 @@ var EDITOR_PIN = pin_default;
17482
17657
  var EDITOR_RELEASE_BASE = "https://github.com/gfargo/pixelkiln/releases/download";
17483
17658
 
17484
17659
  // src/editor/install.ts
17485
- var import_node_crypto7 = require("crypto");
17486
- var import_node_fs30 = require("fs");
17660
+ var import_node_crypto8 = require("crypto");
17661
+ var import_node_fs31 = require("fs");
17487
17662
  var import_promises27 = require("fs/promises");
17488
- var import_node_os3 = __toESM(require("os"), 1);
17489
- var import_node_path34 = __toESM(require("path"), 1);
17663
+ var import_node_os4 = __toESM(require("os"), 1);
17664
+ var import_node_path35 = __toESM(require("path"), 1);
17490
17665
  var EditorInstallError = class extends Error {
17491
17666
  constructor(message6, file) {
17492
17667
  super(message6);
@@ -17497,15 +17672,15 @@ var EditorInstallError = class extends Error {
17497
17672
  };
17498
17673
  function editorToolsRoot() {
17499
17674
  const override = process.env.PIXELKILN_TOOLS_DIR?.trim();
17500
- if (override) return import_node_path34.default.resolve(override);
17501
- if (process.platform === "darwin") return import_node_path34.default.join(import_node_os3.default.homedir(), "Library", "Caches", "pixelkiln", "tools");
17675
+ if (override) return import_node_path35.default.resolve(override);
17676
+ if (process.platform === "darwin") return import_node_path35.default.join(import_node_os4.default.homedir(), "Library", "Caches", "pixelkiln", "tools");
17502
17677
  if (process.platform === "win32") {
17503
- return import_node_path34.default.join(process.env.LOCALAPPDATA ?? import_node_path34.default.join(import_node_os3.default.homedir(), "AppData", "Local"), "pixelkiln", "tools");
17678
+ return import_node_path35.default.join(process.env.LOCALAPPDATA ?? import_node_path35.default.join(import_node_os4.default.homedir(), "AppData", "Local"), "pixelkiln", "tools");
17504
17679
  }
17505
- return import_node_path34.default.join(process.env.XDG_CACHE_HOME ?? import_node_path34.default.join(import_node_os3.default.homedir(), ".cache"), "pixelkiln", "tools");
17680
+ return import_node_path35.default.join(process.env.XDG_CACHE_HOME ?? import_node_path35.default.join(import_node_os4.default.homedir(), ".cache"), "pixelkiln", "tools");
17506
17681
  }
17507
17682
  function editorDir(pin = EDITOR_PIN, root = editorToolsRoot()) {
17508
- return import_node_path34.default.join(root, "pixelorama", pin.release ?? "unpublished");
17683
+ return import_node_path35.default.join(root, "pixelorama", pin.release ?? "unpublished");
17509
17684
  }
17510
17685
  function editorBaseUrl(pin = EDITOR_PIN) {
17511
17686
  const override = process.env.PIXELKILN_EDITOR_URL?.trim();
@@ -17515,7 +17690,7 @@ function editorBaseUrl(pin = EDITOR_PIN) {
17515
17690
  }
17516
17691
  async function sha256Of(file) {
17517
17692
  try {
17518
- return (0, import_node_crypto7.createHash)("sha256").update(await (0, import_promises27.readFile)(file)).digest("hex");
17693
+ return (0, import_node_crypto8.createHash)("sha256").update(await (0, import_promises27.readFile)(file)).digest("hex");
17519
17694
  } catch {
17520
17695
  return null;
17521
17696
  }
@@ -17528,8 +17703,8 @@ async function editorStatus(opts = {}) {
17528
17703
  let totalBytes = 0;
17529
17704
  for (const [name, expected] of Object.entries(pin.files)) {
17530
17705
  totalBytes += expected.bytes;
17531
- const file = import_node_path34.default.join(dir, name);
17532
- const info = (0, import_node_fs30.existsSync)(file) ? await (0, import_promises27.stat)(file).catch(() => null) : null;
17706
+ const file = import_node_path35.default.join(dir, name);
17707
+ const info = (0, import_node_fs31.existsSync)(file) ? await (0, import_promises27.stat)(file).catch(() => null) : null;
17533
17708
  if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
17534
17709
  installedBytes += expected.bytes;
17535
17710
  } else {
@@ -17582,11 +17757,11 @@ async function installEditor(opts = {}) {
17582
17757
  if (bytes.length !== expected.bytes) {
17583
17758
  throw new EditorInstallError(`${name}: expected ${expected.bytes} bytes, got ${bytes.length}`, name);
17584
17759
  }
17585
- const digest2 = (0, import_node_crypto7.createHash)("sha256").update(bytes).digest("hex");
17760
+ const digest2 = (0, import_node_crypto8.createHash)("sha256").update(bytes).digest("hex");
17586
17761
  if (digest2 !== expected.sha256) {
17587
17762
  throw new EditorInstallError(`${name}: sha256 ${digest2.slice(0, 12)}\u2026 does not match the pinned ${expected.sha256.slice(0, 12)}\u2026`, name);
17588
17763
  }
17589
- const final = import_node_path34.default.join(dir, name);
17764
+ const final = import_node_path35.default.join(dir, name);
17590
17765
  const tmp = `${final}.${process.pid}.part`;
17591
17766
  try {
17592
17767
  await (0, import_promises27.writeFile)(tmp, bytes);
@@ -17632,12 +17807,12 @@ var EDITOR_CONTENT_TYPES = {
17632
17807
  ".json": "application/json; charset=utf-8"
17633
17808
  };
17634
17809
  function editorContentType(name) {
17635
- return EDITOR_CONTENT_TYPES[import_node_path34.default.extname(name).toLowerCase()] ?? null;
17810
+ return EDITOR_CONTENT_TYPES[import_node_path35.default.extname(name).toLowerCase()] ?? null;
17636
17811
  }
17637
17812
 
17638
17813
  // src/gallery/editor.ts
17639
17814
  var import_promises28 = require("fs/promises");
17640
- var import_node_path35 = __toESM(require("path"), 1);
17815
+ var import_node_path36 = __toESM(require("path"), 1);
17641
17816
  function editorRoute(release, name = "index.html") {
17642
17817
  return `/editor/${encodeURIComponent(release)}/${encodeURIComponent(name)}`;
17643
17818
  }
@@ -17710,7 +17885,7 @@ function createGalleryEditorHandlers(opts = {}) {
17710
17885
  if (!status.installed) return null;
17711
17886
  let bytes;
17712
17887
  try {
17713
- bytes = await (0, import_promises28.readFile)(import_node_path35.default.join(dir, name));
17888
+ bytes = await (0, import_promises28.readFile)(import_node_path36.default.join(dir, name));
17714
17889
  } catch {
17715
17890
  verified = null;
17716
17891
  return null;
@@ -17725,7 +17900,7 @@ function createGalleryEditorHandlers(opts = {}) {
17725
17900
  }
17726
17901
 
17727
17902
  // src/gallery/server.ts
17728
- var import_node_crypto8 = require("crypto");
17903
+ var import_node_crypto9 = require("crypto");
17729
17904
  var import_node_http2 = require("http");
17730
17905
  var import_promises29 = require("fs/promises");
17731
17906
  var MAX_EDIT_BYTES = 48 * 1024 * 1024;
@@ -17755,7 +17930,7 @@ async function readJsonBody(req) {
17755
17930
  async function serveGallery(opts) {
17756
17931
  const log = opts.onProgress ?? (() => {
17757
17932
  });
17758
- const session = opts.edit || opts.generate || opts.editor || opts.skeleton || opts.price ? (0, import_node_crypto8.randomBytes)(16).toString("hex") : null;
17933
+ const session = opts.edit || opts.generate || opts.editor || opts.skeleton || opts.price ? (0, import_node_crypto9.randomBytes)(16).toString("hex") : null;
17759
17934
  let media = /* @__PURE__ */ new Map();
17760
17935
  let loading = null;
17761
17936
  const reviewAssets = /* @__PURE__ */ new Map();
@@ -17959,7 +18134,7 @@ async function serveGallery(opts) {
17959
18134
  try {
17960
18135
  if (url2.pathname === "/") {
17961
18136
  const { snapshot } = await load();
17962
- const nonce = (0, import_node_crypto8.randomBytes)(16).toString("base64");
18137
+ const nonce = (0, import_node_crypto9.randomBytes)(16).toString("base64");
17963
18138
  res.writeHead(200, {
17964
18139
  "Content-Type": "text/html; charset=utf-8",
17965
18140
  "Cache-Control": "no-store",
@@ -18034,7 +18209,7 @@ async function serveGallery(opts) {
18034
18209
  }
18035
18210
 
18036
18211
  // src/gallery/generate.ts
18037
- var import_node_crypto9 = require("crypto");
18212
+ var import_node_crypto10 = require("crypto");
18038
18213
  var import_zod14 = require("zod");
18039
18214
  var GenerateRequestSchema = import_zod14.z.object({
18040
18215
  project: import_zod14.z.string().min(1).optional(),
@@ -18334,7 +18509,7 @@ function createGenerateHandlers(opts) {
18334
18509
  }
18335
18510
  }
18336
18511
  const job = {
18337
- id: (0, import_node_crypto9.randomBytes)(8).toString("hex"),
18512
+ id: (0, import_node_crypto10.randomBytes)(8).toString("hex"),
18338
18513
  project: request.project ?? null,
18339
18514
  keys: request.keys,
18340
18515
  mode: request.refresh ? "refresh" : request.resume ? "resume" : request.restore ? "restore" : request.revert !== void 0 ? "revert" : "generate",
@@ -18402,15 +18577,15 @@ function createGenerateHandlers(opts) {
18402
18577
 
18403
18578
  // src/pipeline/salvage.ts
18404
18579
  var import_promises30 = require("fs/promises");
18405
- var import_node_fs31 = require("fs");
18406
- var import_node_path36 = __toESM(require("path"), 1);
18580
+ var import_node_fs32 = require("fs");
18581
+ var import_node_path37 = __toESM(require("path"), 1);
18407
18582
  function providerClaimId(provider, objectId) {
18408
18583
  return `${encodeURIComponent(provider)}:${encodeURIComponent(objectId)}`;
18409
18584
  }
18410
18585
  async function loadClaims(lockPaths, opts = {}) {
18411
18586
  const claimed = /* @__PURE__ */ new Set();
18412
18587
  for (const p of lockPaths) {
18413
- if (!(0, import_node_fs31.existsSync)(p)) throw new Error(`Claim lockfile not found: ${p}`);
18588
+ if (!(0, import_node_fs32.existsSync)(p)) throw new Error(`Claim lockfile not found: ${p}`);
18414
18589
  let parsed;
18415
18590
  try {
18416
18591
  parsed = parseLock(JSON.parse(await (0, import_promises30.readFile)(p, "utf8")));
@@ -18517,20 +18692,20 @@ function matchOrphanStyle(prompt, manifest) {
18517
18692
  return matchStyleByPattern(prompt, manifest);
18518
18693
  }
18519
18694
  async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, claimPaths) {
18520
- const own = import_node_path36.default.resolve(ownManifestPath);
18695
+ const own = import_node_path37.default.resolve(ownManifestPath);
18521
18696
  const siblingManifestPaths = [
18522
18697
  .../* @__PURE__ */ new Set([
18523
18698
  ...workspaceManifestPaths,
18524
- ...claimPaths.map((c) => import_node_path36.default.join(import_node_path36.default.dirname(import_node_path36.default.resolve(c)), "pixelkiln.manifest.json"))
18699
+ ...claimPaths.map((c) => import_node_path37.default.join(import_node_path37.default.dirname(import_node_path37.default.resolve(c)), "pixelkiln.manifest.json"))
18525
18700
  ])
18526
18701
  ];
18527
18702
  const siblings = [];
18528
18703
  for (const siblingManifestPath of siblingManifestPaths) {
18529
- if (import_node_path36.default.resolve(siblingManifestPath) === own) continue;
18530
- if (!(0, import_node_fs31.existsSync)(siblingManifestPath)) continue;
18704
+ if (import_node_path37.default.resolve(siblingManifestPath) === own) continue;
18705
+ if (!(0, import_node_fs32.existsSync)(siblingManifestPath)) continue;
18531
18706
  try {
18532
18707
  const { manifest } = await loadManifest(siblingManifestPath);
18533
- siblings.push({ label: import_node_path36.default.basename(import_node_path36.default.dirname(siblingManifestPath)), manifest });
18708
+ siblings.push({ label: import_node_path37.default.basename(import_node_path37.default.dirname(siblingManifestPath)), manifest });
18534
18709
  } catch {
18535
18710
  }
18536
18711
  }
@@ -18651,7 +18826,7 @@ async function applyTags(provider, decisions, existing, opts = {}) {
18651
18826
 
18652
18827
  // src/pick/salvage-server.ts
18653
18828
  var import_promises31 = require("fs/promises");
18654
- var import_node_path37 = __toESM(require("path"), 1);
18829
+ var import_node_path38 = __toESM(require("path"), 1);
18655
18830
 
18656
18831
  // src/pick/salvage-sheet.ts
18657
18832
  var escapeHtml3 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
@@ -18817,7 +18992,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
18817
18992
  });
18818
18993
  const html = renderSalvageSheet(orphans, {
18819
18994
  styleId: ctx.styleId,
18820
- importDir: import_node_path37.default.relative(process.cwd(), ctx.importDir) || "."
18995
+ importDir: import_node_path38.default.relative(process.cwd(), ctx.importDir) || "."
18821
18996
  });
18822
18997
  const byId = new Map(orphans.map((o) => [o.id, o]));
18823
18998
  const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
@@ -18873,9 +19048,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
18873
19048
  if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
18874
19049
  decodePng(buf);
18875
19050
  const assetId = idFromPrompt(orphan.prompt, taken);
18876
- const rel = import_node_path37.default.join("_salvaged", `${assetId}.png`);
18877
- const outFile = import_node_path37.default.resolve(ctx.importDir, rel);
18878
- await (0, import_promises31.mkdir)(import_node_path37.default.dirname(outFile), { recursive: true });
19051
+ const rel = import_node_path38.default.join("_salvaged", `${assetId}.png`);
19052
+ const outFile = import_node_path38.default.resolve(ctx.importDir, rel);
19053
+ await (0, import_promises31.mkdir)(import_node_path38.default.dirname(outFile), { recursive: true });
18879
19054
  await (0, import_promises31.writeFile)(outFile, buf);
18880
19055
  ctx.manifest.assets[assetId] = {
18881
19056
  prompt: orphan.prompt,
@@ -18908,7 +19083,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
18908
19083
  sourceUrl: durableSource,
18909
19084
  sourceUrls: durableSource ? [{ url: durableSource }] : [],
18910
19085
  outputs: [{
18911
- path: portableOutputPath(outFile, import_node_path37.default.dirname(ctx.manifestPath)),
19086
+ path: portableOutputPath(outFile, import_node_path38.default.dirname(ctx.manifestPath)),
18912
19087
  sha256: sha256(buf)
18913
19088
  }],
18914
19089
  submittedAt: orphan.createdAt,