pixelkiln 0.84.0 → 0.85.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
@@ -2141,9 +2141,9 @@ var PixelLabClient = class {
2141
2141
  apiKey;
2142
2142
  http;
2143
2143
  /** Retries transport failures, 429, and 5xx; see http.ts for the policy. */
2144
- async request(path37, init) {
2144
+ async request(path38, init) {
2145
2145
  const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
2146
- const res = await this.http(`${BASE}${path37}`, {
2146
+ const res = await this.http(`${BASE}${path38}`, {
2147
2147
  ...init,
2148
2148
  headers: {
2149
2149
  Authorization: auth,
@@ -2153,13 +2153,13 @@ var PixelLabClient = class {
2153
2153
  });
2154
2154
  const text = await res.text();
2155
2155
  if (!res.ok) {
2156
- throw new PixelLabError(`${init?.method ?? "GET"} ${path37} \u2192 ${res.status}`, res.status, text);
2156
+ throw new PixelLabError(`${init?.method ?? "GET"} ${path38} \u2192 ${res.status}`, res.status, text);
2157
2157
  }
2158
2158
  if (!text) return {};
2159
2159
  try {
2160
2160
  return JSON.parse(text);
2161
2161
  } catch {
2162
- throw new Error(`${init?.method ?? "GET"} ${path37} returned invalid JSON`);
2162
+ throw new Error(`${init?.method ?? "GET"} ${path38} returned invalid JSON`);
2163
2163
  }
2164
2164
  }
2165
2165
  async balance() {
@@ -2564,10 +2564,10 @@ var PixelLabClient = class {
2564
2564
  const palette = args.paletteSwatchBase64 ? { color_image: { type: "base64", base64: args.paletteSwatchBase64, format: "png" }, force_colors: true } : {};
2565
2565
  const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
2566
2566
  const south = args.reference?.south;
2567
- let path37;
2567
+ let path38;
2568
2568
  let body;
2569
2569
  if (args.mode === "v3") {
2570
- path37 = "/create-character-v3";
2570
+ path38 = "/create-character-v3";
2571
2571
  body = {
2572
2572
  description: args.description,
2573
2573
  image_size: imageSize,
@@ -2581,7 +2581,7 @@ var PixelLabClient = class {
2581
2581
  ...args.enhancePrompt ? { enhance_prompt: true } : {}
2582
2582
  };
2583
2583
  } else if (args.mode === "pro-flash") {
2584
- path37 = "/create-character-pro-flash";
2584
+ path38 = "/create-character-pro-flash";
2585
2585
  const traits = args.styleTraits;
2586
2586
  body = {
2587
2587
  description: args.description,
@@ -2605,7 +2605,7 @@ var PixelLabClient = class {
2605
2605
  } : {}
2606
2606
  };
2607
2607
  } else if (args.mode === "pro") {
2608
- path37 = "/create-character-pro";
2608
+ path38 = "/create-character-pro";
2609
2609
  body = {
2610
2610
  description: args.description,
2611
2611
  image_size: imageSize,
@@ -2619,7 +2619,7 @@ var PixelLabClient = class {
2619
2619
  ...!south && args.styleCharacterId ? { style_character_id: args.styleCharacterId } : {}
2620
2620
  };
2621
2621
  } else {
2622
- path37 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
2622
+ path38 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
2623
2623
  body = {
2624
2624
  description: args.description,
2625
2625
  image_size: imageSize,
@@ -2636,8 +2636,8 @@ var PixelLabClient = class {
2636
2636
  ...palette
2637
2637
  };
2638
2638
  }
2639
- const raw = await this.request(path37, { method: "POST", body: JSON.stringify(body) });
2640
- return validateResponse(CharacterSubmitSchema, raw, path37.slice(1));
2639
+ const raw = await this.request(path38, { method: "POST", body: JSON.stringify(body) });
2640
+ return validateResponse(CharacterSubmitSchema, raw, path38.slice(1));
2641
2641
  }
2642
2642
  /** A text edit of an existing character, applied to every direction and stored as a sibling. */
2643
2643
  async createCharacterState(args) {
@@ -3850,10 +3850,10 @@ var import_node_fs = require("fs");
3850
3850
  function sha256(data) {
3851
3851
  return (0, import_node_crypto.createHash)("sha256").update(data).digest("hex");
3852
3852
  }
3853
- async function sha256File(path37) {
3853
+ async function sha256File(path38) {
3854
3854
  return new Promise((resolve, reject) => {
3855
3855
  const hash = (0, import_node_crypto.createHash)("sha256");
3856
- const stream = (0, import_node_fs.createReadStream)(path37);
3856
+ const stream = (0, import_node_fs.createReadStream)(path38);
3857
3857
  stream.on("data", (chunk2) => hash.update(chunk2));
3858
3858
  stream.on("error", reject);
3859
3859
  stream.on("end", () => resolve(hash.digest("hex")));
@@ -6257,9 +6257,9 @@ var RetroDiffusionClient = class {
6257
6257
  if (!response.ok) throw new ProviderError("retrodiffusion", `Retro Diffusion download failed (${response.status})`, { status: response.status });
6258
6258
  return Buffer.from(await response.arrayBuffer());
6259
6259
  }
6260
- async call(path37, init = {}) {
6260
+ async call(path38, init = {}) {
6261
6261
  if (!this.token) throw new Error("RD_API_KEY is not set");
6262
- const response = await this.request(`${this.baseUrl}${path37}`, {
6262
+ const response = await this.request(`${this.baseUrl}${path38}`, {
6263
6263
  ...init,
6264
6264
  headers: {
6265
6265
  "Content-Type": "application/json",
@@ -6568,8 +6568,13 @@ function retroError(value) {
6568
6568
  }
6569
6569
 
6570
6570
  // src/providers/scenario.ts
6571
+ var import_node_crypto3 = require("crypto");
6572
+ var import_node_fs3 = require("fs");
6573
+ var import_node_os2 = __toESM(require("os"), 1);
6574
+ var import_node_path2 = __toESM(require("path"), 1);
6571
6575
  var DEFAULT_BASE_URL2 = "https://api.cloud.scenario.com/v1";
6572
6576
  var SOURCE_PROTOCOL2 = "scenario:";
6577
+ var SUBMIT_RETRIES = 8;
6573
6578
  var PROTECTED_PARAMETERS = /* @__PURE__ */ new Set([
6574
6579
  "dryRun",
6575
6580
  "height",
@@ -6616,7 +6621,11 @@ var ScenarioClient = class {
6616
6621
  async submit(modelId, body, projectId) {
6617
6622
  const response = await this.call(
6618
6623
  `/generate/custom/${encodeURIComponent(modelId)}`,
6619
- { method: "POST", body: JSON.stringify(body) },
6624
+ // A 429 here means this account's concurrent-job limit refused the
6625
+ // request, so nothing was accepted or billed; wait it out instead of
6626
+ // failing the asset. The default four retries give up after about half
6627
+ // a minute, before a running job has finished.
6628
+ { method: "POST", body: JSON.stringify(body), retries: SUBMIT_RETRIES },
6620
6629
  { projectId }
6621
6630
  );
6622
6631
  const job = response?.job;
@@ -6624,6 +6633,29 @@ var ScenarioClient = class {
6624
6633
  if (!jobId) throw new Error("Scenario did not return an async job id");
6625
6634
  return jobId;
6626
6635
  }
6636
+ /**
6637
+ * Uploads one image (POST /assets) and returns its asset id, which a model's
6638
+ * reference input takes. Free, and idempotent per image: the id is kept by
6639
+ * content hash so a rerun does not upload the same bytes again.
6640
+ */
6641
+ async uploadImage(image, projectId) {
6642
+ const bytes = Buffer.from(image.base64, "base64");
6643
+ const digest2 = sha256(bytes);
6644
+ const cached = readUploadCache()[uploadCacheKey(this.baseUrl, projectId, digest2)];
6645
+ if (cached) return cached;
6646
+ const response = await this.call(
6647
+ "/assets",
6648
+ {
6649
+ method: "POST",
6650
+ body: JSON.stringify({ image: image.base64, name: `pixelkiln-${digest2.slice(0, 12)}.${image.format === "png" ? "png" : "jpg"}` })
6651
+ },
6652
+ { projectId }
6653
+ );
6654
+ const id = response?.asset?.id;
6655
+ if (typeof id !== "string" || !id) throw new Error("Scenario did not return an uploaded asset id");
6656
+ writeUploadCache(uploadCacheKey(this.baseUrl, projectId, digest2), id);
6657
+ return id;
6658
+ }
6627
6659
  async job(id, projectId) {
6628
6660
  const response = await this.call(`/jobs/${encodeURIComponent(id)}`, {}, { projectId });
6629
6661
  const raw = response?.job;
@@ -6750,6 +6782,14 @@ var ScenarioProvider = class _ScenarioProvider {
6750
6782
  supports(generator) {
6751
6783
  return generator === "map";
6752
6784
  }
6785
+ /**
6786
+ * An edit is a generation with the parent as a reference image, which the
6787
+ * editing models (GPT Image, Gemini, FLUX) take. Masked inpainting and the
6788
+ * PixelLab-specific modes have no common Scenario input.
6789
+ */
6790
+ supportsRevision(mode2) {
6791
+ return mode2 === "image-to-image";
6792
+ }
6753
6793
  estimate(spec) {
6754
6794
  const options = scenarioOptions(spec);
6755
6795
  return {
@@ -6770,11 +6810,31 @@ var ScenarioProvider = class _ScenarioProvider {
6770
6810
  if (!Number.isInteger(count) || count < 1 || count > 4) {
6771
6811
  throw new Error("Scenario numOutputs must be a whole number from 1 to 4");
6772
6812
  }
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");
6813
+ if (!Number.isInteger(spec.width) || spec.width < 16 || spec.width > 4096 || !Number.isInteger(spec.height) || spec.height < 16 || spec.height > 4096) {
6814
+ throw new Error("Scenario width and height must be whole numbers from 16 to 4096");
6775
6815
  }
6776
- if (styleImages.length) {
6777
- throw new Error("Scenario styleImages are not supported yet; use a reusable Scenario model");
6816
+ const revision = spec.revision;
6817
+ if (revision) {
6818
+ if (revision.sourceMembers?.length) {
6819
+ throw new Error(
6820
+ `Scenario edits one image; ${revision.sourceAssetId} is a ${revision.sourceMembers.length}-member set`
6821
+ );
6822
+ }
6823
+ if (revision.strength != null) {
6824
+ throw new Error("Scenario image-to-image takes no strength; use the model's own parameters");
6825
+ }
6826
+ }
6827
+ const parameter = referenceParameter(options);
6828
+ if ((styleImages.length || revision) && Object.hasOwn(options.parameters ?? {}, parameter)) {
6829
+ throw new Error(
6830
+ `Scenario parameters.${parameter} is the reference-image input and is filled from styleImages and revisions`
6831
+ );
6832
+ }
6833
+ if (styleImages.length + (revision ? 1 : 0) > 1 && options.referenceArray === false) {
6834
+ throw new Error(`Scenario ${parameter} takes one image; use one style image or a revision, not both`);
6835
+ }
6836
+ if (options.referenceParameter != null && (typeof options.referenceParameter !== "string" || !options.referenceParameter.trim())) {
6837
+ throw new Error("Scenario referenceParameter must be a non-empty string");
6778
6838
  }
6779
6839
  if (options.projectId != null && (typeof options.projectId !== "string" || !options.projectId.trim())) {
6780
6840
  throw new Error("Scenario projectId must be a non-empty string");
@@ -6787,7 +6847,12 @@ var ScenarioProvider = class _ScenarioProvider {
6787
6847
  async submit(spec, styleImages) {
6788
6848
  this.validate(spec, styleImages);
6789
6849
  const options = scenarioOptions(spec);
6790
- const body = requestBody(spec, options);
6850
+ const references = [];
6851
+ if (spec.revision) references.push(readRevisionSource(spec));
6852
+ for (const image of styleImages) references.push({ base64: image.base64, format: image.format });
6853
+ const referenceAssetIds = [];
6854
+ for (const image of references) referenceAssetIds.push(await this.client.uploadImage(image, options.projectId));
6855
+ const body = requestBody(spec, options, referenceAssetIds);
6791
6856
  const quote = await this.client.quote(options.modelId, body, options.projectId);
6792
6857
  if (quote.total > options.maxComputeUnits + Number.EPSILON) {
6793
6858
  throw new Error(
@@ -6804,7 +6869,8 @@ var ScenarioProvider = class _ScenarioProvider {
6804
6869
  quotedComputeUnits: quote.total,
6805
6870
  creativeUnitsCost: quote.creativeUnitsCost,
6806
6871
  costDetails: quote.costDetails,
6807
- ipDetectionCost: quote.ipDetectionCost
6872
+ ipDetectionCost: quote.ipDetectionCost,
6873
+ ...referenceAssetIds.length ? { referenceAssetIds } : {}
6808
6874
  }
6809
6875
  };
6810
6876
  }
@@ -6826,7 +6892,13 @@ var ScenarioProvider = class _ScenarioProvider {
6826
6892
  }
6827
6893
  const assetIds = scenarioAssetIds(job);
6828
6894
  if (!assetIds.length) return { status: "failed", error: "Scenario job returned no asset IDs" };
6829
- const assets = await this.assets(assetIds, reference.projectId);
6895
+ let assets;
6896
+ try {
6897
+ assets = await this.assets(assetIds, reference.projectId);
6898
+ } catch (error) {
6899
+ if (error instanceof UnsupportedScenarioOutput) return { status: "failed", error: error.message };
6900
+ throw error;
6901
+ }
6830
6902
  if (assets.length > 1) {
6831
6903
  return {
6832
6904
  status: "review",
@@ -6881,6 +6953,32 @@ var ScenarioProvider = class _ScenarioProvider {
6881
6953
  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
6954
  }
6883
6955
  };
6956
+ function uploadCachePath() {
6957
+ return process.env.SCENARIO_UPLOAD_CACHE ?? import_node_path2.default.join(import_node_os2.default.tmpdir(), "pixelkiln-scenario", "uploads.json");
6958
+ }
6959
+ function uploadCacheKey(baseUrl, projectId, digest2) {
6960
+ return (0, import_node_crypto3.createHash)("sha256").update(`${baseUrl}
6961
+ ${projectId ?? ""}
6962
+ ${digest2}`).digest("hex");
6963
+ }
6964
+ function readUploadCache() {
6965
+ try {
6966
+ const value = JSON.parse((0, import_node_fs3.readFileSync)(uploadCachePath(), "utf8"));
6967
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
6968
+ } catch {
6969
+ return {};
6970
+ }
6971
+ }
6972
+ function writeUploadCache(key, assetId) {
6973
+ try {
6974
+ const file = uploadCachePath();
6975
+ (0, import_node_fs3.mkdirSync)(import_node_path2.default.dirname(file), { recursive: true });
6976
+ const tmp = `${file}.${process.pid}.tmp`;
6977
+ (0, import_node_fs3.writeFileSync)(tmp, JSON.stringify({ ...readUploadCache(), [key]: assetId }));
6978
+ (0, import_node_fs3.renameSync)(tmp, file);
6979
+ } catch {
6980
+ }
6981
+ }
6884
6982
  function scenarioOptions(spec) {
6885
6983
  return spec.providerOptions;
6886
6984
  }
@@ -6896,9 +6994,25 @@ function scenarioBaseUrl(value) {
6896
6994
  }
6897
6995
  return url.href.replace(/\/$/, "");
6898
6996
  }
6899
- function requestBody(spec, options) {
6997
+ function referenceParameter(options) {
6998
+ return options.referenceParameter?.trim() || "referenceImages";
6999
+ }
7000
+ function readRevisionSource(spec) {
7001
+ const revision = spec.revision;
7002
+ if (!revision.sourceFile || !revision.sourceSha256 || !revision.sourceFormat) {
7003
+ throw new Error(`${spec.styleId}/${spec.assetId}: revision source is not ready`);
7004
+ }
7005
+ const bytes = (0, import_node_fs3.readFileSync)(revision.sourceFile);
7006
+ if (sha256(bytes) !== revision.sourceSha256) {
7007
+ throw new Error(`${spec.styleId}/${spec.assetId}: revision source changed after the manifest was resolved`);
7008
+ }
7009
+ return { base64: bytes.toString("base64"), format: revision.sourceFormat };
7010
+ }
7011
+ function requestBody(spec, options, referenceAssetIds = []) {
7012
+ const parameter = referenceParameter(options);
6900
7013
  return {
6901
7014
  ...options.parameters ?? {},
7015
+ ...referenceAssetIds.length ? { [parameter]: options.referenceArray === false ? referenceAssetIds[0] : referenceAssetIds } : {},
6902
7016
  prompt: spec.prompt,
6903
7017
  width: spec.width,
6904
7018
  height: spec.height,
@@ -6988,9 +7102,11 @@ function scenarioAssetIds(job) {
6988
7102
  function assetDownloadUrl(asset) {
6989
7103
  return asset.originalFileUrl || asset.url;
6990
7104
  }
7105
+ var UnsupportedScenarioOutput = class extends Error {
7106
+ };
6991
7107
  function assertPngAsset(asset) {
6992
7108
  if (asset.originalMimeType && asset.originalMimeType !== MediaType.PNG) {
6993
- throw new Error(
7109
+ throw new UnsupportedScenarioOutput(
6994
7110
  `Scenario asset ${asset.id} is ${asset.originalMimeType}; PixelKiln's Scenario MVP requires PNG output`
6995
7111
  );
6996
7112
  }
@@ -7055,9 +7171,9 @@ function redactScenarioError(value, secrets) {
7055
7171
  }
7056
7172
 
7057
7173
  // src/providers/comfyui.ts
7058
- var import_node_crypto3 = require("crypto");
7174
+ var import_node_crypto4 = require("crypto");
7059
7175
  var import_promises = require("fs/promises");
7060
- var import_node_path2 = __toESM(require("path"), 1);
7176
+ var import_node_path3 = __toESM(require("path"), 1);
7061
7177
  var DEFAULT_BASE_URL3 = "http://127.0.0.1:8188";
7062
7178
  var BUILTIN_BINDINGS = /* @__PURE__ */ new Set([
7063
7179
  "prompt",
@@ -7084,7 +7200,7 @@ var ComfyUIClient = class {
7084
7200
  async submit(workflow) {
7085
7201
  const value = await this.json("prompt", {
7086
7202
  method: "POST",
7087
- body: JSON.stringify({ prompt: workflow, client_id: (0, import_node_crypto3.randomUUID)() })
7203
+ body: JSON.stringify({ prompt: workflow, client_id: (0, import_node_crypto4.randomUUID)() })
7088
7204
  });
7089
7205
  if (!isObject(value)) throw new Error("ComfyUI returned an invalid queue response");
7090
7206
  const promptId = value.prompt_id;
@@ -7194,7 +7310,7 @@ var ComfyUIProvider = class _ComfyUIProvider {
7194
7310
  }
7195
7311
  async resolveOptions(value, context) {
7196
7312
  const options = parseOptions(value);
7197
- const workflowPath = import_node_path2.default.resolve(context.root, options.workflowFile);
7313
+ const workflowPath = import_node_path3.default.resolve(context.root, options.workflowFile);
7198
7314
  let raw;
7199
7315
  try {
7200
7316
  raw = await (0, import_promises.readFile)(workflowPath, "utf8");
@@ -7249,7 +7365,7 @@ var ComfyUIProvider = class _ComfyUIProvider {
7249
7365
  if (typeof item !== "string") {
7250
7366
  throw new Error(`ComfyUI ${label} must be a manifest-relative PNG or JPEG path`);
7251
7367
  }
7252
- const file = import_node_path2.default.resolve(context.root, item);
7368
+ const file = import_node_path3.default.resolve(context.root, item);
7253
7369
  let bytes;
7254
7370
  try {
7255
7371
  bytes = await (0, import_promises.readFile)(file);
@@ -7961,7 +8077,7 @@ function canonical(value) {
7961
8077
  }
7962
8078
 
7963
8079
  // src/providers/fake.ts
7964
- var import_node_crypto4 = require("crypto");
8080
+ var import_node_crypto5 = require("crypto");
7965
8081
  var FAKE_PNG = Buffer.from(
7966
8082
  "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
7967
8083
  "base64"
@@ -8063,7 +8179,7 @@ var FakeProvider = class {
8063
8179
  }
8064
8180
  async download(url) {
8065
8181
  if (!url.startsWith("fake://")) throw new Error(`fake provider cannot download ${url}`);
8066
- const salt = (0, import_node_crypto4.createHash)("sha256").update(url).digest();
8182
+ const salt = (0, import_node_crypto5.createHash)("sha256").update(url).digest();
8067
8183
  return Buffer.concat([FAKE_PNG, salt.subarray(0, 4)]);
8068
8184
  }
8069
8185
  async balance() {
@@ -8211,8 +8327,8 @@ registerProvider({
8211
8327
 
8212
8328
  // src/manifest.ts
8213
8329
  var import_promises3 = require("fs/promises");
8214
- var import_node_fs5 = require("fs");
8215
- var import_node_path5 = __toESM(require("path"), 1);
8330
+ var import_node_fs6 = require("fs");
8331
+ var import_node_path6 = __toESM(require("path"), 1);
8216
8332
 
8217
8333
  // src/loop-directions.ts
8218
8334
  var SINGLE_OUTPUT_FIELDS = ["file", "cell", "remoteId", "source", "sourceByStyle", "outputRole"];
@@ -8300,15 +8416,15 @@ function isObject2(value) {
8300
8416
  }
8301
8417
 
8302
8418
  // src/outputs.ts
8303
- var import_node_fs4 = require("fs");
8304
- var import_node_path4 = __toESM(require("path"), 1);
8419
+ var import_node_fs5 = require("fs");
8420
+ var import_node_path5 = __toESM(require("path"), 1);
8305
8421
 
8306
8422
  // src/lock.ts
8307
8423
  var import_promises2 = require("fs/promises");
8308
- var import_node_fs3 = require("fs");
8309
- var import_node_path3 = __toESM(require("path"), 1);
8424
+ var import_node_fs4 = require("fs");
8425
+ var import_node_path4 = __toESM(require("path"), 1);
8310
8426
  async function loadLock(lockPath) {
8311
- if (!(0, import_node_fs3.existsSync)(lockPath)) return { version: 2, entries: {} };
8427
+ if (!(0, import_node_fs4.existsSync)(lockPath)) return { version: 2, entries: {} };
8312
8428
  try {
8313
8429
  return parseLock(JSON.parse(await (0, import_promises2.readFile)(lockPath, "utf8")));
8314
8430
  } catch (err) {
@@ -8323,7 +8439,7 @@ var saveQueues = /* @__PURE__ */ new Map();
8323
8439
  var dirtyPatches = /* @__PURE__ */ new WeakMap();
8324
8440
  var dirtyDeletes = /* @__PURE__ */ new WeakMap();
8325
8441
  function saveLock(lockPath, lock) {
8326
- const queueKey = import_node_path3.default.resolve(lockPath);
8442
+ const queueKey = import_node_path4.default.resolve(lockPath);
8327
8443
  const previous = saveQueues.get(queueKey) ?? Promise.resolve();
8328
8444
  const next = previous.catch(() => {
8329
8445
  }).then(() => writeLockNow(lockPath, lock));
@@ -8335,7 +8451,7 @@ function saveLock(lockPath, lock) {
8335
8451
  return next;
8336
8452
  }
8337
8453
  async function writeLockNow(lockPath, lock) {
8338
- await (0, import_promises2.mkdir)(import_node_path3.default.dirname(import_node_path3.default.resolve(lockPath)), { recursive: true });
8454
+ await (0, import_promises2.mkdir)(import_node_path4.default.dirname(import_node_path4.default.resolve(lockPath)), { recursive: true });
8339
8455
  const release = await acquireFileLock(lockPath);
8340
8456
  try {
8341
8457
  await writeLockWhileHeld(lockPath, lock);
@@ -8345,7 +8461,7 @@ async function writeLockNow(lockPath, lock) {
8345
8461
  }
8346
8462
  async function writeLockWhileHeld(file, lock) {
8347
8463
  let disk = { version: 2, entries: {} };
8348
- if ((0, import_node_fs3.existsSync)(file)) {
8464
+ if ((0, import_node_fs4.existsSync)(file)) {
8349
8465
  try {
8350
8466
  disk = parseLock(JSON.parse(await (0, import_promises2.readFile)(file, "utf8")));
8351
8467
  } catch (err) {
@@ -8480,22 +8596,22 @@ function totalSpend(lock, unit = "generations") {
8480
8596
 
8481
8597
  // src/outputs.ts
8482
8598
  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("/");
8599
+ const root = import_node_path5.default.resolve(manifestDir);
8600
+ const absolute = import_node_path5.default.isAbsolute(file) ? import_node_path5.default.normalize(file) : import_node_path5.default.resolve(root, file);
8601
+ const relative = import_node_path5.default.relative(root, absolute);
8602
+ const value = import_node_path5.default.isAbsolute(relative) ? absolute : relative;
8603
+ return value.split(import_node_path5.default.sep).join("/");
8488
8604
  }
8489
8605
  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));
8606
+ if (import_node_path5.default.isAbsolute(recordedPath)) return import_node_path5.default.normalize(recordedPath);
8607
+ if (import_node_path5.default.win32.isAbsolute(recordedPath)) return recordedPath;
8608
+ return import_node_path5.default.resolve(manifestDir, recordedPath.split(/[\\/]/).join(import_node_path5.default.sep));
8493
8609
  }
8494
8610
  function expectedOutputPath(spec, role, index, total, mediaType) {
8495
8611
  return memberPath(spec.outFile, role, index, total, mediaType);
8496
8612
  }
8497
8613
  function memberPath(file, role, index, total, mediaType) {
8498
- const originalExt = import_node_path4.default.extname(file);
8614
+ const originalExt = import_node_path5.default.extname(file);
8499
8615
  const ext = mediaType ? mediaExtension(mediaType) : originalExt || ".png";
8500
8616
  const stem = originalExt ? file.slice(0, -originalExt.length) : file;
8501
8617
  if (total === 1) return `${stem}${ext}`;
@@ -8509,10 +8625,10 @@ function isFrameSetEntry(entry) {
8509
8625
  return entry.generator === "frames" && isMemberSetEntry(entry);
8510
8626
  }
8511
8627
  function sourceIsStem(source, entry, manifestDir) {
8512
- return isMemberSetEntry(entry) && !(0, import_node_fs4.existsSync)(import_node_path4.default.resolve(manifestDir, source));
8628
+ return isMemberSetEntry(entry) && !(0, import_node_fs5.existsSync)(import_node_path5.default.resolve(manifestDir, source));
8513
8629
  }
8514
8630
  function sourceOutputPath(source, entry, index, manifestDir) {
8515
- const absolute = import_node_path4.default.resolve(manifestDir, source);
8631
+ const absolute = import_node_path5.default.resolve(manifestDir, source);
8516
8632
  if (!sourceIsStem(source, entry, manifestDir)) return absolute;
8517
8633
  const output = entry.outputs[index];
8518
8634
  return memberPath(absolute, output.role, index, entry.outputs.length, output.mediaType);
@@ -8599,7 +8715,7 @@ function resolveSpecOutputs(spec, lock, manifestDir) {
8599
8715
  id: spec.assetId,
8600
8716
  index: 0,
8601
8717
  path: spec.outFile,
8602
- absolutePath: import_node_path4.default.resolve(spec.outFile),
8718
+ absolutePath: import_node_path5.default.resolve(spec.outFile),
8603
8719
  sha256: ""
8604
8720
  }];
8605
8721
  }
@@ -8615,8 +8731,8 @@ function frameSetFps(entry) {
8615
8731
 
8616
8732
  // src/manifest.ts
8617
8733
  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}`);
8734
+ const abs = import_node_path6.default.resolve(manifestPath);
8735
+ if (!(0, import_node_fs6.existsSync)(abs)) throw new ProjectError(`No manifest at ${abs}`);
8620
8736
  const expansion = expandLoopDirections(JSON.parse(await (0, import_promises3.readFile)(abs, "utf8")));
8621
8737
  if (expansion.issues.length) {
8622
8738
  throw new ProjectError(`Manifest at ${abs} is invalid:
@@ -8708,7 +8824,7 @@ ${issues}`);
8708
8824
  ${unknownReferences.map((i) => ` ${i}`).join("\n")}`);
8709
8825
  }
8710
8826
  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 };
8827
+ return { manifest: parsed.data, root: import_node_path6.default.dirname(abs), path: abs, ...loopFamilies };
8712
8828
  }
8713
8829
  function parentAssetId(asset) {
8714
8830
  return asset?.revision?.from ?? asset?.state?.of ?? asset?.animation?.of ?? asset?.portrait?.of ?? asset?.outfit?.of;
@@ -8809,10 +8925,10 @@ async function resolveSpecs(loaded, filter) {
8809
8925
  }
8810
8926
  const styleImageCache = /* @__PURE__ */ new Map();
8811
8927
  async function loadStyleImage(rel, what = "Style image") {
8812
- const abs = import_node_path5.default.resolve(root, rel);
8928
+ const abs = import_node_path6.default.resolve(root, rel);
8813
8929
  let hit = styleImageCache.get(abs);
8814
8930
  if (!hit) {
8815
- if (!(0, import_node_fs5.existsSync)(abs)) throw new Error(`${what} not found: ${abs}`);
8931
+ if (!(0, import_node_fs6.existsSync)(abs)) throw new Error(`${what} not found: ${abs}`);
8816
8932
  const buf = await (0, import_promises3.readFile)(abs);
8817
8933
  const metadata = imageMetadata(buf);
8818
8934
  if (!metadata) throw new Error(`${what} is not a readable PNG or JPEG: ${abs}`);
@@ -8839,7 +8955,7 @@ async function resolveSpecs(loaded, filter) {
8839
8955
  styleImageDimensions.push(loadedImage);
8840
8956
  }
8841
8957
  const resolvedImages = style.styleImages.map((image) => {
8842
- const hit = styleImageCache.get(import_node_path5.default.resolve(root, image.path));
8958
+ const hit = styleImageCache.get(import_node_path6.default.resolve(root, image.path));
8843
8959
  return { base64: hit.base64, width: hit.width, height: hit.height, format: hit.format };
8844
8960
  });
8845
8961
  const styleSpecs = /* @__PURE__ */ new Map();
@@ -8940,9 +9056,9 @@ async function resolveSpecs(loaded, filter) {
8940
9056
  } else {
8941
9057
  prompt = [style.promptPrefix, subject, style.promptSuffix].map((p) => p.trim()).filter(Boolean).join(", ");
8942
9058
  }
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;
9059
+ const relFile = asset.file ?? import_node_path6.default.join(asset.category ?? "", `${assetId}.png`);
9060
+ const outFile = import_node_path6.default.resolve(root, style.outDir, relFile);
9061
+ const qualityOutFile = style.quality ? import_node_path6.default.resolve(root, style.quality.outDir, pngPath(relFile)) : void 0;
8946
9062
  const tileSize = generator === "tiles" ? size : style.tileSize ?? 32;
8947
9063
  const tileVariations = tileFeatureOutputCount(generator === "tiles" ? style.tileFeature : void 0) ?? tileVariationCount(countNumberedDescriptions(prompt));
8948
9064
  const terrainTiles = generator === "terrain" ? terrainTileCount(style.terrainTransitionSize) : 0;
@@ -9037,7 +9153,7 @@ async function resolveSpecs(loaded, filter) {
9037
9153
  minGridConfidence: style.quality.minGridConfidence,
9038
9154
  ...style.quality.minTransparency == null ? {} : { minTransparency: style.quality.minTransparency },
9039
9155
  ...style.quality.fixerRevision ? { fixerRevision: style.quality.fixerRevision } : {},
9040
- ...style.quality.fixerPython ? { fixerPython: import_node_path5.default.resolve(root, style.quality.fixerPython) } : {},
9156
+ ...style.quality.fixerPython ? { fixerPython: import_node_path6.default.resolve(root, style.quality.fixerPython) } : {},
9041
9157
  ...style.quality.fps == null ? {} : { fps: style.quality.fps }
9042
9158
  }
9043
9159
  } : {},
@@ -9120,7 +9236,7 @@ async function resolveSpecs(loaded, filter) {
9120
9236
  const file = memberPath(anchorSpec.outFile, "south", 0, anchorSpec.character.directions, MediaType.PNG);
9121
9237
  resolved.character = {
9122
9238
  ...resolved.character,
9123
- styleAnchor: { assetId: anchorId, spec: anchorSpec, file, sha256: (0, import_node_fs5.existsSync)(file) ? await sha256File(file) : null }
9239
+ styleAnchor: { assetId: anchorId, spec: anchorSpec, file, sha256: (0, import_node_fs6.existsSync)(file) ? await sha256File(file) : null }
9124
9240
  };
9125
9241
  }
9126
9242
  const characterParent = asset.state?.of ?? asset.animation?.of ?? asset.portrait?.of;
@@ -9133,7 +9249,7 @@ async function resolveSpecs(loaded, filter) {
9133
9249
  parentAssetId: characterParent,
9134
9250
  parentSpec,
9135
9251
  parentFile,
9136
- parentSha256: (0, import_node_fs5.existsSync)(parentFile) ? await sha256File(parentFile) : null,
9252
+ parentSha256: (0, import_node_fs6.existsSync)(parentFile) ? await sha256File(parentFile) : null,
9137
9253
  // A state and its animations keep the engine and rotations of the base.
9138
9254
  mode: resolved.character.kind === "base" ? resolved.character.mode : parentSpec.character?.mode ?? resolved.character.mode,
9139
9255
  directions: resolved.character.kind === "base" ? resolved.character.directions : parentSpec.character?.directions ?? resolved.character.directions
@@ -9151,7 +9267,7 @@ async function resolveSpecs(loaded, filter) {
9151
9267
  parentAssetId: characterParent,
9152
9268
  parentSpec,
9153
9269
  parentFile,
9154
- parentSha256: (0, import_node_fs5.existsSync)(parentFile) ? await sha256File(parentFile) : null,
9270
+ parentSha256: (0, import_node_fs6.existsSync)(parentFile) ? await sha256File(parentFile) : null,
9155
9271
  // A state and its animations keep the base's own rotation count.
9156
9272
  directions: resolved.objectPro.kind === "base" ? resolved.objectPro.directions : parentSpec.objectPro.directions
9157
9273
  };
@@ -9173,7 +9289,7 @@ async function resolveSpecs(loaded, filter) {
9173
9289
  animation: sourceSpec.character.animation,
9174
9290
  outfit: {
9175
9291
  reference: {
9176
- path: import_node_path5.default.resolve(root, asset.outfit.reference),
9292
+ path: import_node_path6.default.resolve(root, asset.outfit.reference),
9177
9293
  sha256: referenceImage.hash,
9178
9294
  width: referenceImage.width,
9179
9295
  height: referenceImage.height,
@@ -9195,23 +9311,23 @@ async function resolveSpecs(loaded, filter) {
9195
9311
  );
9196
9312
  }
9197
9313
  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);
9314
+ const sourceStem = sourceSpec.quality?.outFile ?? (sourceSpec.source ? import_node_path6.default.resolve(root, sourceSpec.source) : sourceSpec.outFile);
9199
9315
  const sourceMembers = await revisionSourceMembers(sourceStem, `revision source ${asset.revision.from}`);
9200
9316
  const sourceFile = sourceMembers?.[0]?.file ?? sourceStem;
9201
9317
  const sourceImage = sourceMembers ? { ...sourceMembers[0], hash: sha256(JSON.stringify(sourceMembers.map((member) => member.sha256))) } : await optionalRevisionImage(sourceFile, "revision source");
9202
9318
  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;
9319
+ const maskFile = asset.revision.mask ? import_node_path6.default.resolve(root, asset.revision.mask) : void 0;
9204
9320
  const maskImage = maskFile ? await optionalRevisionImage(maskFile, "revision mask", "png") : null;
9205
9321
  if (sourceImage && maskImage && (sourceImage.width !== maskImage.width || sourceImage.height !== maskImage.height)) {
9206
9322
  throw new Error(
9207
9323
  `Revision mask for ${styleId}/${assetId} is ${maskImage.width}x${maskImage.height}; source ${asset.revision.from} is ${sourceImage.width}x${sourceImage.height}`
9208
9324
  );
9209
9325
  }
9210
- const paletteImageFile = asset.revision.paletteImage ? import_node_path5.default.resolve(root, asset.revision.paletteImage) : void 0;
9326
+ const paletteImageFile = asset.revision.paletteImage ? import_node_path6.default.resolve(root, asset.revision.paletteImage) : void 0;
9211
9327
  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;
9328
+ const lastFrameFile = asset.revision.lastFrame ? import_node_path6.default.resolve(root, asset.revision.lastFrame) : void 0;
9213
9329
  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;
9330
+ const keypointsFile = asset.revision.keypointsFile ? import_node_path6.default.resolve(root, asset.revision.keypointsFile) : void 0;
9215
9331
  const keypoints = keypointsFile ? await optionalSkeletonSet(keypointsFile) : null;
9216
9332
  resolved.revision = {
9217
9333
  mode: asset.revision.mode,
@@ -9378,7 +9494,7 @@ async function resolveSpecs(loaded, filter) {
9378
9494
  return specs;
9379
9495
  }
9380
9496
  function pngPath(file) {
9381
- const extension = import_node_path5.default.extname(file);
9497
+ const extension = import_node_path6.default.extname(file);
9382
9498
  return extension ? `${file.slice(0, -extension.length)}.png` : `${file}.png`;
9383
9499
  }
9384
9500
  async function resolveMapScene(root, styleId, assetId, generator, scene, provider) {
@@ -9388,12 +9504,12 @@ async function resolveMapScene(root, styleId, assetId, generator, scene, provide
9388
9504
  if (!provider.supportsMapScene) {
9389
9505
  throw new Error(`assets.${assetId}.scene: provider "${provider.id}" cannot draw into a scene; PixelLab's /map-objects can`);
9390
9506
  }
9391
- const file = import_node_path5.default.resolve(root, scene.image);
9507
+ const file = import_node_path6.default.resolve(root, scene.image);
9392
9508
  const image = await optionalRevisionImage(file, "scene image", "png");
9393
9509
  const base = { file, sha256: image?.hash ?? null, width: image?.width ?? null, height: image?.height ?? null };
9394
9510
  const placement = scene.placement;
9395
9511
  if ("mask" in placement) {
9396
- const maskFile = import_node_path5.default.resolve(root, placement.mask);
9512
+ const maskFile = import_node_path6.default.resolve(root, placement.mask);
9397
9513
  const mask = await optionalRevisionImage(maskFile, "scene mask", "png");
9398
9514
  if (image && mask && (image.width !== mask.width || image.height !== mask.height)) {
9399
9515
  throw new Error(
@@ -9408,7 +9524,7 @@ async function resolveMapScene(root, styleId, assetId, generator, scene, provide
9408
9524
  return "oval" in placement ? { ...base, placement: { type: "oval", fraction: placement.oval } } : { ...base, placement: { type: "rectangle", fraction: placement.rectangle } };
9409
9525
  }
9410
9526
  async function optionalRevisionImage(file, label, requiredFormat) {
9411
- if (!(0, import_node_fs5.existsSync)(file)) return null;
9527
+ if (!(0, import_node_fs6.existsSync)(file)) return null;
9412
9528
  const bytes = await (0, import_promises3.readFile)(file);
9413
9529
  const metadata = imageMetadata(bytes);
9414
9530
  if (!metadata) throw new Error(`${label} is not a readable PNG or JPEG: ${file}`);
@@ -9418,7 +9534,7 @@ async function optionalRevisionImage(file, label, requiredFormat) {
9418
9534
  return { hash: sha256(bytes), ...metadata };
9419
9535
  }
9420
9536
  async function optionalSkeletonSet(file) {
9421
- if (!(0, import_node_fs5.existsSync)(file)) return null;
9537
+ if (!(0, import_node_fs6.existsSync)(file)) return null;
9422
9538
  const bytes = await (0, import_promises3.readFile)(file);
9423
9539
  let json;
9424
9540
  try {
@@ -9429,17 +9545,17 @@ async function optionalSkeletonSet(file) {
9429
9545
  return { hash: sha256(bytes), skeleton: parseSkeletonSet(json, file) };
9430
9546
  }
9431
9547
  async function revisionSourceMembers(stem, label) {
9432
- if ((0, import_node_fs5.existsSync)(stem)) return null;
9548
+ if ((0, import_node_fs6.existsSync)(stem)) return null;
9433
9549
  const at = (role) => memberPath(stem, role, 0, 2, MediaType.PNG);
9434
9550
  let roles = [];
9435
9551
  for (const set of [CHARACTER_DIRECTIONS_8, CHARACTER_DIRECTIONS_4]) {
9436
- if (set.every((direction) => (0, import_node_fs5.existsSync)(at(direction)))) {
9552
+ if (set.every((direction) => (0, import_node_fs6.existsSync)(at(direction)))) {
9437
9553
  roles = [...set];
9438
9554
  break;
9439
9555
  }
9440
9556
  }
9441
9557
  if (!roles.length) {
9442
- for (let index = 0; (0, import_node_fs5.existsSync)(at(`frame-${String(index).padStart(2, "0")}`)); index++) {
9558
+ for (let index = 0; (0, import_node_fs6.existsSync)(at(`frame-${String(index).padStart(2, "0")}`)); index++) {
9443
9559
  roles.push(`frame-${String(index).padStart(2, "0")}`);
9444
9560
  }
9445
9561
  }
@@ -9465,7 +9581,7 @@ async function resolveStyleImages(loaded, styleId) {
9465
9581
  if (!style) throw new Error(`Unknown style "${styleId}"`);
9466
9582
  const out = [];
9467
9583
  for (const img of style.styleImages) {
9468
- const file = import_node_path5.default.resolve(loaded.root, img.path);
9584
+ const file = import_node_path6.default.resolve(loaded.root, img.path);
9469
9585
  const buf = await (0, import_promises3.readFile)(file);
9470
9586
  const metadata = imageMetadata(buf);
9471
9587
  if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${file}`);
@@ -9496,7 +9612,7 @@ async function resolveCharacterShape(asset, style, kind, files) {
9496
9612
  };
9497
9613
  if (asset.concept) {
9498
9614
  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 };
9615
+ 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
9616
  }
9501
9617
  if (asset.reference) {
9502
9618
  const byDirection = typeof asset.reference === "string" ? { south: asset.reference } : asset.reference;
@@ -9504,7 +9620,7 @@ async function resolveCharacterShape(asset, style, kind, files) {
9504
9620
  for (const [direction, rel] of Object.entries(byDirection)) {
9505
9621
  const image = await files.load(rel, `Reference sprite (${direction})`);
9506
9622
  shape.reference[direction] = {
9507
- path: import_node_path5.default.resolve(files.root, rel),
9623
+ path: import_node_path6.default.resolve(files.root, rel),
9508
9624
  sha256: image.hash,
9509
9625
  width: image.width,
9510
9626
  height: image.height,
@@ -9525,7 +9641,7 @@ async function resolveCharacterShape(asset, style, kind, files) {
9525
9641
  const animationMode = asset.animation.mode ?? (asset.animation.template ? "template" : "v3");
9526
9642
  const pose = async (rel, what) => {
9527
9643
  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 };
9644
+ return { path: import_node_path6.default.resolve(files.root, rel), sha256: image.hash, width: image.width, height: image.height, format: image.format };
9529
9645
  };
9530
9646
  shape.animation = {
9531
9647
  mode: animationMode,
@@ -9556,7 +9672,7 @@ async function resolveObjectProShape(assetId, asset, style, kind, files) {
9556
9672
  const south = typeof asset.reference === "string" ? asset.reference : asset.reference.south;
9557
9673
  if (south) {
9558
9674
  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 };
9675
+ shape.reference = { path: import_node_path6.default.resolve(files.root, south), sha256: image.hash, width: image.width, height: image.height, format: image.format };
9560
9676
  }
9561
9677
  }
9562
9678
  if (asset.state) {
@@ -9584,7 +9700,7 @@ async function resolveObjectProShape(assetId, asset, style, kind, files) {
9584
9700
  }
9585
9701
  const pose = async (rel, what) => {
9586
9702
  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 };
9703
+ return { path: import_node_path6.default.resolve(files.root, rel), sha256: image.hash, width: image.width, height: image.height, format: image.format };
9588
9704
  };
9589
9705
  shape.animation = {
9590
9706
  mode: asset.animation.mode ?? "v3",
@@ -9604,11 +9720,11 @@ function characterDirections(directions) {
9604
9720
  }
9605
9721
 
9606
9722
  // src/project.ts
9607
- var import_node_path13 = __toESM(require("path"), 1);
9723
+ var import_node_path14 = __toESM(require("path"), 1);
9608
9724
 
9609
9725
  // src/env.ts
9610
- var import_node_fs6 = require("fs");
9611
- var import_node_path6 = __toESM(require("path"), 1);
9726
+ var import_node_fs7 = require("fs");
9727
+ var import_node_path7 = __toESM(require("path"), 1);
9612
9728
  function loadEnvFiles(dir) {
9613
9729
  const loaded = [];
9614
9730
  for (const [file, vars] of envFiles(dir)) {
@@ -9622,10 +9738,10 @@ function loadEnvFiles(dir) {
9622
9738
  function envFiles(dir) {
9623
9739
  const out = [];
9624
9740
  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;
9741
+ const file = import_node_path7.default.join(dir, name);
9742
+ if (!(0, import_node_fs7.existsSync)(file)) continue;
9627
9743
  try {
9628
- out.push([file, parseEnv((0, import_node_fs6.readFileSync)(file, "utf8"))]);
9744
+ out.push([file, parseEnv((0, import_node_fs7.readFileSync)(file, "utf8"))]);
9629
9745
  } catch {
9630
9746
  }
9631
9747
  }
@@ -9651,12 +9767,12 @@ function parseEnv(contents) {
9651
9767
  }
9652
9768
 
9653
9769
  // src/pipeline/plan.ts
9654
- var import_node_fs9 = require("fs");
9655
- var import_node_path12 = __toESM(require("path"), 1);
9770
+ var import_node_fs10 = require("fs");
9771
+ var import_node_path13 = __toESM(require("path"), 1);
9656
9772
 
9657
9773
  // src/pipeline/mirror.ts
9658
9774
  var import_promises4 = require("fs/promises");
9659
- var import_node_path7 = __toESM(require("path"), 1);
9775
+ var import_node_path8 = __toESM(require("path"), 1);
9660
9776
 
9661
9777
  // src/palette.ts
9662
9778
  function normalizePalette(values) {
@@ -9795,7 +9911,7 @@ async function deriveMirror(spec, lock, lockPath, opts = {}) {
9795
9911
  for (const [index, member] of members.entries()) {
9796
9912
  const bytes = flipHorizontal(await (0, import_promises4.readFile)(member.file));
9797
9913
  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 });
9914
+ await (0, import_promises4.mkdir)(import_node_path8.default.dirname(target), { recursive: true });
9799
9915
  const tmp = `${target}.pixelkiln.tmp`;
9800
9916
  await (0, import_promises4.writeFile)(tmp, bytes);
9801
9917
  await (0, import_promises4.rename)(tmp, target);
@@ -9857,29 +9973,29 @@ async function deriveMirror(spec, lock, lockPath, opts = {}) {
9857
9973
  }
9858
9974
 
9859
9975
  // src/pipeline/quality-profile.ts
9860
- var import_node_fs7 = require("fs");
9861
- var import_node_path10 = __toESM(require("path"), 1);
9976
+ var import_node_fs8 = require("fs");
9977
+ var import_node_path11 = __toESM(require("path"), 1);
9862
9978
 
9863
9979
  // src/pipeline/refine.ts
9864
- var import_node_path9 = __toESM(require("path"), 1);
9980
+ var import_node_path10 = __toESM(require("path"), 1);
9865
9981
  var import_node_child_process2 = require("child_process");
9866
9982
  var import_promises6 = require("fs/promises");
9867
- var import_node_os2 = require("os");
9983
+ var import_node_os3 = require("os");
9868
9984
  var import_node_util = require("util");
9869
9985
 
9870
9986
  // src/artifacts.ts
9871
- var import_node_path8 = __toESM(require("path"), 1);
9872
- var import_node_crypto5 = require("crypto");
9987
+ var import_node_path9 = __toESM(require("path"), 1);
9988
+ var import_node_crypto6 = require("crypto");
9873
9989
  var import_promises5 = require("fs/promises");
9874
9990
  var activeTransactions = /* @__PURE__ */ new Set();
9875
9991
  function message(error) {
9876
9992
  return error instanceof Error ? error.message : String(error);
9877
9993
  }
9878
9994
  function digest(data) {
9879
- return (0, import_node_crypto5.createHash)("sha256").update(data).digest("hex");
9995
+ return (0, import_node_crypto6.createHash)("sha256").update(data).digest("hex");
9880
9996
  }
9881
9997
  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("/") || ".";
9998
+ return import_node_path9.default.relative(from, import_node_path9.default.resolve(to)).split(import_node_path9.default.sep).join("/") || ".";
9883
9999
  }
9884
10000
  function canonical2(value) {
9885
10001
  if (value === null || typeof value === "string" || typeof value === "boolean") return value;
@@ -9929,7 +10045,7 @@ function parseArtifactManifest(absolute, data) {
9929
10045
  return raw;
9930
10046
  }
9931
10047
  async function readArtifactBundleManifest(manifestPath) {
9932
- const absolute = import_node_path8.default.resolve(manifestPath);
10048
+ const absolute = import_node_path9.default.resolve(manifestPath);
9933
10049
  return parseArtifactManifest(absolute, await (0, import_promises5.readFile)(absolute, "utf8"));
9934
10050
  }
9935
10051
  async function readOptional(file) {
@@ -9953,8 +10069,8 @@ function processIsAlive(pid) {
9953
10069
  }
9954
10070
  }
9955
10071
  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}-`
10072
+ return import_node_path9.default.dirname(candidate) === import_node_path9.default.dirname(destination) && import_node_path9.default.basename(candidate).startsWith(
10073
+ `.${import_node_path9.default.basename(destination)}.pixelkiln-${type}-`
9958
10074
  );
9959
10075
  }
9960
10076
  function parseTransaction(journal, data) {
@@ -9974,7 +10090,7 @@ async function removeTransactionFiles(journal) {
9974
10090
  await (0, import_promises5.rm)(transactionMarker(journal), { force: true });
9975
10091
  }
9976
10092
  async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
9977
- const journal = import_node_path8.default.resolve(recoveryFile);
10093
+ const journal = import_node_path9.default.resolve(recoveryFile);
9978
10094
  const bytes = await readOptional(journal);
9979
10095
  if (!bytes) {
9980
10096
  await (0, import_promises5.rm)(transactionMarker(journal), { force: true });
@@ -9990,7 +10106,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
9990
10106
  );
9991
10107
  }
9992
10108
  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")) {
10109
+ 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
10110
  throw new Error(
9995
10111
  `Refusing unsafe artifact recovery from ${journal}; its destinations or temporary paths do not match the current bundle.`
9996
10112
  );
@@ -10036,7 +10152,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
10036
10152
  await removeTransactionFiles(journal);
10037
10153
  }
10038
10154
  function createArtifactBundleManifest(manifestPath, outputs, provenance) {
10039
- const root = import_node_path8.default.dirname(import_node_path8.default.resolve(manifestPath));
10155
+ const root = import_node_path9.default.dirname(import_node_path9.default.resolve(manifestPath));
10040
10156
  const sources = provenance.sources.map((source) => ({
10041
10157
  ...source,
10042
10158
  path: portableRelative(root, source.path)
@@ -10066,11 +10182,11 @@ function withArtifactManifest(manifestPath, outputs, provenance) {
10066
10182
  ];
10067
10183
  }
10068
10184
  async function writeManagedArtifactBundle(manifestPath, outputs, provenance, options = {}) {
10069
- const absoluteManifest = import_node_path8.default.resolve(manifestPath);
10185
+ const absoluteManifest = import_node_path9.default.resolve(manifestPath);
10070
10186
  const recoveryFile = `${absoluteManifest}.transaction`;
10071
10187
  const allowedDestinations = /* @__PURE__ */ new Set([
10072
10188
  absoluteManifest,
10073
- ...outputs.map((output) => import_node_path8.default.resolve(output.path))
10189
+ ...outputs.map((output) => import_node_path9.default.resolve(output.path))
10074
10190
  ]);
10075
10191
  await recoverArtifactTransaction(recoveryFile, allowedDestinations);
10076
10192
  const existingManifestBytes = await readOptional(absoluteManifest);
@@ -10088,13 +10204,13 @@ async function writeManagedArtifactBundle(manifestPath, outputs, provenance, opt
10088
10204
  }
10089
10205
  }
10090
10206
  if (!options.force) {
10091
- const root = import_node_path8.default.dirname(absoluteManifest);
10207
+ const root = import_node_path9.default.dirname(absoluteManifest);
10092
10208
  const recorded = new Map(
10093
- previous?.outputs.map((output) => [import_node_path8.default.resolve(root, output.path), output.sha256]) ?? []
10209
+ previous?.outputs.map((output) => [import_node_path9.default.resolve(root, output.path), output.sha256]) ?? []
10094
10210
  );
10095
10211
  const conflicts = [];
10096
10212
  for (const output of outputs) {
10097
- const destination = import_node_path8.default.resolve(output.path);
10213
+ const destination = import_node_path9.default.resolve(output.path);
10098
10214
  const current = await readOptional(destination);
10099
10215
  if (!current || digest(current) === digest(output.data)) continue;
10100
10216
  const expected = recorded.get(destination);
@@ -10116,15 +10232,15 @@ Pass { force: true } (CLI: --force) to take ownership and replace the complete b
10116
10232
  );
10117
10233
  }
10118
10234
  async function verifyArtifactBundle(manifestPath) {
10119
- const absolute = import_node_path8.default.resolve(manifestPath);
10235
+ const absolute = import_node_path9.default.resolve(manifestPath);
10120
10236
  const raw = await readArtifactBundleManifest(absolute);
10121
- const root = import_node_path8.default.dirname(absolute);
10237
+ const root = import_node_path9.default.dirname(absolute);
10122
10238
  const changedSources = [];
10123
10239
  const changedOutputs = [];
10124
10240
  for (const source of raw.sources) {
10125
10241
  let actual = null;
10126
10242
  try {
10127
- actual = digest(await (0, import_promises5.readFile)(import_node_path8.default.resolve(root, source.path)));
10243
+ actual = digest(await (0, import_promises5.readFile)(import_node_path9.default.resolve(root, source.path)));
10128
10244
  } catch (error) {
10129
10245
  if (!isCode(error, "ENOENT")) throw error;
10130
10246
  }
@@ -10132,7 +10248,7 @@ async function verifyArtifactBundle(manifestPath) {
10132
10248
  }
10133
10249
  for (const output of raw.outputs) {
10134
10250
  try {
10135
- if (digest(await (0, import_promises5.readFile)(import_node_path8.default.resolve(root, output.path))) !== output.sha256) {
10251
+ if (digest(await (0, import_promises5.readFile)(import_node_path9.default.resolve(root, output.path))) !== output.sha256) {
10136
10252
  changedOutputs.push(output.path);
10137
10253
  }
10138
10254
  } catch (error) {
@@ -10188,7 +10304,7 @@ async function writeArtifactBundle(files, options = {}) {
10188
10304
  if (!files.length) throw new Error("An artifact bundle must contain at least one file.");
10189
10305
  const normalized = files.map((file) => {
10190
10306
  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) };
10307
+ return { destination: import_node_path9.default.resolve(file.path), data: Buffer.from(file.data) };
10192
10308
  });
10193
10309
  const destinations = /* @__PURE__ */ new Set();
10194
10310
  for (const artifact of normalized) {
@@ -10216,23 +10332,23 @@ async function writeArtifactBundle(files, options = {}) {
10216
10332
  }
10217
10333
  }
10218
10334
  if (!changed.length) return { changed: [], unchanged };
10219
- const token = (0, import_node_crypto5.randomUUID)();
10335
+ const token = (0, import_node_crypto6.randomUUID)();
10220
10336
  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),
10337
+ const basename = import_node_path9.default.basename(artifact.destination);
10338
+ artifact.stage = import_node_path9.default.join(
10339
+ import_node_path9.default.dirname(artifact.destination),
10224
10340
  `.${basename}.pixelkiln-stage-${token}-${index}`
10225
10341
  );
10226
10342
  if (artifact.existed) {
10227
- artifact.backup = import_node_path8.default.join(
10228
- import_node_path8.default.dirname(artifact.destination),
10343
+ artifact.backup = import_node_path9.default.join(
10344
+ import_node_path9.default.dirname(artifact.destination),
10229
10345
  `.${basename}.pixelkiln-backup-${token}-${index}`
10230
10346
  );
10231
10347
  }
10232
10348
  }
10233
- const journal = options.recoveryFile ? import_node_path8.default.resolve(options.recoveryFile) : null;
10349
+ const journal = options.recoveryFile ? import_node_path9.default.resolve(options.recoveryFile) : null;
10234
10350
  if (journal) {
10235
- await (0, import_promises5.mkdir)(import_node_path8.default.dirname(journal), { recursive: true });
10351
+ await (0, import_promises5.mkdir)(import_node_path9.default.dirname(journal), { recursive: true });
10236
10352
  const transaction = {
10237
10353
  format: "pixelkiln-artifact-transaction",
10238
10354
  version: 1,
@@ -10257,7 +10373,7 @@ async function writeArtifactBundle(files, options = {}) {
10257
10373
  }
10258
10374
  try {
10259
10375
  for (const [index, artifact] of changed.entries()) {
10260
- await (0, import_promises5.mkdir)(import_node_path8.default.dirname(artifact.destination), { recursive: true });
10376
+ await (0, import_promises5.mkdir)(import_node_path9.default.dirname(artifact.destination), { recursive: true });
10261
10377
  await options.beforeStage?.(artifact.destination, index);
10262
10378
  await (0, import_promises5.writeFile)(artifact.stage, artifact.data, { flag: "wx" });
10263
10379
  }
@@ -10523,10 +10639,10 @@ function sameFrameGrid(first, next) {
10523
10639
  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
10640
  }
10525
10641
  async function refineAsset(options) {
10526
- const source = import_node_path9.default.resolve(options.source);
10527
- const output = import_node_path9.default.resolve(options.output);
10642
+ const source = import_node_path10.default.resolve(options.source);
10643
+ const output = import_node_path10.default.resolve(options.output);
10528
10644
  if (source === output) throw new Error("Refinement source and output must be different files.");
10529
- const record = import_node_path9.default.resolve(qualityRecordPath(output));
10645
+ const record = import_node_path10.default.resolve(qualityRecordPath(output));
10530
10646
  const palette = normalizePalette(options.palette).map((color) => color.hex);
10531
10647
  const minGridConfidence = requireConfidence(options.minGridConfidence ?? "high");
10532
10648
  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 +10665,8 @@ async function refineAsset(options) {
10549
10665
  );
10550
10666
  }
10551
10667
  }
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");
10668
+ const temp = await (0, import_promises6.mkdtemp)(import_node_path10.default.join((0, import_node_os3.tmpdir)(), "pixelkiln-refine-"));
10669
+ const recovered = import_node_path10.default.join(temp, "native.png");
10554
10670
  const fixerCommand = compatibilityCommand ?? options.fixerPython ?? process.env.PIXELKILN_PIXEL_FIXER_PYTHON ?? "python3";
10555
10671
  const args = compatibilityCommand ? [...options.fixerArgsPrefix ?? [], source, "--extract", recovered] : ["-c", PYTHON_FIXER_BRIDGE, source, recovered];
10556
10672
  try {
@@ -10635,18 +10751,18 @@ async function refineFrameSet(options) {
10635
10751
  if (roles.some((role) => !role.trim()) || new Set(roles).size !== roles.length) {
10636
10752
  throw new Error("Frame-set roles must be non-empty and unique.");
10637
10753
  }
10638
- const output = import_node_path9.default.resolve(options.output);
10639
- const record = import_node_path9.default.resolve(qualityRecordPath(output));
10754
+ const output = import_node_path10.default.resolve(options.output);
10755
+ const record = import_node_path10.default.resolve(qualityRecordPath(output));
10640
10756
  const palette = normalizeRefinementPalette(options.palette);
10641
10757
  const finalOutputs = options.sources.map((source, index) => qualityFrameOutputPath(output, source.role, index));
10642
10758
  if (new Set(finalOutputs).size !== finalOutputs.length) {
10643
10759
  throw new Error("Frame-set roles resolve to duplicate output filenames.");
10644
10760
  }
10645
- const sourceSet = new Set(options.sources.map((source) => import_node_path9.default.resolve(source.path)));
10761
+ const sourceSet = new Set(options.sources.map((source) => import_node_path10.default.resolve(source.path)));
10646
10762
  if (finalOutputs.some((file) => sourceSet.has(file))) {
10647
10763
  throw new Error("Frame-set refinement sources and outputs must be different files.");
10648
10764
  }
10649
- const temp = await (0, import_promises6.mkdtemp)(import_node_path9.default.join((0, import_node_os2.tmpdir)(), "pixelkiln-refine-frames-"));
10765
+ const temp = await (0, import_promises6.mkdtemp)(import_node_path10.default.join((0, import_node_os3.tmpdir)(), "pixelkiln-refine-frames-"));
10650
10766
  try {
10651
10767
  const frames = [];
10652
10768
  const outputs = [];
@@ -10654,8 +10770,8 @@ async function refineFrameSet(options) {
10654
10770
  const sourcePaths = [];
10655
10771
  let firstOptions = null;
10656
10772
  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`);
10773
+ const source = import_node_path10.default.resolve(sourceEntry.path);
10774
+ const temporaryOutput = import_node_path10.default.join(temp, `${String(index).padStart(2, "0")}.png`);
10659
10775
  const refined = await refineAsset({
10660
10776
  source,
10661
10777
  output: temporaryOutput,
@@ -10730,7 +10846,7 @@ async function refineFrameSet(options) {
10730
10846
  );
10731
10847
  return {
10732
10848
  sources: sourcePaths,
10733
- outputs: outputs.map((item) => import_node_path9.default.resolve(item.path)),
10849
+ outputs: outputs.map((item) => import_node_path10.default.resolve(item.path)),
10734
10850
  record,
10735
10851
  frames,
10736
10852
  palette,
@@ -10741,12 +10857,12 @@ async function refineFrameSet(options) {
10741
10857
  }
10742
10858
  }
10743
10859
  async function checkQualityRecord(recordPath) {
10744
- const record = import_node_path9.default.resolve(recordPath);
10860
+ const record = import_node_path10.default.resolve(recordPath);
10745
10861
  const manifest = await readArtifactBundleManifest(record);
10746
10862
  const options = parseRecordOptions(manifest, record);
10747
10863
  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));
10864
+ const sources = manifest.sources.map((source2) => import_node_path10.default.resolve(import_node_path10.default.dirname(record), source2.path));
10865
+ const outputs = manifest.outputs.map((output2) => import_node_path10.default.resolve(import_node_path10.default.dirname(record), output2.path));
10750
10866
  const source = sources[0] ?? "";
10751
10867
  const output = outputs[0] ?? "";
10752
10868
  const expectedCount = options.frameSet?.count ?? 1;
@@ -10779,7 +10895,7 @@ async function checkQualityRecord(recordPath) {
10779
10895
  async function approveQualityRecord(recordPath, approval) {
10780
10896
  const reviewer = approval.reviewer.trim();
10781
10897
  if (!reviewer) throw new Error("A human reviewer name is required.");
10782
- const record = import_node_path9.default.resolve(recordPath);
10898
+ const record = import_node_path10.default.resolve(recordPath);
10783
10899
  const manifest = await readArtifactBundleManifest(record);
10784
10900
  const options = parseRecordOptions(manifest, record);
10785
10901
  const verification = await verifyArtifactBundle(record);
@@ -10800,14 +10916,14 @@ async function approveQualityRecord(recordPath, approval) {
10800
10916
  `A refinement record must contain exactly ${expectedCount} source(s) and output(s).`
10801
10917
  );
10802
10918
  }
10803
- const root = import_node_path9.default.dirname(record);
10919
+ const root = import_node_path10.default.dirname(record);
10804
10920
  const outputs = await Promise.all(manifest.outputs.map(async (entry) => {
10805
- const outputPath = import_node_path9.default.resolve(root, entry.path);
10921
+ const outputPath = import_node_path10.default.resolve(root, entry.path);
10806
10922
  return { path: outputPath, data: await (0, import_promises6.readFile)(outputPath) };
10807
10923
  }));
10808
10924
  const sources = manifest.sources.map((source) => ({
10809
10925
  ...source,
10810
- path: import_node_path9.default.resolve(root, source.path)
10926
+ path: import_node_path10.default.resolve(root, source.path)
10811
10927
  }));
10812
10928
  const approved = {
10813
10929
  ...options,
@@ -10839,8 +10955,8 @@ async function qualitySource(spec, lock) {
10839
10955
  if (spec.generator === "frames") {
10840
10956
  return { items: null, reason: "frame-set quality requires ordered downloaded provider outputs" };
10841
10957
  }
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}` };
10958
+ const source = import_node_path11.default.resolve(spec.root, spec.source);
10959
+ return (0, import_node_fs8.existsSync)(source) ? { items: [{ role: "source", path: source }] } : { items: null, reason: `declared source is missing: ${spec.source}` };
10844
10960
  }
10845
10961
  const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
10846
10962
  if (!entry) return { items: null, reason: "raw provider output is not in the lockfile" };
@@ -10869,7 +10985,7 @@ async function qualitySource(spec, lock) {
10869
10985
  }
10870
10986
  if (role) roles.add(role);
10871
10987
  const source = currentEntryOutputPath(entry, spec, index);
10872
- if (!(0, import_node_fs7.existsSync)(source)) return { items: null, reason: `raw provider output is missing: ${source}` };
10988
+ if (!(0, import_node_fs8.existsSync)(source)) return { items: null, reason: `raw provider output is missing: ${source}` };
10873
10989
  if (await sha256File(source) !== output.sha256) {
10874
10990
  return { items: null, reason: "raw provider output was modified after download" };
10875
10991
  }
@@ -10888,7 +11004,7 @@ async function inspectQualityProfile(spec, lock) {
10888
11004
  }
10889
11005
  const sourcePaths = source.items.map((item) => item.path);
10890
11006
  const expectedOutputs = spec.generator === "frames" ? source.items.map((item, index) => qualityFrameOutputPath(output, item.role, index)) : [output];
10891
- if (!(0, import_node_fs7.existsSync)(record)) {
11007
+ if (!(0, import_node_fs8.existsSync)(record)) {
10892
11008
  return {
10893
11009
  key,
10894
11010
  state: "needs-refinement",
@@ -10906,7 +11022,7 @@ async function inspectQualityProfile(spec, lock) {
10906
11022
  const expectedRevision = spec.quality.fixerRevision ?? PIXEL_ART_FIXER_REVISION;
10907
11023
  const expectedTransparency = spec.quality.minTransparency ?? null;
10908
11024
  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);
11025
+ 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
11026
  if (contractChanged) {
10911
11027
  return {
10912
11028
  key,
@@ -11069,8 +11185,8 @@ async function requireApprovedQualitySources(specs, lock, assetIds, outputRoles)
11069
11185
  }
11070
11186
 
11071
11187
  // src/pipeline/revision.ts
11072
- var import_node_fs8 = require("fs");
11073
- var import_node_path11 = __toESM(require("path"), 1);
11188
+ var import_node_fs9 = require("fs");
11189
+ var import_node_path12 = __toESM(require("path"), 1);
11074
11190
  async function inspectRevisionReadiness(spec, lock) {
11075
11191
  if (spec.mirror) return inspectMirror(spec, lock, /* @__PURE__ */ new Set());
11076
11192
  if (spec.character?.kind === "outfit") return inspectOutfit(spec, lock, /* @__PURE__ */ new Set());
@@ -11086,7 +11202,7 @@ async function inspectStyleAnchor(spec, lock, seen) {
11086
11202
  const label = `style anchor ${spec.styleId}/${anchor.assetId}`;
11087
11203
  const dependency = await inspectParent(anchor.spec, lock, seen);
11088
11204
  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` };
11205
+ if (!anchor.sha256 || !(0, import_node_fs9.existsSync)(anchor.file)) return { ready: false, reason: `${label} has no generated south-facing file yet` };
11090
11206
  if (await sha256File(anchor.file) !== anchor.sha256) {
11091
11207
  return { ready: false, reason: `${label} changed after the manifest was resolved` };
11092
11208
  }
@@ -11100,7 +11216,7 @@ async function inspectScene(spec) {
11100
11216
  const files = [{ label: "scene image", file: scene.file, sha256: scene.sha256 }];
11101
11217
  if (scene.placement.type === "mask") files.push({ label: "scene mask", file: scene.placement.file, sha256: scene.placement.sha256 });
11102
11218
  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}` };
11219
+ if (!sha2562 || !(0, import_node_fs9.existsSync)(file)) return { ready: false, reason: `${label} does not exist yet: ${file}` };
11104
11220
  if (await sha256File(file) !== sha2562) return { ready: false, reason: `${label} changed after the manifest was resolved` };
11105
11221
  }
11106
11222
  return { ready: true, reason: "scene is current" };
@@ -11117,7 +11233,7 @@ async function inspectOutfit(spec, lock, seen) {
11117
11233
  const label = `outfit source ${spec.styleId}/${character.parentAssetId}`;
11118
11234
  const dependency = await inspectParent(character.parentSpec, lock, seen);
11119
11235
  if (!dependency.ready) return { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
11120
- if (!(0, import_node_fs8.existsSync)(outfit.reference.path)) {
11236
+ if (!(0, import_node_fs9.existsSync)(outfit.reference.path)) {
11121
11237
  return { ready: false, reason: `outfit reference is missing: ${outfit.reference.path}` };
11122
11238
  }
11123
11239
  if (await sha256File(outfit.reference.path) !== outfit.reference.sha256) {
@@ -11131,7 +11247,7 @@ async function inspectCharacterParent(spec, lock, seen) {
11131
11247
  const label = `${character.kind} parent ${spec.styleId}/${character.parentAssetId}`;
11132
11248
  const dependency = await inspectParent(parentSpec, lock, seen);
11133
11249
  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)) {
11250
+ if (!character.parentSha256 || !character.parentFile || !(0, import_node_fs9.existsSync)(character.parentFile)) {
11135
11251
  return { ready: false, reason: `${label} has no generated south-facing file yet` };
11136
11252
  }
11137
11253
  if (await sha256File(character.parentFile) !== character.parentSha256) {
@@ -11149,7 +11265,7 @@ async function inspectObjectProParent(spec, lock, seen) {
11149
11265
  const label = `${object.kind} parent ${spec.styleId}/${object.parentAssetId}`;
11150
11266
  const dependency = await inspectParent(parentSpec, lock, seen);
11151
11267
  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)) {
11268
+ if (!object.parentSha256 || !object.parentFile || !(0, import_node_fs9.existsSync)(object.parentFile)) {
11153
11269
  return { ready: false, reason: `${label} has no generated south-facing file yet` };
11154
11270
  }
11155
11271
  if (await sha256File(object.parentFile) !== object.parentSha256) {
@@ -11175,7 +11291,7 @@ async function inspectRevision(spec, lock, seen) {
11175
11291
  const members = await inspectSourceMembers(spec, lock);
11176
11292
  if (!members.ready) return members;
11177
11293
  } else {
11178
- if (!revision.sourceSha256 || !(0, import_node_fs8.existsSync)(revision.sourceFile)) {
11294
+ if (!revision.sourceSha256 || !(0, import_node_fs9.existsSync)(revision.sourceFile)) {
11179
11295
  return { ready: false, reason: `revision source is missing: ${revision.sourceFile}` };
11180
11296
  }
11181
11297
  if (await sha256File(revision.sourceFile) !== revision.sourceSha256) {
@@ -11183,7 +11299,7 @@ async function inspectRevision(spec, lock, seen) {
11183
11299
  }
11184
11300
  }
11185
11301
  if (revision.maskFile) {
11186
- if (!revision.maskSha256 || !(0, import_node_fs8.existsSync)(revision.maskFile)) {
11302
+ if (!revision.maskSha256 || !(0, import_node_fs9.existsSync)(revision.maskFile)) {
11187
11303
  return { ready: false, reason: `revision mask is missing: ${revision.maskFile}` };
11188
11304
  }
11189
11305
  if (await sha256File(revision.maskFile) !== revision.maskSha256) {
@@ -11191,7 +11307,7 @@ async function inspectRevision(spec, lock, seen) {
11191
11307
  }
11192
11308
  }
11193
11309
  if (revision.lastFrameFile) {
11194
- if (!revision.lastFrameSha256 || !(0, import_node_fs8.existsSync)(revision.lastFrameFile)) {
11310
+ if (!revision.lastFrameSha256 || !(0, import_node_fs9.existsSync)(revision.lastFrameFile)) {
11195
11311
  return { ready: false, reason: `revision last frame is missing: ${revision.lastFrameFile}` };
11196
11312
  }
11197
11313
  if (await sha256File(revision.lastFrameFile) !== revision.lastFrameSha256) {
@@ -11204,7 +11320,7 @@ async function inspectSourceMembers(spec, lock) {
11204
11320
  const revision = spec.revision;
11205
11321
  const members = revision.sourceMembers;
11206
11322
  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}` };
11323
+ if (!(0, import_node_fs9.existsSync)(member.file)) return { ready: false, reason: `revision source member is missing: ${member.file}` };
11208
11324
  if (await sha256File(member.file) !== member.sha256) {
11209
11325
  return { ready: false, reason: `revision source member changed after the manifest was resolved: ${member.file}` };
11210
11326
  }
@@ -11213,8 +11329,8 @@ async function inspectSourceMembers(spec, lock) {
11213
11329
  if (parent.source) return { ready: true, reason: "declared source set is present" };
11214
11330
  const entry = lock.entries[lockKey(parent.styleId, parent.assetId)];
11215
11331
  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();
11332
+ const recorded = entry.outputs.map((_, index) => import_node_path12.default.resolve(currentEntryOutputPath(entry, parent, index))).sort();
11333
+ const found = members.map((member) => import_node_path12.default.resolve(member.file)).sort();
11218
11334
  if (recorded.length !== found.length || recorded.some((file, index) => file !== found[index])) {
11219
11335
  return {
11220
11336
  ready: false,
@@ -11263,8 +11379,8 @@ async function inspectParent(spec, lock, seen) {
11263
11379
  };
11264
11380
  }
11265
11381
  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}` };
11382
+ const source = import_node_path12.default.resolve(spec.root, spec.source);
11383
+ return (0, import_node_fs9.existsSync)(source) ? { ready: true, reason: "declared source is present" } : { ready: false, reason: `declared source is missing: ${spec.source}` };
11268
11384
  }
11269
11385
  const entry = lock.entries[key];
11270
11386
  if (!entry) return { ready: false, reason: "parent is not in the lockfile" };
@@ -11278,7 +11394,7 @@ async function inspectParent(spec, lock, seen) {
11278
11394
  for (let index = 0; index < entry.outputs.length; index++) {
11279
11395
  const output = entry.outputs[index];
11280
11396
  const file = currentEntryOutputPath(entry, spec, index);
11281
- if (!(0, import_node_fs8.existsSync)(file)) return { ready: false, reason: `parent output is missing: ${file}` };
11397
+ if (!(0, import_node_fs9.existsSync)(file)) return { ready: false, reason: `parent output is missing: ${file}` };
11282
11398
  if (await sha256File(file) !== output.sha256) {
11283
11399
  return { ready: false, reason: `parent output was modified: ${file}` };
11284
11400
  }
@@ -11342,7 +11458,7 @@ async function buildPlan(specs, lock, opts = {}) {
11342
11458
  state = "missing";
11343
11459
  reason = "--force";
11344
11460
  } else if (!entry && spec.source && spec.generator !== "frames") {
11345
- if ((0, import_node_fs9.existsSync)(import_node_path12.default.resolve(spec.root, spec.source))) {
11461
+ if ((0, import_node_fs10.existsSync)(import_node_path13.default.resolve(spec.root, spec.source))) {
11346
11462
  state = "ok";
11347
11463
  reason = `placed from ${spec.source}; not generated`;
11348
11464
  } else {
@@ -11350,7 +11466,7 @@ async function buildPlan(specs, lock, opts = {}) {
11350
11466
  reason = `declared source is missing: ${spec.source}`;
11351
11467
  }
11352
11468
  } else if (!entry) {
11353
- if ((0, import_node_fs9.existsSync)(spec.outFile)) {
11469
+ if ((0, import_node_fs10.existsSync)(spec.outFile)) {
11354
11470
  state = "untracked";
11355
11471
  reason = "file on disk, no provenance recorded";
11356
11472
  } else {
@@ -11363,7 +11479,7 @@ async function buildPlan(specs, lock, opts = {}) {
11363
11479
  } else if (spec.mirror && !mirrorCurrent(entry, lock.entries[lockKey(spec.styleId, spec.mirror.sourceAssetId)])) {
11364
11480
  state = "stale";
11365
11481
  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)))) {
11482
+ } else if (spec.mirror && entry.status === "downloaded" && entry.outputs.some((_output, index) => !(0, import_node_fs10.existsSync)(currentEntryOutputPath(entry, spec, index)))) {
11367
11483
  state = "stale";
11368
11484
  reason = "mirror output missing on disk; flip it again (no generation cost)";
11369
11485
  } else if (spec.character?.kind === "outfit" && entry.outfit?.sourceSha256 !== mirrorSourceHash(lock.entries[lockKey(spec.styleId, spec.character.parentAssetId)] ?? { outputs: [] })) {
@@ -11391,9 +11507,9 @@ async function buildPlan(specs, lock, opts = {}) {
11391
11507
  } else if (entry.status !== "downloaded") {
11392
11508
  state = "in-flight";
11393
11509
  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)))) {
11510
+ } else if (entry.outputs.length === 0 || entry.outputs.some((_output, index) => !(0, import_node_fs10.existsSync)(currentEntryOutputPath(entry, spec, index)))) {
11395
11511
  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(", ")}`;
11512
+ 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
11513
  } else if (await anyOutputModified(entry, spec)) {
11398
11514
  state = "orphaned";
11399
11515
  reason = "output modified since download";
@@ -11484,13 +11600,13 @@ function summarize(plan) {
11484
11600
 
11485
11601
  // src/project.ts
11486
11602
  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);
11603
+ const absolute = import_node_path14.default.resolve(manifestPath);
11604
+ const root = import_node_path14.default.dirname(absolute);
11489
11605
  if (opts.env !== false) {
11490
11606
  loadEnvFiles(root);
11491
- if (import_node_path13.default.resolve(process.cwd()) !== root) loadEnvFiles(process.cwd());
11607
+ if (import_node_path14.default.resolve(process.cwd()) !== root) loadEnvFiles(process.cwd());
11492
11608
  }
11493
- const lockPath = import_node_path13.default.resolve(opts.lockPath ?? import_node_path13.default.join(root, "pixelkiln.lock.json"));
11609
+ const lockPath = import_node_path14.default.resolve(opts.lockPath ?? import_node_path14.default.join(root, "pixelkiln.lock.json"));
11494
11610
  const loaded = await loadManifest(absolute);
11495
11611
  const specs = await resolveSpecs(loaded, { styles: opts.styles ?? [], assets: opts.assets ?? [] });
11496
11612
  const lock = await loadLock(lockPath);
@@ -11557,8 +11673,8 @@ function shouldPersistSourceUrl(value) {
11557
11673
  }
11558
11674
 
11559
11675
  // src/recipes.ts
11560
- var import_node_path14 = __toESM(require("path"), 1);
11561
- var import_node_fs10 = require("fs");
11676
+ var import_node_path15 = __toESM(require("path"), 1);
11677
+ var import_node_fs11 = require("fs");
11562
11678
  var import_promises7 = require("fs/promises");
11563
11679
  var import_node_url = require("url");
11564
11680
  var import_zod4 = require("zod");
@@ -11568,7 +11684,7 @@ var VERSION_RE = /^\d+\.\d+\.\d+$/;
11568
11684
  var RECIPE_FILE = "pixelkiln.recipe.json";
11569
11685
  var RECIPE_DIR_TOKEN = "{{recipeDir}}";
11570
11686
  var PortablePathSchema = import_zod4.z.string().min(1).refine((value) => {
11571
- if (value.includes("\\") || import_node_path14.default.posix.isAbsolute(value)) return false;
11687
+ if (value.includes("\\") || import_node_path15.default.posix.isAbsolute(value)) return false;
11572
11688
  const parts = value.split("/");
11573
11689
  return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
11574
11690
  }, "expected a portable relative path without '.', '..', or backslashes");
@@ -11744,7 +11860,7 @@ function message3(error) {
11744
11860
  return error instanceof Error ? error.message : String(error);
11745
11861
  }
11746
11862
  async function readRecipeFile(recipePath, bundled) {
11747
- const absolute = import_node_path14.default.resolve(recipePath);
11863
+ const absolute = import_node_path15.default.resolve(recipePath);
11748
11864
  let raw;
11749
11865
  try {
11750
11866
  raw = JSON.parse(await (0, import_promises7.readFile)(absolute, "utf8"));
@@ -11758,13 +11874,13 @@ async function readRecipeFile(recipePath, bundled) {
11758
11874
  ` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
11759
11875
  );
11760
11876
  }
11761
- return { recipe: parsed.data, authored: raw, path: absolute, dir: import_node_path14.default.dirname(absolute), bundled };
11877
+ return { recipe: parsed.data, authored: raw, path: absolute, dir: import_node_path15.default.dirname(absolute), bundled };
11762
11878
  }
11763
11879
  async function walkRecipeFiles(root) {
11764
- if (!(0, import_node_fs10.existsSync)(root)) return [];
11880
+ if (!(0, import_node_fs11.existsSync)(root)) return [];
11765
11881
  const out = [];
11766
11882
  for (const entry of await (0, import_promises7.readdir)(root, { withFileTypes: true })) {
11767
- const resolved = import_node_path14.default.join(root, entry.name);
11883
+ const resolved = import_node_path15.default.join(root, entry.name);
11768
11884
  if (entry.isDirectory()) out.push(...await walkRecipeFiles(resolved));
11769
11885
  else if (entry.isFile() && entry.name === RECIPE_FILE) out.push(resolved);
11770
11886
  }
@@ -11775,7 +11891,7 @@ function bundledRecipeRoot() {
11775
11891
  }
11776
11892
  async function listBundledRecipes(root = bundledRecipeRoot()) {
11777
11893
  const recipes = await Promise.all(
11778
- (await walkRecipeFiles(import_node_path14.default.resolve(root))).map((file) => readRecipeFile(file, true))
11894
+ (await walkRecipeFiles(import_node_path15.default.resolve(root))).map((file) => readRecipeFile(file, true))
11779
11895
  );
11780
11896
  return recipes.sort(
11781
11897
  (a, b) => a.recipe.id.localeCompare(b.recipe.id) || compareVersions(b.recipe.version, a.recipe.version)
@@ -11798,9 +11914,9 @@ function parseSelector(selector) {
11798
11914
  return { id: match[1], version: match[2] };
11799
11915
  }
11800
11916
  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);
11917
+ const local = import_node_path15.default.resolve(target);
11918
+ if ((0, import_node_fs11.existsSync)(local)) {
11919
+ const recipePath = import_node_path15.default.basename(local) === RECIPE_FILE ? local : import_node_path15.default.join(local, RECIPE_FILE);
11804
11920
  return readRecipeFile(recipePath, false);
11805
11921
  }
11806
11922
  if (target.startsWith(".") || target.startsWith("/") || target.includes("\\")) {
@@ -11814,8 +11930,8 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
11814
11930
  return matches[0];
11815
11931
  }
11816
11932
  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)) {
11933
+ const absolute = import_node_path15.default.resolve(root, ...file.path.split("/"));
11934
+ if (!(0, import_node_fs11.existsSync)(absolute)) {
11819
11935
  return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
11820
11936
  }
11821
11937
  const actualSha256 = await sha256File(absolute);
@@ -11830,7 +11946,7 @@ async function verifyRecipe(target, options = {}) {
11830
11946
  const loaded = await resolveRecipe(target, options.bundledRoot);
11831
11947
  const actualIntegrity = recipeDigest(loaded.authored);
11832
11948
  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;
11949
+ const modelRoot = options.modelRoot ? import_node_path15.default.resolve(options.modelRoot) : null;
11834
11950
  const models = modelRoot ? await Promise.all(loaded.recipe.models.map(async (model) => ({
11835
11951
  ...await verifyFile(modelRoot, model),
11836
11952
  source: model.source,
@@ -11864,7 +11980,7 @@ async function verifyRecipe(target, options = {}) {
11864
11980
  };
11865
11981
  }
11866
11982
  function renderStyle(style, destination, cwd) {
11867
- const portableDir = import_node_path14.default.relative(cwd, destination).split(import_node_path14.default.sep).join("/") || ".";
11983
+ const portableDir = import_node_path15.default.relative(cwd, destination).split(import_node_path15.default.sep).join("/") || ".";
11868
11984
  const replace = (value) => {
11869
11985
  if (typeof value === "string") return value.replaceAll(RECIPE_DIR_TOKEN, portableDir);
11870
11986
  if (Array.isArray(value)) return value.map(replace);
@@ -11876,24 +11992,24 @@ function renderStyle(style, destination, cwd) {
11876
11992
  return replace(style);
11877
11993
  }
11878
11994
  async function installRecipe(target, options = {}) {
11879
- const cwd = import_node_path14.default.resolve(options.cwd ?? process.cwd());
11995
+ const cwd = import_node_path15.default.resolve(options.cwd ?? process.cwd());
11880
11996
  const loaded = await resolveRecipe(target, options.bundledRoot);
11881
11997
  const verification = await verifyRecipe(loaded.path);
11882
11998
  if (!verification.ok) {
11883
11999
  throw new Error(`Recipe ${loaded.recipe.id}@${loaded.recipe.version} failed integrity verification.`);
11884
12000
  }
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);
12001
+ 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
12002
  const sources = [
11887
12003
  { 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("/")) }))
12004
+ ...loaded.recipe.files.map((file) => ({ path: file.path, source: import_node_path15.default.join(loaded.dir, ...file.path.split("/")) }))
11889
12005
  ];
11890
12006
  const files = await Promise.all(sources.map(async (file) => ({
11891
- path: import_node_path14.default.join(destination, ...file.path.split("/")),
12007
+ path: import_node_path15.default.join(destination, ...file.path.split("/")),
11892
12008
  data: await (0, import_promises7.readFile)(file.source)
11893
12009
  })));
11894
12010
  if (!options.force) {
11895
12011
  for (const file of files) {
11896
- if (!(0, import_node_fs10.existsSync)(file.path)) continue;
12012
+ if (!(0, import_node_fs11.existsSync)(file.path)) continue;
11897
12013
  const current = await (0, import_promises7.readFile)(file.path);
11898
12014
  if (!current.equals(file.data)) {
11899
12015
  throw new OverwriteRefusedError(
@@ -11919,8 +12035,8 @@ async function installRecipe(target, options = {}) {
11919
12035
 
11920
12036
  // src/pipeline/audit.ts
11921
12037
  var import_promises8 = require("fs/promises");
11922
- var import_node_fs11 = require("fs");
11923
- var import_node_path15 = __toESM(require("path"), 1);
12038
+ var import_node_fs12 = require("fs");
12039
+ var import_node_path16 = __toESM(require("path"), 1);
11924
12040
  function paletteDistance(asset, reference) {
11925
12041
  if (!asset.length || !reference.length) return 0;
11926
12042
  let total = 0;
@@ -11956,7 +12072,7 @@ async function auditStyle(loaded, specs, styleId, lock) {
11956
12072
  const unreadable = [];
11957
12073
  for (const spec of mine) {
11958
12074
  for (const output of resolveSpecOutputs(spec, lock, loaded.root)) {
11959
- if (!(0, import_node_fs11.existsSync)(output.absolutePath)) {
12075
+ if (!(0, import_node_fs12.existsSync)(output.absolutePath)) {
11960
12076
  missing.push(output.id);
11961
12077
  continue;
11962
12078
  }
@@ -11985,8 +12101,8 @@ async function auditStyle(loaded, specs, styleId, lock) {
11985
12101
  let referenceFromStyleImages = false;
11986
12102
  const refPalettes = [];
11987
12103
  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;
12104
+ const abs = import_node_path16.default.resolve(loaded.root, img.path);
12105
+ if (!(0, import_node_fs12.existsSync)(abs)) continue;
11990
12106
  try {
11991
12107
  refPalettes.push(extractPalette(decodePng(await (0, import_promises8.readFile)(abs)), 12));
11992
12108
  } catch {
@@ -12064,19 +12180,19 @@ function hex(c) {
12064
12180
  }
12065
12181
 
12066
12182
  // src/pixellab-utils.ts
12067
- var import_node_fs12 = require("fs");
12183
+ var import_node_fs13 = require("fs");
12068
12184
  var import_promises9 = require("fs/promises");
12069
- var import_node_path16 = __toESM(require("path"), 1);
12185
+ var import_node_path17 = __toESM(require("path"), 1);
12070
12186
  var UNZOOM_MIN_SIDE = 256;
12071
12187
  var UNZOOM_MAX_AREA = 2048 * 2048;
12072
12188
  function defaultUnzoomOut(input) {
12073
- const ext = import_node_path16.default.extname(input);
12189
+ const ext = import_node_path17.default.extname(input);
12074
12190
  return `${ext ? input.slice(0, -ext.length) : input}.unzoomed.png`;
12075
12191
  }
12076
12192
  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`);
12193
+ const out = import_node_path17.default.resolve(opts.out ?? defaultUnzoomOut(input));
12194
+ if (import_node_path17.default.resolve(input) === out) throw new Error("unzoom will not overwrite its own input; pass a different --out");
12195
+ if ((0, import_node_fs13.existsSync)(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
12080
12196
  const bytes = await (0, import_promises9.readFile)(input);
12081
12197
  const metadata = imageMetadata(bytes);
12082
12198
  if (!metadata) throw new Error(`${input} is not a readable PNG or JPEG`);
@@ -12090,7 +12206,7 @@ async function unzoomFile(client2, input, opts = {}) {
12090
12206
  throw new Error(`${input} is ${width}x${height}; PixelLab's unzoom takes at most 2048x2048 worth of pixels`);
12091
12207
  }
12092
12208
  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 });
12209
+ await (0, import_promises9.mkdir)(import_node_path17.default.dirname(out), { recursive: true });
12094
12210
  await (0, import_promises9.writeFile)(out, res.png);
12095
12211
  return { out, original: res.originalSize, unzoomed: res.unzoomedSize, zoomFactor: res.zoomFactor, usage: res.usage };
12096
12212
  }
@@ -12098,14 +12214,14 @@ var FONT_WEIGHTS = ["Bold", "Regular"];
12098
12214
  var FONT_GLYPH_SIZES = [8, 16, 32, 64];
12099
12215
  var FONT_COST_GENERATIONS = 25;
12100
12216
  function fontOutputPaths(out) {
12101
- const base = import_node_path16.default.resolve(out.replace(/\.(ttf|png)$/i, ""));
12217
+ const base = import_node_path17.default.resolve(out.replace(/\.(ttf|png)$/i, ""));
12102
12218
  return { ttf: `${base}.ttf`, atlas: `${base}.png` };
12103
12219
  }
12104
12220
  async function generateFont(client2, opts) {
12105
12221
  const { ttf, atlas } = fontOutputPaths(opts.out);
12106
12222
  if (!opts.force) {
12107
12223
  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`);
12224
+ if ((0, import_node_fs13.existsSync)(file)) throw new Error(`${file} already exists; pass --force to replace it`);
12109
12225
  }
12110
12226
  }
12111
12227
  const sleep3 = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
@@ -12132,7 +12248,7 @@ async function generateFont(client2, opts) {
12132
12248
  client2.download(job.download_ttf_url),
12133
12249
  client2.download(job.download_atlas_url)
12134
12250
  ]);
12135
- await (0, import_promises9.mkdir)(import_node_path16.default.dirname(ttf), { recursive: true });
12251
+ await (0, import_promises9.mkdir)(import_node_path17.default.dirname(ttf), { recursive: true });
12136
12252
  await (0, import_promises9.writeFile)(ttf, ttfBytes);
12137
12253
  await (0, import_promises9.writeFile)(atlas, atlasBytes);
12138
12254
  return { jobId, ttf, atlas, glyphPx: job.glyph_px ?? null, usage: job.usage };
@@ -12147,15 +12263,15 @@ async function generateFont(client2, opts) {
12147
12263
  }
12148
12264
 
12149
12265
  // src/pipeline/quality-regression.ts
12150
- var import_node_path17 = __toESM(require("path"), 1);
12151
- var import_node_fs13 = require("fs");
12266
+ var import_node_path18 = __toESM(require("path"), 1);
12267
+ var import_node_fs14 = require("fs");
12152
12268
  var import_promises10 = require("fs/promises");
12153
12269
  var import_zod5 = require("zod");
12154
12270
  var SHA256_RE2 = /^[0-9a-f]{64}$/;
12155
12271
  var HEX_RE = /^#[0-9a-f]{6}$/;
12156
12272
  var MAX_REDMEAN_DISTANCE = 765;
12157
12273
  var QualityPathSchema = import_zod5.z.string().min(1).refine(
12158
- (value) => !import_node_path17.default.posix.isAbsolute(value) && !value.includes("\\"),
12274
+ (value) => !import_node_path18.default.posix.isAbsolute(value) && !value.includes("\\"),
12159
12275
  "expected a portable relative path"
12160
12276
  );
12161
12277
  var DEFAULT_QUALITY_TOLERANCES = {
@@ -12302,7 +12418,7 @@ function measureDecodedImageQuality(png, digest2) {
12302
12418
  };
12303
12419
  }
12304
12420
  async function measureImageQuality(file) {
12305
- const absolute = import_node_path17.default.resolve(file);
12421
+ const absolute = import_node_path18.default.resolve(file);
12306
12422
  let bytes;
12307
12423
  try {
12308
12424
  bytes = await (0, import_promises10.readFile)(absolute);
@@ -12324,11 +12440,11 @@ function resolveQualityInputs(raw, inputsFilePath) {
12324
12440
  }
12325
12441
  const ids = parsed.data.map((entry) => entry.id);
12326
12442
  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));
12443
+ const root = import_node_path18.default.dirname(import_node_path18.default.resolve(inputsFilePath));
12328
12444
  return parsed.data.map((entry) => ({
12329
12445
  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) } : {},
12446
+ file: import_node_path18.default.resolve(root, entry.path),
12447
+ ...entry.record ? { record: import_node_path18.default.resolve(root, entry.record) } : {},
12332
12448
  tolerances: QualityToleranceSchema.parse({
12333
12449
  ...DEFAULT_QUALITY_TOLERANCES,
12334
12450
  ...entry.tolerances
@@ -12336,7 +12452,7 @@ function resolveQualityInputs(raw, inputsFilePath) {
12336
12452
  }));
12337
12453
  }
12338
12454
  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("/") || ".";
12455
+ return import_node_path18.default.relative(from, import_node_path18.default.resolve(to)).split(import_node_path18.default.sep).join("/") || ".";
12340
12456
  }
12341
12457
  async function verifyRecordForImage(recordPath, imagePath) {
12342
12458
  const verification = await verifyArtifactBundle(recordPath);
@@ -12351,20 +12467,20 @@ async function verifyRecordForImage(recordPath, imagePath) {
12351
12467
  if (record.kind !== "refine" || options === null || typeof options !== "object" || options.schema !== "pixelkiln-quality") {
12352
12468
  throw new Error(`Quality record ${recordPath} is not a PixelKiln refinement record.`);
12353
12469
  }
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))) {
12470
+ const outputs = record.outputs.map((output) => import_node_path18.default.resolve(import_node_path18.default.dirname(recordPath), output.path));
12471
+ if (!outputs.includes(import_node_path18.default.resolve(imagePath))) {
12356
12472
  throw new Error(`Quality record ${recordPath} does not own ${imagePath}.`);
12357
12473
  }
12358
12474
  }
12359
12475
  async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
12360
12476
  if (!inputs.length) throw new Error("Cannot snapshot an empty quality baseline.");
12361
- const absolute = import_node_path17.default.resolve(baselinePath);
12477
+ const absolute = import_node_path18.default.resolve(baselinePath);
12362
12478
  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) {
12479
+ if (import_node_path18.default.resolve(input.file) === absolute || input.record && import_node_path18.default.resolve(input.record) === absolute) {
12364
12480
  throw new Error(`Quality baseline output would overwrite an input: ${absolute}.`);
12365
12481
  }
12366
12482
  }
12367
- const root = import_node_path17.default.dirname(absolute);
12483
+ const root = import_node_path18.default.dirname(absolute);
12368
12484
  const cases = [];
12369
12485
  for (const input of [...inputs].sort((a, b) => a.id.localeCompare(b.id))) {
12370
12486
  const expected = await measureImageQuality(input.file);
@@ -12391,7 +12507,7 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
12391
12507
  cases
12392
12508
  });
12393
12509
  const data = Buffer.from(JSON.stringify(baseline, null, 2) + "\n");
12394
- if ((0, import_node_fs13.existsSync)(absolute) && !options.force) {
12510
+ if ((0, import_node_fs14.existsSync)(absolute) && !options.force) {
12395
12511
  const current = await (0, import_promises10.readFile)(absolute);
12396
12512
  if (!current.equals(data)) {
12397
12513
  throw new OverwriteRefusedError(
@@ -12404,7 +12520,7 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
12404
12520
  return { baseline, path: absolute, changed: result.changed.length > 0 };
12405
12521
  }
12406
12522
  async function readQualityBaseline(baselinePath) {
12407
- const absolute = import_node_path17.default.resolve(baselinePath);
12523
+ const absolute = import_node_path18.default.resolve(baselinePath);
12408
12524
  let raw;
12409
12525
  try {
12410
12526
  raw = JSON.parse(await (0, import_promises10.readFile)(absolute, "utf8"));
@@ -12477,12 +12593,12 @@ function regressionViolations(expected, actual, tolerances) {
12477
12593
  return { violations, warnings };
12478
12594
  }
12479
12595
  async function checkQualityBaseline(baselinePath) {
12480
- const absolute = import_node_path17.default.resolve(baselinePath);
12596
+ const absolute = import_node_path18.default.resolve(baselinePath);
12481
12597
  const baseline = await readQualityBaseline(absolute);
12482
- const root = import_node_path17.default.dirname(absolute);
12598
+ const root = import_node_path18.default.dirname(absolute);
12483
12599
  const cases = [];
12484
12600
  for (const entry of baseline.cases) {
12485
- const file = import_node_path17.default.resolve(root, entry.file);
12601
+ const file = import_node_path18.default.resolve(root, entry.file);
12486
12602
  let actual = null;
12487
12603
  const violations = [];
12488
12604
  const warnings = [];
@@ -12495,7 +12611,7 @@ async function checkQualityBaseline(baselinePath) {
12495
12611
  violations.push(message4(error));
12496
12612
  }
12497
12613
  if (entry.record) {
12498
- const recordPath = import_node_path17.default.resolve(root, entry.record.path);
12614
+ const recordPath = import_node_path18.default.resolve(root, entry.record.path);
12499
12615
  try {
12500
12616
  const recordHash = await sha256File(recordPath);
12501
12617
  if (recordHash !== entry.record.sha256) violations.push("quality record hash changed");
@@ -12531,14 +12647,14 @@ async function checkQualityBaseline(baselinePath) {
12531
12647
  }
12532
12648
 
12533
12649
  // src/pipeline/cache-health.ts
12534
- var import_node_fs17 = require("fs");
12650
+ var import_node_fs18 = require("fs");
12535
12651
  var import_promises13 = require("fs/promises");
12536
- var import_node_path21 = __toESM(require("path"), 1);
12652
+ var import_node_path22 = __toESM(require("path"), 1);
12537
12653
 
12538
12654
  // src/cache.ts
12539
12655
  var import_promises11 = require("fs/promises");
12540
- var import_node_fs14 = require("fs");
12541
- var import_node_path18 = __toESM(require("path"), 1);
12656
+ var import_node_fs15 = require("fs");
12657
+ var import_node_path19 = __toESM(require("path"), 1);
12542
12658
  var import_zod6 = require("zod");
12543
12659
  var HashCacheSchema = import_zod6.z.object({
12544
12660
  version: import_zod6.z.literal(1),
@@ -12552,10 +12668,10 @@ function isSha256Hash(value) {
12552
12668
  function parseCache(value) {
12553
12669
  return HashCacheSchema.parse(value);
12554
12670
  }
12555
- async function loadCache(path37) {
12556
- if (!(0, import_node_fs14.existsSync)(path37)) return { version: 1, hashes: {} };
12671
+ async function loadCache(path38) {
12672
+ if (!(0, import_node_fs15.existsSync)(path38)) return { version: 1, hashes: {} };
12557
12673
  try {
12558
- const parsed = HashCacheSchema.safeParse(JSON.parse(await (0, import_promises11.readFile)(path37, "utf8")));
12674
+ const parsed = HashCacheSchema.safeParse(JSON.parse(await (0, import_promises11.readFile)(path38, "utf8")));
12559
12675
  if (!parsed.success) return { version: 1, hashes: {} };
12560
12676
  return {
12561
12677
  version: 1,
@@ -12567,18 +12683,18 @@ async function loadCache(path37) {
12567
12683
  return { version: 1, hashes: {} };
12568
12684
  }
12569
12685
  }
12570
- async function saveCache(path37, cache) {
12686
+ async function saveCache(path38, cache) {
12571
12687
  const sorted = {};
12572
12688
  for (const key of Object.keys(cache.hashes).sort()) {
12573
12689
  const hash = cache.hashes[key];
12574
12690
  if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
12575
12691
  sorted[key] = hash;
12576
12692
  }
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`;
12693
+ await (0, import_promises11.mkdir)(import_node_path19.default.dirname(import_node_path19.default.resolve(path38)), { recursive: true });
12694
+ const tmp = `${path38}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
12579
12695
  try {
12580
12696
  await (0, import_promises11.writeFile)(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
12581
- await (0, import_promises11.rename)(tmp, path37);
12697
+ await (0, import_promises11.rename)(tmp, path38);
12582
12698
  } finally {
12583
12699
  await (0, import_promises11.rm)(tmp, { force: true });
12584
12700
  }
@@ -12602,20 +12718,20 @@ function cachePathFor(lockPath) {
12602
12718
  }
12603
12719
 
12604
12720
  // src/pipeline/history.ts
12605
- var import_node_fs16 = require("fs");
12606
- var import_node_path20 = __toESM(require("path"), 1);
12721
+ var import_node_fs17 = require("fs");
12722
+ var import_node_path21 = __toESM(require("path"), 1);
12607
12723
 
12608
12724
  // src/pipeline/fetch.ts
12609
- var import_node_fs15 = require("fs");
12725
+ var import_node_fs16 = require("fs");
12610
12726
  var import_promises12 = require("fs/promises");
12611
- var import_node_path19 = __toESM(require("path"), 1);
12727
+ var import_node_path20 = __toESM(require("path"), 1);
12612
12728
  async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12613
12729
  const log = opts.onProgress ?? (() => {
12614
12730
  });
12615
12731
  const result = { downloaded: 0, skipped: 0, failed: 0, ...opts.refresh ? { unchanged: 0 } : {} };
12616
12732
  const specByKey = new Map(specs.map((s) => [lockKey(s.styleId, s.assetId), s]));
12617
12733
  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"));
12734
+ 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
12735
  const pending = Object.entries(lock.entries).filter(([key, e]) => {
12620
12736
  if (e.provider !== provider.id || !specByKey.has(key)) return false;
12621
12737
  if (specByKey.get(key)?.mirror) return false;
@@ -12626,15 +12742,15 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12626
12742
  if (spec && e.specHash === spec.specHash && e.outputs.length && !postprocessCurrent(e, spec)) {
12627
12743
  return Boolean(opts.force) || e.outputs.every((output) => {
12628
12744
  const file = resolveOutputPath(output.path, spec.root);
12629
- return (0, import_node_fs15.existsSync)(file) && sha256((0, import_node_fs15.readFileSync)(file)) === output.sha256;
12745
+ return (0, import_node_fs16.existsSync)(file) && sha256((0, import_node_fs16.readFileSync)(file)) === output.sha256;
12630
12746
  });
12631
12747
  }
12632
12748
  if (!opts.repair) return false;
12633
12749
  if (e.outputs.length === 0 || !spec) return true;
12634
12750
  return e.outputs.some((output) => {
12635
12751
  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));
12752
+ if (!(0, import_node_fs16.existsSync)(file)) return true;
12753
+ const current = sha256((0, import_node_fs16.readFileSync)(file));
12638
12754
  if (current === output.sha256) return false;
12639
12755
  return Boolean(opts.force) || (e.supersededOutputs ?? []).some((old) => old.sha256 === current);
12640
12756
  });
@@ -12717,14 +12833,14 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12717
12833
  if (!source.url) throw new Error(`no durable source URL to refresh ${source.role ?? "asset"} from`);
12718
12834
  fresh = await provider.download(source.url);
12719
12835
  if (recorded && sha256(fresh) === recordedRaw && !reapply) {
12720
- log(` current ${import_node_path19.default.relative(process.cwd(), target)}`);
12836
+ log(` current ${import_node_path20.default.relative(process.cwd(), target)}`);
12721
12837
  outputs.push({ ...recorded, path: portableOutputPath(target, spec.root) });
12722
12838
  continue;
12723
12839
  }
12724
12840
  }
12725
12841
  let raw = null;
12726
12842
  let final = null;
12727
- if ((0, import_node_fs15.existsSync)(target)) {
12843
+ if ((0, import_node_fs16.existsSync)(target)) {
12728
12844
  const currentHash = await sha256File(target);
12729
12845
  if (recorded && currentHash === recorded.sha256) {
12730
12846
  if (!fresh && !reapply) {
@@ -12741,9 +12857,9 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12741
12857
  }
12742
12858
  if (reapply && !fresh) {
12743
12859
  if (!recorded.raw) raw = await (0, import_promises12.readFile)(target);
12744
- log(` reapply ${import_node_path19.default.relative(process.cwd(), target)}`);
12860
+ log(` reapply ${import_node_path20.default.relative(process.cwd(), target)}`);
12745
12861
  } else {
12746
- log(` changed ${import_node_path19.default.relative(process.cwd(), target)} (upstream)`);
12862
+ log(` changed ${import_node_path20.default.relative(process.cwd(), target)} (upstream)`);
12747
12863
  }
12748
12864
  } else {
12749
12865
  const superseded = (entry.supersededOutputs ?? []).find(
@@ -12771,7 +12887,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12771
12887
  }
12772
12888
  }
12773
12889
  log(
12774
- ` replace ${import_node_path19.default.relative(process.cwd(), target)}` + (opts.force ? " (--force)" : " (previous tracked generation)")
12890
+ ` replace ${import_node_path20.default.relative(process.cwd(), target)}` + (opts.force ? " (--force)" : " (previous tracked generation)")
12775
12891
  );
12776
12892
  }
12777
12893
  }
@@ -12780,7 +12896,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12780
12896
  } else if (!raw && recorded && cacheDir) {
12781
12897
  if (!reapply) final = await readCachedMedia(cacheDir, recorded.sha256, expectedMediaType);
12782
12898
  if (!final && recordedRaw) raw = await readCachedMedia(cacheDir, recordedRaw, expectedMediaType);
12783
- if (final || raw) log(` cached ${import_node_path19.default.relative(process.cwd(), target)}`);
12899
+ if (final || raw) log(` cached ${import_node_path20.default.relative(process.cwd(), target)}`);
12784
12900
  }
12785
12901
  if (!final && !raw) {
12786
12902
  if (!source.url) {
@@ -12807,11 +12923,11 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12807
12923
  buf = processed.bytes;
12808
12924
  if (processed.changed) {
12809
12925
  rawHash = sha256(raw);
12810
- log(` snapped ${import_node_path19.default.relative(process.cwd(), target)} to ${wanted.palette.colors.length} colours`);
12926
+ log(` snapped ${import_node_path20.default.relative(process.cwd(), target)} to ${wanted.palette.colors.length} colours`);
12811
12927
  }
12812
12928
  }
12813
12929
  if (cacheDir) await cacheMedia(cacheDir, buf, mediaType);
12814
- await (0, import_promises12.mkdir)(import_node_path19.default.dirname(target), { recursive: true });
12930
+ await (0, import_promises12.mkdir)(import_node_path20.default.dirname(target), { recursive: true });
12815
12931
  const tmp = `${target}.pixelkiln.tmp`;
12816
12932
  await (0, import_promises12.writeFile)(tmp, buf);
12817
12933
  outputs.push({
@@ -12825,7 +12941,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12825
12941
  await saveLock(lockPath, lock);
12826
12942
  await (0, import_promises12.rename)(tmp, target);
12827
12943
  wrote = true;
12828
- log(` wrote ${import_node_path19.default.relative(process.cwd(), target)}`);
12944
+ log(` wrote ${import_node_path20.default.relative(process.cwd(), target)}`);
12829
12945
  }
12830
12946
  if (opts.refresh && !wrote) {
12831
12947
  if (rotated) {
@@ -12869,8 +12985,8 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
12869
12985
  return result;
12870
12986
  }
12871
12987
  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;
12988
+ const file = import_node_path20.default.join(cacheDir, cacheFileName(hash, mediaType));
12989
+ if (!(0, import_node_fs16.existsSync)(file)) return null;
12874
12990
  try {
12875
12991
  const buf = await (0, import_promises12.readFile)(file);
12876
12992
  if (sha256(buf) !== hash) return null;
@@ -12883,7 +12999,7 @@ async function readCachedMedia(cacheDir, hash, mediaType) {
12883
12999
  async function cacheMedia(cacheDir, buf, mediaType, knownHash) {
12884
13000
  const hash = knownHash ?? sha256(buf);
12885
13001
  validateMedia(buf, mediaType);
12886
- const file = import_node_path19.default.join(cacheDir, cacheFileName(hash, mediaType));
13002
+ const file = import_node_path20.default.join(cacheDir, cacheFileName(hash, mediaType));
12887
13003
  if (await readCachedMedia(cacheDir, hash, mediaType)) return;
12888
13004
  await (0, import_promises12.mkdir)(cacheDir, { recursive: true });
12889
13005
  const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
@@ -12891,7 +13007,7 @@ async function cacheMedia(cacheDir, buf, mediaType, knownHash) {
12891
13007
  try {
12892
13008
  await (0, import_promises12.rename)(tmp, file);
12893
13009
  } catch (err) {
12894
- if (!(0, import_node_fs15.existsSync)(file)) throw err;
13010
+ if (!(0, import_node_fs16.existsSync)(file)) throw err;
12895
13011
  } finally {
12896
13012
  await (0, import_promises12.rm)(tmp, { force: true });
12897
13013
  }
@@ -13010,10 +13126,10 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
13010
13126
  }
13011
13127
  for (let i = 0; i < entry.outputs.length; i++) {
13012
13128
  const file = currentEntryOutputPath(entry, spec, i);
13013
- if (!(0, import_node_fs16.existsSync)(file)) continue;
13129
+ if (!(0, import_node_fs17.existsSync)(file)) continue;
13014
13130
  if (await sha256File(file) !== entry.outputs[i].sha256 && !opts.force) {
13015
13131
  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`,
13132
+ `${import_node_path21.default.relative(process.cwd(), file)} was changed after download; keep it with pixelkiln edit, or pass --force to replace it`,
13017
13133
  [file]
13018
13134
  );
13019
13135
  }
@@ -13065,11 +13181,11 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
13065
13181
 
13066
13182
  // src/pipeline/cache-health.ts
13067
13183
  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)}`);
13184
+ if (options.prune && !(0, import_node_fs18.existsSync)(lockPath)) {
13185
+ throw new Error(`Refusing to prune without an existing lockfile at ${import_node_path22.default.resolve(lockPath)}`);
13070
13186
  }
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));
13187
+ const contentDir = import_node_path22.default.resolve(import_node_path22.default.dirname(lockPath), ".pixelkiln", "cache");
13188
+ const remotePath = import_node_path22.default.resolve(cachePathFor(lockPath));
13073
13189
  const referenced = referencedHashes(lock);
13074
13190
  let content = await inspectContentCache(contentDir, referenced);
13075
13191
  let remoteHashes = await inspectRemoteHashCache(remotePath);
@@ -13081,7 +13197,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
13081
13197
  ]);
13082
13198
  for (const name of names) {
13083
13199
  try {
13084
- await (0, import_promises13.rm)(import_node_path21.default.join(contentDir, name), { force: true });
13200
+ await (0, import_promises13.rm)(import_node_path22.default.join(contentDir, name), { force: true });
13085
13201
  removed.contentFiles++;
13086
13202
  } catch {
13087
13203
  }
@@ -13108,7 +13224,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
13108
13224
  async function inspectContentCache(contentDir, referenced) {
13109
13225
  const report = {
13110
13226
  path: contentDir,
13111
- exists: (0, import_node_fs17.existsSync)(contentDir),
13227
+ exists: (0, import_node_fs18.existsSync)(contentDir),
13112
13228
  files: 0,
13113
13229
  bytes: 0,
13114
13230
  valid: 0,
@@ -13129,7 +13245,7 @@ async function inspectContentCache(contentDir, referenced) {
13129
13245
  continue;
13130
13246
  }
13131
13247
  report.files++;
13132
- const file = import_node_path21.default.join(contentDir, entry.name);
13248
+ const file = import_node_path22.default.join(contentDir, entry.name);
13133
13249
  const mediaType = mediaTypeFromExtension(entry.name);
13134
13250
  const expected = mediaType ? entry.name.slice(0, -4) : "";
13135
13251
  let bytes;
@@ -13172,7 +13288,7 @@ async function inspectContentCache(contentDir, referenced) {
13172
13288
  async function inspectRemoteHashCache(remotePath) {
13173
13289
  const report = {
13174
13290
  path: remotePath,
13175
- exists: (0, import_node_fs17.existsSync)(remotePath),
13291
+ exists: (0, import_node_fs18.existsSync)(remotePath),
13176
13292
  entries: 0,
13177
13293
  valid: 0,
13178
13294
  invalidIds: [],
@@ -13196,20 +13312,20 @@ async function inspectRemoteHashCache(remotePath) {
13196
13312
  }
13197
13313
 
13198
13314
  // src/pipeline/pack.ts
13199
- var import_node_fs18 = require("fs");
13200
- var import_node_path22 = __toESM(require("path"), 1);
13315
+ var import_node_fs19 = require("fs");
13316
+ var import_node_path23 = __toESM(require("path"), 1);
13201
13317
  var DEFAULT_FRAME_SET_FPS = 12;
13202
13318
  function resolvePackInputs(raw, inputsFilePath) {
13203
13319
  if (!Array.isArray(raw) || !raw.length) {
13204
13320
  throw new Error("--inputs must be a non-empty JSON array of { id, path }");
13205
13321
  }
13206
- const baseDir = import_node_path22.default.dirname(import_node_path22.default.resolve(inputsFilePath));
13322
+ const baseDir = import_node_path23.default.dirname(import_node_path23.default.resolve(inputsFilePath));
13207
13323
  return raw.map((entry, i) => {
13208
13324
  const e = entry;
13209
13325
  if (typeof e.id !== "string" || typeof e.path !== "string") {
13210
13326
  throw new Error(`--inputs[${i}] needs string "id" and "path"`);
13211
13327
  }
13212
- return { id: e.id, path: import_node_path22.default.resolve(baseDir, e.path) };
13328
+ return { id: e.id, path: import_node_path23.default.resolve(baseDir, e.path) };
13213
13329
  });
13214
13330
  }
13215
13331
  function packSprites(inputs, options = {}) {
@@ -13230,7 +13346,7 @@ function packSprites(inputs, options = {}) {
13230
13346
  for (const input of ordered) {
13231
13347
  let sourceHash = null;
13232
13348
  try {
13233
- const bytes = (0, import_node_fs18.readFileSync)(input.path);
13349
+ const bytes = (0, import_node_fs19.readFileSync)(input.path);
13234
13350
  sourceHash = sha256(bytes);
13235
13351
  const png = decodePng(bytes);
13236
13352
  sprites.push({ id: input.id, width: png.width, height: png.height, pixels: png.pixels });
@@ -13293,7 +13409,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
13293
13409
  }
13294
13410
  const inputs2 = Object.entries(options.sourceOverrides).map(([id, source]) => ({
13295
13411
  id,
13296
- path: import_node_path22.default.resolve(source)
13412
+ path: import_node_path23.default.resolve(source)
13297
13413
  }));
13298
13414
  if (!inputs2.length) throw new Error(`No approved quality outputs for style "${styleId}".`);
13299
13415
  const packed2 = packSprites(inputs2, options);
@@ -13317,7 +13433,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
13317
13433
  let outputs = resolveEntryOutputs(entry, id, manifestDir);
13318
13434
  const source = sources[id];
13319
13435
  if (source && outputs.length <= 1) {
13320
- inputs.push({ id, path: import_node_path22.default.resolve(manifestDir, source) });
13436
+ inputs.push({ id, path: import_node_path23.default.resolve(manifestDir, source) });
13321
13437
  continue;
13322
13438
  }
13323
13439
  if (source && sourceIsStem(source, entry, manifestDir)) {
@@ -13351,7 +13467,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
13351
13467
  }
13352
13468
  }
13353
13469
  for (const [id, source] of Object.entries(sources)) {
13354
- if (!locked.has(id)) inputs.push({ id, path: import_node_path22.default.resolve(manifestDir, source) });
13470
+ if (!locked.has(id)) inputs.push({ id, path: import_node_path23.default.resolve(manifestDir, source) });
13355
13471
  }
13356
13472
  if (!inputs.length) {
13357
13473
  throw new Error(`No readable sprites for style "${styleId}"; ${noOutput.length} skipped.`);
@@ -13388,7 +13504,7 @@ function mountSprites(placements, options) {
13388
13504
  for (const p of placements) {
13389
13505
  let sourceHash = null;
13390
13506
  try {
13391
- const bytes = (0, import_node_fs18.readFileSync)(p.path);
13507
+ const bytes = (0, import_node_fs19.readFileSync)(p.path);
13392
13508
  sourceHash = sha256(bytes);
13393
13509
  const png = decodePng(bytes);
13394
13510
  if (png.width > cellWidth || png.height > cellHeight) {
@@ -13459,7 +13575,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
13459
13575
  const source = sources[id];
13460
13576
  const entry = lock.entries[`${styleId}/${id}`];
13461
13577
  if (source && !(entry && sourceIsStem(source, entry, manifestDir))) {
13462
- placements.push({ id, path: import_node_path22.default.resolve(manifestDir, source), cell });
13578
+ placements.push({ id, path: import_node_path23.default.resolve(manifestDir, source), cell });
13463
13579
  continue;
13464
13580
  }
13465
13581
  if (source && entry) {
@@ -13492,7 +13608,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
13492
13608
  `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
13609
  );
13494
13610
  }
13495
- const basePng = mount.base ? (0, import_node_fs18.readFileSync)(import_node_path22.default.resolve(manifestDir, mount.base)) : void 0;
13611
+ const basePng = mount.base ? (0, import_node_fs19.readFileSync)(import_node_path23.default.resolve(manifestDir, mount.base)) : void 0;
13496
13612
  const mounted = mountSprites(placements, {
13497
13613
  cellWidth: mount.cellWidth,
13498
13614
  cellHeight: mount.cellHeight,
@@ -13502,7 +13618,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
13502
13618
  ...mounted,
13503
13619
  sources: mount.base ? [{
13504
13620
  id: "$base",
13505
- path: import_node_path22.default.resolve(manifestDir, mount.base),
13621
+ path: import_node_path23.default.resolve(manifestDir, mount.base),
13506
13622
  sha256: sha256(basePng),
13507
13623
  included: true
13508
13624
  }, ...mounted.sources] : mounted.sources,
@@ -14312,9 +14428,9 @@ async function poll(provider, lock, lockPath, opts = {}) {
14312
14428
  }
14313
14429
 
14314
14430
  // src/pipeline/doctor.ts
14315
- var import_node_fs19 = require("fs");
14431
+ var import_node_fs20 = require("fs");
14316
14432
  var import_promises14 = require("fs/promises");
14317
- var import_node_path23 = __toESM(require("path"), 1);
14433
+ var import_node_path24 = __toESM(require("path"), 1);
14318
14434
  async function doctor(loaded, specs, lock, lockPath, opts = {}) {
14319
14435
  const checks = [];
14320
14436
  const add = (id, level, message6) => checks.push({ id, level, message: message6 });
@@ -14325,14 +14441,14 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
14325
14441
  );
14326
14442
  add("lock", "ok", `${Object.keys(lock.entries).length} valid v2 lock entr(ies)`);
14327
14443
  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)] : []
14444
+ import_node_path24.default.dirname(spec.outFile),
14445
+ ...spec.quality ? [import_node_path24.default.dirname(spec.quality.outFile)] : []
14330
14446
  ]))];
14331
14447
  const unwritable = [];
14332
14448
  for (const dir of dirs) {
14333
14449
  const ancestor = await nearestExistingDirectory(dir);
14334
14450
  try {
14335
- await (0, import_promises14.access)(ancestor, import_node_fs19.constants.W_OK);
14451
+ await (0, import_promises14.access)(ancestor, import_node_fs20.constants.W_OK);
14336
14452
  } catch {
14337
14453
  unwritable.push(dir);
14338
14454
  }
@@ -14349,7 +14465,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
14349
14465
  for (let index = 0; index < entry.outputs.length; index++) {
14350
14466
  const output = entry.outputs[index];
14351
14467
  const spec = specByKey.get(key);
14352
- const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, import_node_path23.default.dirname(lockPath));
14468
+ const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, import_node_path24.default.dirname(lockPath));
14353
14469
  const owner = outputOwners.get(absolute);
14354
14470
  if (owner && owner !== key) duplicateOutputs.push(`${output.path} (${owner}, ${key})`);
14355
14471
  outputOwners.set(absolute, key);
@@ -14364,7 +14480,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
14364
14480
  } else {
14365
14481
  add("lock-outputs", "ok", `${outputOwners.size} recorded output path(s) are uniquely owned`);
14366
14482
  }
14367
- const cacheDir = import_node_path23.default.join(import_node_path23.default.dirname(lockPath), ".pixelkiln", "cache");
14483
+ const cacheDir = import_node_path24.default.join(import_node_path24.default.dirname(lockPath), ".pixelkiln", "cache");
14368
14484
  const stranded = [];
14369
14485
  const invalidState = [];
14370
14486
  const staleMapUrls = [];
@@ -14380,11 +14496,11 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
14380
14496
  const hasSource = Boolean(entry.sourceUrl || entry.sourceUrls?.length);
14381
14497
  let hasCache = false;
14382
14498
  for (const output of entry.outputs) {
14383
- const cached = import_node_path23.default.join(
14499
+ const cached = import_node_path24.default.join(
14384
14500
  cacheDir,
14385
14501
  cacheFileName(output.sha256, output.mediaType ?? MediaType.PNG)
14386
14502
  );
14387
- if ((0, import_node_fs19.existsSync)(cached) && await sha256File(cached) === output.sha256) {
14503
+ if ((0, import_node_fs20.existsSync)(cached) && await sha256File(cached) === output.sha256) {
14388
14504
  hasCache = true;
14389
14505
  break;
14390
14506
  }
@@ -14480,16 +14596,16 @@ async function nearestExistingDirectory(start) {
14480
14596
  if ((await (0, import_promises14.stat)(current)).isDirectory()) return current;
14481
14597
  } catch {
14482
14598
  }
14483
- const parent = import_node_path23.default.dirname(current);
14599
+ const parent = import_node_path24.default.dirname(current);
14484
14600
  if (parent === current) return current;
14485
14601
  current = parent;
14486
14602
  }
14487
14603
  }
14488
14604
 
14489
14605
  // src/pipeline/adopt.ts
14490
- var import_node_fs20 = require("fs");
14606
+ var import_node_fs21 = require("fs");
14491
14607
  var import_promises15 = require("fs/promises");
14492
- var import_node_path24 = __toESM(require("path"), 1);
14608
+ var import_node_path25 = __toESM(require("path"), 1);
14493
14609
  async function adopt(provider, specs, lock, lockPath, opts = {}) {
14494
14610
  const log = opts.onProgress ?? (() => {
14495
14611
  });
@@ -14500,7 +14616,7 @@ async function adopt(provider, specs, lock, lockPath, opts = {}) {
14500
14616
  const localByHash = /* @__PURE__ */ new Map();
14501
14617
  const unmatchedLocal = [];
14502
14618
  for (const spec of specs) {
14503
- if (!(0, import_node_fs20.existsSync)(spec.outFile)) {
14619
+ if (!(0, import_node_fs21.existsSync)(spec.outFile)) {
14504
14620
  unmatchedLocal.push(`${spec.styleId}/${spec.assetId} (no file at ${spec.outFile})`);
14505
14621
  continue;
14506
14622
  }
@@ -14622,13 +14738,13 @@ async function tagAdopted(provider, specs, lock, opts = {}) {
14622
14738
  }
14623
14739
 
14624
14740
  // src/pick/server.ts
14625
- var import_node_fs22 = require("fs");
14741
+ var import_node_fs23 = require("fs");
14626
14742
  var import_promises18 = require("fs/promises");
14627
14743
 
14628
14744
  // src/pipeline/init.ts
14629
14745
  var import_promises16 = require("fs/promises");
14630
- var import_node_fs21 = require("fs");
14631
- var import_node_path25 = __toESM(require("path"), 1);
14746
+ var import_node_fs22 = require("fs");
14747
+ var import_node_path26 = __toESM(require("path"), 1);
14632
14748
  function pngSize(buf) {
14633
14749
  if (buf.length < 24) return null;
14634
14750
  const isPng = buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834;
@@ -14642,7 +14758,7 @@ function slugify(value) {
14642
14758
  async function walk(dir, exclude) {
14643
14759
  const out = [];
14644
14760
  for (const entry of await (0, import_promises16.readdir)(dir, { withFileTypes: true })) {
14645
- const full = import_node_path25.default.join(dir, entry.name);
14761
+ const full = import_node_path26.default.join(dir, entry.name);
14646
14762
  if (exclude.some((x) => full.includes(x))) continue;
14647
14763
  if (entry.isDirectory()) out.push(...await walk(full, exclude));
14648
14764
  else if (entry.name.toLowerCase().endsWith(".png")) out.push(full);
@@ -14661,9 +14777,9 @@ async function scanAssets(root, opts = {}) {
14661
14777
  skipped.push(`${file} (not a readable PNG)`);
14662
14778
  continue;
14663
14779
  }
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));
14780
+ const rel = import_node_path26.default.relative(root, file);
14781
+ const category = import_node_path26.default.dirname(rel) === "." ? "" : import_node_path26.default.dirname(rel);
14782
+ let id = slugify(import_node_path26.default.basename(rel));
14667
14783
  if (seen.has(id)) {
14668
14784
  const n = seen.get(id) + 1;
14669
14785
  seen.set(id, n);
@@ -15212,7 +15328,7 @@ async function currentArt(entry, spec) {
15212
15328
  const previous = primaryOutput({ ...entry, outputs: entry.supersededOutputs });
15213
15329
  if (!previous) return null;
15214
15330
  const file = resolveOutputPath(previous.path, spec.root);
15215
- if (!(0, import_node_fs22.existsSync)(file)) return null;
15331
+ if (!(0, import_node_fs23.existsSync)(file)) return null;
15216
15332
  const contentType = previous.mediaType ?? mediaTypeFromExtension(file);
15217
15333
  if (!contentType) return null;
15218
15334
  let width = entry.width;
@@ -15386,15 +15502,15 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
15386
15502
  }
15387
15503
 
15388
15504
  // src/gallery/snapshot.ts
15389
- var import_node_crypto6 = require("crypto");
15390
- var import_node_fs28 = require("fs");
15505
+ var import_node_crypto7 = require("crypto");
15506
+ var import_node_fs29 = require("fs");
15391
15507
  var import_promises26 = require("fs/promises");
15392
- var import_node_path33 = __toESM(require("path"), 1);
15508
+ var import_node_path34 = __toESM(require("path"), 1);
15393
15509
 
15394
15510
  // src/workspace.ts
15395
15511
  var import_promises19 = require("fs/promises");
15396
- var import_node_fs23 = require("fs");
15397
- var import_node_path26 = __toESM(require("path"), 1);
15512
+ var import_node_fs24 = require("fs");
15513
+ var import_node_path27 = __toESM(require("path"), 1);
15398
15514
  var import_zod7 = require("zod");
15399
15515
  var WorkspaceProjectSchema = import_zod7.z.object({
15400
15516
  id: import_zod7.z.string().min(1),
@@ -15419,7 +15535,7 @@ ${parsed.error.issues.slice(0, 5).map((i) => ` ${i.path.join(".")}: ${i.message
15419
15535
  );
15420
15536
  }
15421
15537
  async function loadWorkspace(workspacePath) {
15422
- if (!(0, import_node_fs23.existsSync)(workspacePath)) return { version: 1, projects: [] };
15538
+ if (!(0, import_node_fs24.existsSync)(workspacePath)) return { version: 1, projects: [] };
15423
15539
  let raw;
15424
15540
  try {
15425
15541
  raw = JSON.parse(await (0, import_promises19.readFile)(workspacePath, "utf8"));
@@ -15436,7 +15552,7 @@ async function saveWorkspace(workspacePath, ws) {
15436
15552
  version: 1,
15437
15553
  projects: [...ws.projects].sort((a, b) => a.id.localeCompare(b.id))
15438
15554
  };
15439
- await (0, import_promises19.mkdir)(import_node_path26.default.dirname(import_node_path26.default.resolve(workspacePath)), { recursive: true });
15555
+ await (0, import_promises19.mkdir)(import_node_path27.default.dirname(import_node_path27.default.resolve(workspacePath)), { recursive: true });
15440
15556
  const tmp = `${workspacePath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
15441
15557
  try {
15442
15558
  await (0, import_promises19.writeFile)(tmp, JSON.stringify(sorted, null, 2) + "\n");
@@ -15446,12 +15562,12 @@ async function saveWorkspace(workspacePath, ws) {
15446
15562
  }
15447
15563
  }
15448
15564
  function toPortablePath(dir, absolute) {
15449
- return import_node_path26.default.relative(dir, absolute).split(import_node_path26.default.sep).join("/");
15565
+ return import_node_path27.default.relative(dir, absolute).split(import_node_path27.default.sep).join("/");
15450
15566
  }
15451
15567
  function resolveProject(dir, project) {
15452
15568
  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))
15569
+ manifestPath: import_node_path27.default.resolve(dir, project.manifest.split("/").join(import_node_path27.default.sep)),
15570
+ lockPath: import_node_path27.default.resolve(dir, project.lock.split("/").join(import_node_path27.default.sep))
15455
15571
  };
15456
15572
  }
15457
15573
  function validateWorkspace(ws, dir) {
@@ -15464,21 +15580,21 @@ function validateWorkspace(ws, dir) {
15464
15580
  const { manifestPath, lockPath } = resolveProject(dir, project);
15465
15581
  lockOwners.set(lockPath, [...lockOwners.get(lockPath) ?? [], project.id]);
15466
15582
  manifestOwners.set(manifestPath, [...manifestOwners.get(manifestPath) ?? [], project.id]);
15467
- if (import_node_path26.default.isAbsolute(project.manifest) || import_node_path26.default.isAbsolute(project.lock)) {
15583
+ if (import_node_path27.default.isAbsolute(project.manifest) || import_node_path27.default.isAbsolute(project.lock)) {
15468
15584
  diagnostics.push({
15469
15585
  id: "absolute-path",
15470
15586
  level: "warning",
15471
15587
  message: `project "${project.id}" stores an absolute path; the catalog will not resolve correctly if this tree is cloned or moved elsewhere`
15472
15588
  });
15473
15589
  }
15474
- if (!(0, import_node_fs23.existsSync)(manifestPath)) {
15590
+ if (!(0, import_node_fs24.existsSync)(manifestPath)) {
15475
15591
  diagnostics.push({
15476
15592
  id: "missing-manifest",
15477
15593
  level: "error",
15478
15594
  message: `project "${project.id}" manifest not found: ${manifestPath}`
15479
15595
  });
15480
15596
  }
15481
- if (!(0, import_node_fs23.existsSync)(lockPath)) {
15597
+ if (!(0, import_node_fs24.existsSync)(lockPath)) {
15482
15598
  diagnostics.push({
15483
15599
  id: "missing-lock",
15484
15600
  level: "error",
@@ -15526,15 +15642,15 @@ function validateWorkspace(ws, dir) {
15526
15642
 
15527
15643
  // src/gallery/edit.ts
15528
15644
  var import_promises25 = require("fs/promises");
15529
- var import_node_path32 = __toESM(require("path"), 1);
15645
+ var import_node_path33 = __toESM(require("path"), 1);
15530
15646
  var import_zod13 = require("zod");
15531
15647
 
15532
15648
  // src/manifest-edit.ts
15533
15649
  var import_promises20 = require("fs/promises");
15534
- var import_node_path27 = __toESM(require("path"), 1);
15650
+ var import_node_path28 = __toESM(require("path"), 1);
15535
15651
  var import_zod8 = require("zod");
15536
15652
  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");
15653
+ 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
15654
  var AssetPatchSchema = import_zod8.z.object({
15539
15655
  prompt: import_zod8.z.string().optional(),
15540
15656
  /** Per-style override for the style being viewed; null removes it. */
@@ -15953,7 +16069,7 @@ function applyEdit(raw, edit) {
15953
16069
  }
15954
16070
  }
15955
16071
  async function withCandidate(manifestPath, edit, use, unchanged) {
15956
- const absolute = import_node_path27.default.resolve(manifestPath);
16072
+ const absolute = import_node_path28.default.resolve(manifestPath);
15957
16073
  const original = await (0, import_promises20.readFile)(absolute, "utf8");
15958
16074
  if (sha256(original) !== edit.expectedSha256) throw new ManifestDriftError(absolute);
15959
16075
  let raw;
@@ -15967,9 +16083,9 @@ async function withCandidate(manifestPath, edit, use, unchanged) {
15967
16083
  applyEdit(raw, edit);
15968
16084
  const next = serializeLike(original, raw);
15969
16085
  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`
16086
+ const tmp = import_node_path28.default.join(
16087
+ import_node_path28.default.dirname(absolute),
16088
+ `.${import_node_path28.default.basename(absolute)}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`
15973
16089
  );
15974
16090
  try {
15975
16091
  await (0, import_promises20.writeFile)(tmp, next);
@@ -15998,18 +16114,18 @@ async function applyManifestEdit(manifestPath, edit) {
15998
16114
  }
15999
16115
 
16000
16116
  // src/pipeline/hand-edit.ts
16001
- var import_node_fs24 = require("fs");
16117
+ var import_node_fs25 = require("fs");
16002
16118
  var import_promises21 = require("fs/promises");
16003
- var import_node_path28 = __toESM(require("path"), 1);
16119
+ var import_node_path29 = __toESM(require("path"), 1);
16004
16120
  var import_zod9 = require("zod");
16005
16121
  var HAND_EDIT_DIR = "edits";
16006
16122
  function handEditPath(loaded, spec) {
16007
16123
  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"));
16124
+ const outDir = import_node_path29.default.resolve(loaded.root, style?.outDir ?? ".");
16125
+ const relative = import_node_path29.default.relative(outDir, spec.outFile);
16126
+ const inside = relative && !relative.startsWith("..") && !import_node_path29.default.isAbsolute(relative);
16127
+ const file = inside ? relative : import_node_path29.default.basename(spec.outFile);
16128
+ return import_node_path29.default.join(outDir, HAND_EDIT_DIR, file.replace(/\.gif$/i, ".png"));
16013
16129
  }
16014
16130
  function handEditBases(spec, lock) {
16015
16131
  const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
@@ -16022,10 +16138,10 @@ function handEditBases(spec, lock) {
16022
16138
  }));
16023
16139
  if (members.some((member) => member.mediaType && member.mediaType !== "image/png")) return null;
16024
16140
  if (entry.outputs.length === 1 && !primaryOutput(entry)) return null;
16025
- if (!members.every((member) => (0, import_node_fs24.existsSync)(member.path))) return null;
16141
+ if (!members.every((member) => (0, import_node_fs25.existsSync)(member.path))) return null;
16026
16142
  return members.map(({ role, path: file }) => ({ role, path: file }));
16027
16143
  }
16028
- return (0, import_node_fs24.existsSync)(spec.outFile) ? [{ role: null, path: spec.outFile }] : null;
16144
+ return (0, import_node_fs25.existsSync)(spec.outFile) ? [{ role: null, path: spec.outFile }] : null;
16029
16145
  }
16030
16146
  function handEditBase(spec, lock) {
16031
16147
  const bases = handEditBases(spec, lock);
@@ -16055,18 +16171,18 @@ async function startHandEdit(loaded, lock, spec, opts = {}) {
16055
16171
  `${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
16172
  );
16057
16173
  }
16058
- const existing = spec.source ? import_node_path28.default.resolve(loaded.root, spec.source) : null;
16174
+ const existing = spec.source ? import_node_path29.default.resolve(loaded.root, spec.source) : null;
16059
16175
  const editPath = existing ?? handEditPath(loaded, spec);
16060
16176
  const members = handEditMembers(loaded, spec, lock, editPath);
16061
16177
  let created = false;
16062
16178
  for (const member of members) {
16063
- if ((0, import_node_fs24.existsSync)(member.path)) continue;
16179
+ if ((0, import_node_fs25.existsSync)(member.path)) continue;
16064
16180
  if (!member.base) {
16065
16181
  throw new ManifestEditError(
16066
16182
  `${spec.styleId}/${spec.assetId} has no generated PNG on disk to start from; generate or restore it first`
16067
16183
  );
16068
16184
  }
16069
- await (0, import_promises21.mkdir)(import_node_path28.default.dirname(member.path), { recursive: true });
16185
+ await (0, import_promises21.mkdir)(import_node_path29.default.dirname(member.path), { recursive: true });
16070
16186
  await (0, import_promises21.copyFile)(member.base, member.path);
16071
16187
  created = true;
16072
16188
  }
@@ -16204,7 +16320,7 @@ async function saveHandEdit(loaded, lock, spec, input) {
16204
16320
  editor: input.editor,
16205
16321
  protocol: input.protocol,
16206
16322
  savedAt: (input.now ?? /* @__PURE__ */ new Date()).toISOString(),
16207
- project: projectPath ? import_node_path28.default.basename(projectPath) : null,
16323
+ project: projectPath ? import_node_path29.default.basename(projectPath) : null,
16208
16324
  outputs: incoming.map((frame, index) => ({
16209
16325
  role: frame.role,
16210
16326
  basedOn: basedOnHash(entry, index),
@@ -16246,9 +16362,9 @@ function openInEditor(file, editor = process.env.PIXELKILN_EDITOR) {
16246
16362
  }
16247
16363
 
16248
16364
  // src/gallery/upload.ts
16249
- var import_node_fs25 = require("fs");
16365
+ var import_node_fs26 = require("fs");
16250
16366
  var import_promises22 = require("fs/promises");
16251
- var import_node_path29 = __toESM(require("path"), 1);
16367
+ var import_node_path30 = __toESM(require("path"), 1);
16252
16368
  var import_zod10 = require("zod");
16253
16369
  var MAX_UPLOAD_BYTES = 8 * 1024 * 1024;
16254
16370
  var UploadImageSchema = import_zod10.z.object({
@@ -16261,12 +16377,12 @@ var UploadImageSchema = import_zod10.z.object({
16261
16377
  overwrite: import_zod10.z.boolean().optional()
16262
16378
  }).strict();
16263
16379
  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)) {
16380
+ const file = import_node_path30.default.resolve(root, request.path);
16381
+ const relative = import_node_path30.default.relative(root, file);
16382
+ if (relative.startsWith("..") || import_node_path30.default.isAbsolute(relative)) {
16267
16383
  throw new ManifestEditError("an upload must stay inside the project");
16268
16384
  }
16269
- const extension = import_node_path29.default.extname(file).toLowerCase();
16385
+ const extension = import_node_path30.default.extname(file).toLowerCase();
16270
16386
  const expected = extension === ".png" ? "png" : extension === ".jpg" || extension === ".jpeg" ? "jpeg" : null;
16271
16387
  if (!expected) throw new ManifestEditError("an upload must be a .png, .jpg, or .jpeg path");
16272
16388
  const bytes = Buffer.from(request.base64, "base64");
@@ -16278,18 +16394,18 @@ async function saveProjectImage(root, request) {
16278
16394
  if (metadata.format !== expected) {
16279
16395
  throw new ManifestEditError(`the upload is a ${metadata.format.toUpperCase()}; name it ${metadata.format === "png" ? ".png" : ".jpg"}`);
16280
16396
  }
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`);
16397
+ if ((0, import_node_fs26.existsSync)(file) && !request.overwrite) {
16398
+ throw new ManifestEditError(`${relative.split(import_node_path30.default.sep).join("/")} already exists; choose another name, or replace it`);
16283
16399
  }
16284
- await (0, import_promises22.mkdir)(import_node_path29.default.dirname(file), { recursive: true });
16400
+ await (0, import_promises22.mkdir)(import_node_path30.default.dirname(file), { recursive: true });
16285
16401
  await (0, import_promises22.writeFile)(file, bytes);
16286
- return relative.split(import_node_path29.default.sep).join("/");
16402
+ return relative.split(import_node_path30.default.sep).join("/");
16287
16403
  }
16288
16404
 
16289
16405
  // src/gallery/image-revision.ts
16290
- var import_node_fs26 = require("fs");
16406
+ var import_node_fs27 = require("fs");
16291
16407
  var import_promises23 = require("fs/promises");
16292
- var import_node_path30 = __toESM(require("path"), 1);
16408
+ var import_node_path31 = __toESM(require("path"), 1);
16293
16409
  var import_zod11 = require("zod");
16294
16410
  var InpaintRevisionRequestSchema = import_zod11.z.object({
16295
16411
  action: import_zod11.z.literal("create-inpaint-revision"),
@@ -16308,15 +16424,15 @@ var InpaintRevisionRequestSchema = import_zod11.z.object({
16308
16424
  overwrite: import_zod11.z.boolean().optional()
16309
16425
  }).strict();
16310
16426
  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") {
16427
+ const root = import_node_path31.default.dirname(import_node_path31.default.resolve(manifestPath));
16428
+ if (import_node_path31.default.extname(request.maskPath).toLowerCase() !== ".png") {
16313
16429
  throw new ManifestEditError("the mask must be a .png path inside the project");
16314
16430
  }
16315
16431
  const bytes = Buffer.from(request.maskBase64, "base64");
16316
16432
  const metadata = imageMetadata(bytes);
16317
16433
  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;
16434
+ const file = import_node_path31.default.resolve(root, request.maskPath);
16435
+ const previous = (0, import_node_fs27.existsSync)(file) ? await (0, import_promises23.readFile)(file) : null;
16320
16436
  const mask = await saveProjectImage(root, {
16321
16437
  action: "upload-image",
16322
16438
  path: request.maskPath,
@@ -16343,9 +16459,9 @@ async function createInpaintRevision(manifestPath, request) {
16343
16459
  }
16344
16460
 
16345
16461
  // src/gallery/skeleton.ts
16346
- var import_node_fs27 = require("fs");
16462
+ var import_node_fs28 = require("fs");
16347
16463
  var import_promises24 = require("fs/promises");
16348
- var import_node_path31 = __toESM(require("path"), 1);
16464
+ var import_node_path32 = __toESM(require("path"), 1);
16349
16465
  var import_zod12 = require("zod");
16350
16466
  var SkeletonEstimateRequestSchema = import_zod12.z.object({
16351
16467
  project: import_zod12.z.string().min(1).optional(),
@@ -16383,20 +16499,20 @@ var SkeletonKeypointsUpdateSchema = import_zod12.z.object({
16383
16499
  set: SkeletonSetSchema
16384
16500
  }).strict();
16385
16501
  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)) {
16502
+ const root = import_node_path32.default.dirname(import_node_path32.default.resolve(manifestPath));
16503
+ const file = import_node_path32.default.resolve(root, request.keypointsFile);
16504
+ const relative = import_node_path32.default.relative(root, file);
16505
+ if (!request.keypointsFile.endsWith(".json") || relative.startsWith("..") || import_node_path32.default.isAbsolute(relative)) {
16390
16506
  throw new ManifestEditError("the keypoints file must be a .json path inside the project");
16391
16507
  }
16392
- const existed = (0, import_node_fs27.existsSync)(file);
16508
+ const existed = (0, import_node_fs28.existsSync)(file);
16393
16509
  let previous = null;
16394
16510
  if (request.set) {
16395
16511
  if (existed && !request.overwrite) {
16396
16512
  throw new ManifestEditError(`${request.keypointsFile} already exists; choose another path, or replace it`);
16397
16513
  }
16398
16514
  if (existed) previous = await (0, import_promises24.readFile)(file);
16399
- await (0, import_promises24.mkdir)(import_node_path31.default.dirname(file), { recursive: true });
16515
+ await (0, import_promises24.mkdir)(import_node_path32.default.dirname(file), { recursive: true });
16400
16516
  await (0, import_promises24.writeFile)(file, JSON.stringify(request.set, null, 2) + "\n");
16401
16517
  } else if (!existed) {
16402
16518
  throw new ManifestEditError(`${request.keypointsFile} does not exist; paste poses or estimate them from the sprite`);
@@ -16412,7 +16528,7 @@ async function createSkeletonAnimation(manifestPath, request) {
16412
16528
  revision: {
16413
16529
  mode: "animate-skeleton",
16414
16530
  from: request.from,
16415
- keypointsFile: relative.split(import_node_path31.default.sep).join("/"),
16531
+ keypointsFile: relative.split(import_node_path32.default.sep).join("/"),
16416
16532
  direction: request.direction,
16417
16533
  ...request.description ? { description: request.description } : {},
16418
16534
  ...request.skeletonTemplate ? { skeletonTemplate: request.skeletonTemplate } : {}
@@ -16433,19 +16549,19 @@ async function updateSkeletonKeypoints(ctx, request) {
16433
16549
  if (!spec) throw new ManifestEditError(`"${key}" is not declared by the manifest`);
16434
16550
  const file = spec.revision?.mode === "animate-skeleton" ? spec.revision.keypointsFile : void 0;
16435
16551
  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)) {
16552
+ const relative = import_node_path32.default.relative(ctx.loaded.root, file);
16553
+ if (relative.startsWith("..") || import_node_path32.default.isAbsolute(relative)) {
16438
16554
  throw new ManifestEditError(`${key}'s keypoints file is outside the project; edit it by hand`);
16439
16555
  }
16440
- const current = (0, import_node_fs27.existsSync)(file) ? sha256(await (0, import_promises24.readFile)(file)) : null;
16556
+ const current = (0, import_node_fs28.existsSync)(file) ? sha256(await (0, import_promises24.readFile)(file)) : null;
16441
16557
  if (current !== request.expectedSha256) {
16442
16558
  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`),
16559
+ new Error(`${relative.split(import_node_path32.default.sep).join("/")} changed on disk since this page loaded it; press Refresh and edit again`),
16444
16560
  { status: 409 }
16445
16561
  );
16446
16562
  }
16447
16563
  await (0, import_promises24.writeFile)(file, JSON.stringify(request.set, null, 2) + "\n");
16448
- return relative.split(import_node_path31.default.sep).join("/");
16564
+ return relative.split(import_node_path32.default.sep).join("/");
16449
16565
  }
16450
16566
 
16451
16567
  // src/gallery/edit.ts
@@ -16499,7 +16615,7 @@ function createGalleryEditHandler(opts) {
16499
16615
  const history = /* @__PURE__ */ new Map();
16500
16616
  const manifestOf = async (project) => {
16501
16617
  try {
16502
- return import_node_path32.default.resolve(await opts.manifestFor(typeof project === "string" ? project : void 0));
16618
+ return import_node_path33.default.resolve(await opts.manifestFor(typeof project === "string" ? project : void 0));
16503
16619
  } catch {
16504
16620
  return null;
16505
16621
  }
@@ -16522,7 +16638,7 @@ function createGalleryEditHandler(opts) {
16522
16638
  await (0, import_promises25.writeFile)(tmp, last.before);
16523
16639
  await (0, import_promises25.rename)(tmp, manifestPath2);
16524
16640
  stack.pop();
16525
- log(` manifest edit undone in ${import_node_path32.default.relative(process.cwd(), manifestPath2) || manifestPath2}`);
16641
+ log(` manifest edit undone in ${import_node_path33.default.relative(process.cwd(), manifestPath2) || manifestPath2}`);
16526
16642
  return opts.reload();
16527
16643
  }
16528
16644
  const manifestPath = await manifestOf(body?.project);
@@ -16584,7 +16700,7 @@ function createEditApplier(opts, log) {
16584
16700
  } catch (error) {
16585
16701
  throw new ManifestEditError(error instanceof Error ? error.message : String(error));
16586
16702
  }
16587
- const written = await saveProjectImage(import_node_path32.default.dirname(import_node_path32.default.resolve(manifestPath2)), upload.data);
16703
+ const written = await saveProjectImage(import_node_path33.default.dirname(import_node_path33.default.resolve(manifestPath2)), upload.data);
16588
16704
  log(` image added: ${written}`);
16589
16705
  return opts.reload();
16590
16706
  }
@@ -16651,7 +16767,7 @@ function createEditApplier(opts, log) {
16651
16767
  }
16652
16768
  const result = await applyManifestEdit(manifestPath, parsed.data);
16653
16769
  if (result.changed) {
16654
- const relative = import_node_path32.default.relative(process.cwd(), result.manifestPath);
16770
+ const relative = import_node_path33.default.relative(process.cwd(), result.manifestPath);
16655
16771
  const shown = !relative || relative.startsWith("..") ? result.manifestPath : relative;
16656
16772
  log(` manifest edited: ${describeEdit(parsed.data)} in ${shown}`);
16657
16773
  }
@@ -16679,7 +16795,7 @@ function describeEdit(edit) {
16679
16795
  // src/gallery/snapshot.ts
16680
16796
  var PROJECT_FILE_TYPE = "application/octet-stream";
16681
16797
  function galleryMediaId(absolutePath) {
16682
- return (0, import_node_crypto6.createHash)("sha256").update(import_node_path33.default.resolve(absolutePath)).digest("hex").slice(0, 24);
16798
+ return (0, import_node_crypto7.createHash)("sha256").update(import_node_path34.default.resolve(absolutePath)).digest("hex").slice(0, 24);
16683
16799
  }
16684
16800
  function galleryMediaRoute(id) {
16685
16801
  return `/media/${id}`;
@@ -16698,9 +16814,9 @@ function describeHistory(media, entry, cacheDir) {
16698
16814
  return (entry.history ?? []).map((generation, i) => {
16699
16815
  const outputs = generation.outputs.map((output) => {
16700
16816
  const mediaType = output.mediaType ?? MediaType.PNG;
16701
- const file = import_node_path33.default.join(cacheDir, cacheFileName(output.sha256, mediaType));
16817
+ const file = import_node_path34.default.join(cacheDir, cacheFileName(output.sha256, mediaType));
16702
16818
  let url = null;
16703
- if ((0, import_node_fs28.existsSync)(file)) {
16819
+ if ((0, import_node_fs29.existsSync)(file)) {
16704
16820
  const id = galleryMediaId(file);
16705
16821
  media.set(id, { path: file, contentType: mediaType });
16706
16822
  url = `${galleryMediaRoute(id)}?v=${output.sha256.slice(0, 24)}`;
@@ -16830,10 +16946,10 @@ async function describeOutput(media, root, absolutePath, recorded = {}) {
16830
16946
  async function describeQuality(media, root, inspection) {
16831
16947
  const outputPaths = inspection.outputs ?? [inspection.output];
16832
16948
  const outputs = await Promise.all(
16833
- outputPaths.map((file) => describeOutput(media, root, import_node_path33.default.resolve(root, file)))
16949
+ outputPaths.map((file) => describeOutput(media, root, import_node_path34.default.resolve(root, file)))
16834
16950
  );
16835
- const recordPath = import_node_path33.default.resolve(root, inspection.record);
16836
- const recordExists = (0, import_node_fs28.existsSync)(recordPath);
16951
+ const recordPath = import_node_path34.default.resolve(root, inspection.record);
16952
+ const recordExists = (0, import_node_fs29.existsSync)(recordPath);
16837
16953
  let options = null;
16838
16954
  let check = null;
16839
16955
  if (recordExists) {
@@ -16852,7 +16968,7 @@ async function describeQuality(media, root, inspection) {
16852
16968
  return {
16853
16969
  state: inspection.state,
16854
16970
  reason: inspection.reason,
16855
- output: portableOutputPath(import_node_path33.default.resolve(root, inspection.output), root),
16971
+ output: portableOutputPath(import_node_path34.default.resolve(root, inspection.output), root),
16856
16972
  record: portableOutputPath(recordPath, root),
16857
16973
  recordExists,
16858
16974
  outputs,
@@ -16892,7 +17008,7 @@ async function buildGallerySnapshot(opts) {
16892
17008
  const items = [];
16893
17009
  const manifestText = await (0, import_promises26.readFile)(loaded.path, "utf8");
16894
17010
  const manifestSha256 = sha256(manifestText);
16895
- const cacheDir = import_node_path33.default.resolve(import_node_path33.default.dirname(opts.lockPath), ".pixelkiln", "cache");
17011
+ const cacheDir = import_node_path34.default.resolve(import_node_path34.default.dirname(opts.lockPath), ".pixelkiln", "cache");
16896
17012
  let rawStyles = {};
16897
17013
  try {
16898
17014
  const raw = JSON.parse(manifestText);
@@ -16916,12 +17032,12 @@ async function buildGallerySnapshot(opts) {
16916
17032
  )
16917
17033
  );
16918
17034
  if (spec.source) {
16919
- const editPath = import_node_path33.default.resolve(root, spec.source);
17035
+ const editPath = import_node_path34.default.resolve(root, spec.source);
16920
17036
  const memberCount = sourceIsStem(spec.source, entry, root) ? entry.outputs.length : 1;
16921
17037
  const editShas = await Promise.all(
16922
17038
  Array.from({ length: memberCount }, async (_, index) => {
16923
17039
  const file = sourceOutputPath(spec.source, entry, index, root);
16924
- return { file, sha256: (0, import_node_fs28.existsSync)(file) ? await sha256File(file) : null };
17040
+ return { file, sha256: (0, import_node_fs29.existsSync)(file) ? await sha256File(file) : null };
16925
17041
  })
16926
17042
  );
16927
17043
  edits = await Promise.all(editShas.map(({ file, sha256: editSha }) => describeOutput(media, root, file, { sha256: editSha })));
@@ -16929,7 +17045,7 @@ async function buildGallerySnapshot(opts) {
16929
17045
  const companion = await readHandEditCompanion(editPath);
16930
17046
  if (companion) {
16931
17047
  const projectPath = handEditProjectPath(editPath);
16932
- const project = companion.project && (0, import_node_fs28.existsSync)(projectPath) ? await fileInfo(projectPath) : null;
17048
+ const project = companion.project && (0, import_node_fs29.existsSync)(projectPath) ? await fileInfo(projectPath) : null;
16933
17049
  let projectUrl = null;
16934
17050
  if (project) {
16935
17051
  const id = galleryMediaId(projectPath);
@@ -16965,8 +17081,8 @@ async function buildGallerySnapshot(opts) {
16965
17081
  editStatus = edits.some((member) => !member.exists) ? "missing" : editChanged.every((changed) => !changed) ? "same" : regenerated ? "regenerated-since" : "edited";
16966
17082
  }
16967
17083
  } 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)) {
17084
+ outputs = [await describeOutput(media, root, import_node_path34.default.resolve(root, spec.source))];
17085
+ } else if (!entry && (0, import_node_fs29.existsSync)(spec.outFile)) {
16970
17086
  outputs = [await describeOutput(media, root, spec.outFile)];
16971
17087
  } else {
16972
17088
  outputs = [];
@@ -17169,7 +17285,7 @@ async function buildGallerySnapshot(opts) {
17169
17285
  id: loaded.manifest.name,
17170
17286
  name: loaded.manifest.name,
17171
17287
  manifest: loaded.path,
17172
- lock: import_node_path33.default.resolve(opts.lockPath),
17288
+ lock: import_node_path34.default.resolve(opts.lockPath),
17173
17289
  root,
17174
17290
  provider: loaded.manifest.provider,
17175
17291
  account: null,
@@ -17195,7 +17311,7 @@ async function buildGallerySnapshot(opts) {
17195
17311
  return { snapshot, media };
17196
17312
  }
17197
17313
  async function buildWorkspaceGallerySnapshot(opts) {
17198
- const dir = import_node_path33.default.dirname(import_node_path33.default.resolve(opts.workspacePath));
17314
+ const dir = import_node_path34.default.dirname(import_node_path34.default.resolve(opts.workspacePath));
17199
17315
  const media = /* @__PURE__ */ new Map();
17200
17316
  const projects = [];
17201
17317
  const items = [];
@@ -17238,7 +17354,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
17238
17354
  id: project.id,
17239
17355
  name: loaded.manifest.name,
17240
17356
  manifest: manifestPath,
17241
- lock: import_node_path33.default.resolve(lockPath),
17357
+ lock: import_node_path34.default.resolve(lockPath),
17242
17358
  root: loaded.root,
17243
17359
  provider: loaded.manifest.provider,
17244
17360
  account: project.account ?? null,
@@ -17254,8 +17370,8 @@ async function buildWorkspaceGallerySnapshot(opts) {
17254
17370
  id: project.id,
17255
17371
  name: project.id,
17256
17372
  manifest: manifestPath,
17257
- lock: import_node_path33.default.resolve(lockPath),
17258
- root: import_node_path33.default.dirname(manifestPath),
17373
+ lock: import_node_path34.default.resolve(lockPath),
17374
+ root: import_node_path34.default.dirname(manifestPath),
17259
17375
  provider: project.provider,
17260
17376
  account: project.account ?? null,
17261
17377
  manifestSha256: null,
@@ -17282,7 +17398,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
17282
17398
  version: 1,
17283
17399
  generatedAt: (opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
17284
17400
  project: null,
17285
- workspace: { path: import_node_path33.default.resolve(opts.workspacePath), dir, projects },
17401
+ workspace: { path: import_node_path34.default.resolve(opts.workspacePath), dir, projects },
17286
17402
  filter,
17287
17403
  totals: {
17288
17404
  entries: projects.reduce((sum, project) => sum + project.entries, 0),
@@ -17299,7 +17415,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
17299
17415
  }
17300
17416
 
17301
17417
  // src/gallery/page.ts
17302
- var import_node_fs29 = require("fs");
17418
+ var import_node_fs30 = require("fs");
17303
17419
 
17304
17420
  // src/gallery/client-bundle.ts
17305
17421
  var import_node_module = require("module");
@@ -17339,8 +17455,8 @@ var client;
17339
17455
  function clientAssets() {
17340
17456
  const prebuilt = new URL("gallery.js", CLIENT_DIR);
17341
17457
  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()
17458
+ css: (0, import_node_fs30.readFileSync)(new URL("gallery.css", CLIENT_DIR), "utf8"),
17459
+ js: (0, import_node_fs30.existsSync)(prebuilt) ? (0, import_node_fs30.readFileSync)(prebuilt, "utf8") : bundleGalleryClient()
17344
17460
  };
17345
17461
  return client;
17346
17462
  }
@@ -17482,11 +17598,11 @@ var EDITOR_PIN = pin_default;
17482
17598
  var EDITOR_RELEASE_BASE = "https://github.com/gfargo/pixelkiln/releases/download";
17483
17599
 
17484
17600
  // src/editor/install.ts
17485
- var import_node_crypto7 = require("crypto");
17486
- var import_node_fs30 = require("fs");
17601
+ var import_node_crypto8 = require("crypto");
17602
+ var import_node_fs31 = require("fs");
17487
17603
  var import_promises27 = require("fs/promises");
17488
- var import_node_os3 = __toESM(require("os"), 1);
17489
- var import_node_path34 = __toESM(require("path"), 1);
17604
+ var import_node_os4 = __toESM(require("os"), 1);
17605
+ var import_node_path35 = __toESM(require("path"), 1);
17490
17606
  var EditorInstallError = class extends Error {
17491
17607
  constructor(message6, file) {
17492
17608
  super(message6);
@@ -17497,15 +17613,15 @@ var EditorInstallError = class extends Error {
17497
17613
  };
17498
17614
  function editorToolsRoot() {
17499
17615
  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");
17616
+ if (override) return import_node_path35.default.resolve(override);
17617
+ if (process.platform === "darwin") return import_node_path35.default.join(import_node_os4.default.homedir(), "Library", "Caches", "pixelkiln", "tools");
17502
17618
  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");
17619
+ return import_node_path35.default.join(process.env.LOCALAPPDATA ?? import_node_path35.default.join(import_node_os4.default.homedir(), "AppData", "Local"), "pixelkiln", "tools");
17504
17620
  }
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");
17621
+ 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
17622
  }
17507
17623
  function editorDir(pin = EDITOR_PIN, root = editorToolsRoot()) {
17508
- return import_node_path34.default.join(root, "pixelorama", pin.release ?? "unpublished");
17624
+ return import_node_path35.default.join(root, "pixelorama", pin.release ?? "unpublished");
17509
17625
  }
17510
17626
  function editorBaseUrl(pin = EDITOR_PIN) {
17511
17627
  const override = process.env.PIXELKILN_EDITOR_URL?.trim();
@@ -17515,7 +17631,7 @@ function editorBaseUrl(pin = EDITOR_PIN) {
17515
17631
  }
17516
17632
  async function sha256Of(file) {
17517
17633
  try {
17518
- return (0, import_node_crypto7.createHash)("sha256").update(await (0, import_promises27.readFile)(file)).digest("hex");
17634
+ return (0, import_node_crypto8.createHash)("sha256").update(await (0, import_promises27.readFile)(file)).digest("hex");
17519
17635
  } catch {
17520
17636
  return null;
17521
17637
  }
@@ -17528,8 +17644,8 @@ async function editorStatus(opts = {}) {
17528
17644
  let totalBytes = 0;
17529
17645
  for (const [name, expected] of Object.entries(pin.files)) {
17530
17646
  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;
17647
+ const file = import_node_path35.default.join(dir, name);
17648
+ const info = (0, import_node_fs31.existsSync)(file) ? await (0, import_promises27.stat)(file).catch(() => null) : null;
17533
17649
  if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
17534
17650
  installedBytes += expected.bytes;
17535
17651
  } else {
@@ -17582,11 +17698,11 @@ async function installEditor(opts = {}) {
17582
17698
  if (bytes.length !== expected.bytes) {
17583
17699
  throw new EditorInstallError(`${name}: expected ${expected.bytes} bytes, got ${bytes.length}`, name);
17584
17700
  }
17585
- const digest2 = (0, import_node_crypto7.createHash)("sha256").update(bytes).digest("hex");
17701
+ const digest2 = (0, import_node_crypto8.createHash)("sha256").update(bytes).digest("hex");
17586
17702
  if (digest2 !== expected.sha256) {
17587
17703
  throw new EditorInstallError(`${name}: sha256 ${digest2.slice(0, 12)}\u2026 does not match the pinned ${expected.sha256.slice(0, 12)}\u2026`, name);
17588
17704
  }
17589
- const final = import_node_path34.default.join(dir, name);
17705
+ const final = import_node_path35.default.join(dir, name);
17590
17706
  const tmp = `${final}.${process.pid}.part`;
17591
17707
  try {
17592
17708
  await (0, import_promises27.writeFile)(tmp, bytes);
@@ -17632,12 +17748,12 @@ var EDITOR_CONTENT_TYPES = {
17632
17748
  ".json": "application/json; charset=utf-8"
17633
17749
  };
17634
17750
  function editorContentType(name) {
17635
- return EDITOR_CONTENT_TYPES[import_node_path34.default.extname(name).toLowerCase()] ?? null;
17751
+ return EDITOR_CONTENT_TYPES[import_node_path35.default.extname(name).toLowerCase()] ?? null;
17636
17752
  }
17637
17753
 
17638
17754
  // src/gallery/editor.ts
17639
17755
  var import_promises28 = require("fs/promises");
17640
- var import_node_path35 = __toESM(require("path"), 1);
17756
+ var import_node_path36 = __toESM(require("path"), 1);
17641
17757
  function editorRoute(release, name = "index.html") {
17642
17758
  return `/editor/${encodeURIComponent(release)}/${encodeURIComponent(name)}`;
17643
17759
  }
@@ -17710,7 +17826,7 @@ function createGalleryEditorHandlers(opts = {}) {
17710
17826
  if (!status.installed) return null;
17711
17827
  let bytes;
17712
17828
  try {
17713
- bytes = await (0, import_promises28.readFile)(import_node_path35.default.join(dir, name));
17829
+ bytes = await (0, import_promises28.readFile)(import_node_path36.default.join(dir, name));
17714
17830
  } catch {
17715
17831
  verified = null;
17716
17832
  return null;
@@ -17725,7 +17841,7 @@ function createGalleryEditorHandlers(opts = {}) {
17725
17841
  }
17726
17842
 
17727
17843
  // src/gallery/server.ts
17728
- var import_node_crypto8 = require("crypto");
17844
+ var import_node_crypto9 = require("crypto");
17729
17845
  var import_node_http2 = require("http");
17730
17846
  var import_promises29 = require("fs/promises");
17731
17847
  var MAX_EDIT_BYTES = 48 * 1024 * 1024;
@@ -17755,7 +17871,7 @@ async function readJsonBody(req) {
17755
17871
  async function serveGallery(opts) {
17756
17872
  const log = opts.onProgress ?? (() => {
17757
17873
  });
17758
- const session = opts.edit || opts.generate || opts.editor || opts.skeleton || opts.price ? (0, import_node_crypto8.randomBytes)(16).toString("hex") : null;
17874
+ const session = opts.edit || opts.generate || opts.editor || opts.skeleton || opts.price ? (0, import_node_crypto9.randomBytes)(16).toString("hex") : null;
17759
17875
  let media = /* @__PURE__ */ new Map();
17760
17876
  let loading = null;
17761
17877
  const reviewAssets = /* @__PURE__ */ new Map();
@@ -17959,7 +18075,7 @@ async function serveGallery(opts) {
17959
18075
  try {
17960
18076
  if (url2.pathname === "/") {
17961
18077
  const { snapshot } = await load();
17962
- const nonce = (0, import_node_crypto8.randomBytes)(16).toString("base64");
18078
+ const nonce = (0, import_node_crypto9.randomBytes)(16).toString("base64");
17963
18079
  res.writeHead(200, {
17964
18080
  "Content-Type": "text/html; charset=utf-8",
17965
18081
  "Cache-Control": "no-store",
@@ -18034,7 +18150,7 @@ async function serveGallery(opts) {
18034
18150
  }
18035
18151
 
18036
18152
  // src/gallery/generate.ts
18037
- var import_node_crypto9 = require("crypto");
18153
+ var import_node_crypto10 = require("crypto");
18038
18154
  var import_zod14 = require("zod");
18039
18155
  var GenerateRequestSchema = import_zod14.z.object({
18040
18156
  project: import_zod14.z.string().min(1).optional(),
@@ -18334,7 +18450,7 @@ function createGenerateHandlers(opts) {
18334
18450
  }
18335
18451
  }
18336
18452
  const job = {
18337
- id: (0, import_node_crypto9.randomBytes)(8).toString("hex"),
18453
+ id: (0, import_node_crypto10.randomBytes)(8).toString("hex"),
18338
18454
  project: request.project ?? null,
18339
18455
  keys: request.keys,
18340
18456
  mode: request.refresh ? "refresh" : request.resume ? "resume" : request.restore ? "restore" : request.revert !== void 0 ? "revert" : "generate",
@@ -18402,15 +18518,15 @@ function createGenerateHandlers(opts) {
18402
18518
 
18403
18519
  // src/pipeline/salvage.ts
18404
18520
  var import_promises30 = require("fs/promises");
18405
- var import_node_fs31 = require("fs");
18406
- var import_node_path36 = __toESM(require("path"), 1);
18521
+ var import_node_fs32 = require("fs");
18522
+ var import_node_path37 = __toESM(require("path"), 1);
18407
18523
  function providerClaimId(provider, objectId) {
18408
18524
  return `${encodeURIComponent(provider)}:${encodeURIComponent(objectId)}`;
18409
18525
  }
18410
18526
  async function loadClaims(lockPaths, opts = {}) {
18411
18527
  const claimed = /* @__PURE__ */ new Set();
18412
18528
  for (const p of lockPaths) {
18413
- if (!(0, import_node_fs31.existsSync)(p)) throw new Error(`Claim lockfile not found: ${p}`);
18529
+ if (!(0, import_node_fs32.existsSync)(p)) throw new Error(`Claim lockfile not found: ${p}`);
18414
18530
  let parsed;
18415
18531
  try {
18416
18532
  parsed = parseLock(JSON.parse(await (0, import_promises30.readFile)(p, "utf8")));
@@ -18517,20 +18633,20 @@ function matchOrphanStyle(prompt, manifest) {
18517
18633
  return matchStyleByPattern(prompt, manifest);
18518
18634
  }
18519
18635
  async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, claimPaths) {
18520
- const own = import_node_path36.default.resolve(ownManifestPath);
18636
+ const own = import_node_path37.default.resolve(ownManifestPath);
18521
18637
  const siblingManifestPaths = [
18522
18638
  .../* @__PURE__ */ new Set([
18523
18639
  ...workspaceManifestPaths,
18524
- ...claimPaths.map((c) => import_node_path36.default.join(import_node_path36.default.dirname(import_node_path36.default.resolve(c)), "pixelkiln.manifest.json"))
18640
+ ...claimPaths.map((c) => import_node_path37.default.join(import_node_path37.default.dirname(import_node_path37.default.resolve(c)), "pixelkiln.manifest.json"))
18525
18641
  ])
18526
18642
  ];
18527
18643
  const siblings = [];
18528
18644
  for (const siblingManifestPath of siblingManifestPaths) {
18529
- if (import_node_path36.default.resolve(siblingManifestPath) === own) continue;
18530
- if (!(0, import_node_fs31.existsSync)(siblingManifestPath)) continue;
18645
+ if (import_node_path37.default.resolve(siblingManifestPath) === own) continue;
18646
+ if (!(0, import_node_fs32.existsSync)(siblingManifestPath)) continue;
18531
18647
  try {
18532
18648
  const { manifest } = await loadManifest(siblingManifestPath);
18533
- siblings.push({ label: import_node_path36.default.basename(import_node_path36.default.dirname(siblingManifestPath)), manifest });
18649
+ siblings.push({ label: import_node_path37.default.basename(import_node_path37.default.dirname(siblingManifestPath)), manifest });
18534
18650
  } catch {
18535
18651
  }
18536
18652
  }
@@ -18651,7 +18767,7 @@ async function applyTags(provider, decisions, existing, opts = {}) {
18651
18767
 
18652
18768
  // src/pick/salvage-server.ts
18653
18769
  var import_promises31 = require("fs/promises");
18654
- var import_node_path37 = __toESM(require("path"), 1);
18770
+ var import_node_path38 = __toESM(require("path"), 1);
18655
18771
 
18656
18772
  // src/pick/salvage-sheet.ts
18657
18773
  var escapeHtml3 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
@@ -18817,7 +18933,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
18817
18933
  });
18818
18934
  const html = renderSalvageSheet(orphans, {
18819
18935
  styleId: ctx.styleId,
18820
- importDir: import_node_path37.default.relative(process.cwd(), ctx.importDir) || "."
18936
+ importDir: import_node_path38.default.relative(process.cwd(), ctx.importDir) || "."
18821
18937
  });
18822
18938
  const byId = new Map(orphans.map((o) => [o.id, o]));
18823
18939
  const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
@@ -18873,9 +18989,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
18873
18989
  if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
18874
18990
  decodePng(buf);
18875
18991
  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 });
18992
+ const rel = import_node_path38.default.join("_salvaged", `${assetId}.png`);
18993
+ const outFile = import_node_path38.default.resolve(ctx.importDir, rel);
18994
+ await (0, import_promises31.mkdir)(import_node_path38.default.dirname(outFile), { recursive: true });
18879
18995
  await (0, import_promises31.writeFile)(outFile, buf);
18880
18996
  ctx.manifest.assets[assetId] = {
18881
18997
  prompt: orphan.prompt,
@@ -18908,7 +19024,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
18908
19024
  sourceUrl: durableSource,
18909
19025
  sourceUrls: durableSource ? [{ url: durableSource }] : [],
18910
19026
  outputs: [{
18911
- path: portableOutputPath(outFile, import_node_path37.default.dirname(ctx.manifestPath)),
19027
+ path: portableOutputPath(outFile, import_node_path38.default.dirname(ctx.manifestPath)),
18912
19028
  sha256: sha256(buf)
18913
19029
  }],
18914
19030
  submittedAt: orphan.createdAt,