pixelkiln 0.29.0 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import path30 from "path";
5
- import { existsSync as existsSync23 } from "fs";
5
+ import { existsSync as existsSync24 } from "fs";
6
6
  import { readFile as readFile25 } from "fs/promises";
7
7
 
8
8
  // src/env.ts
@@ -3478,10 +3478,11 @@ registerProvider({
3478
3478
 
3479
3479
  // src/manifest.ts
3480
3480
  import { readFile as readFile3 } from "fs/promises";
3481
- import { existsSync as existsSync4 } from "fs";
3481
+ import { existsSync as existsSync5 } from "fs";
3482
3482
  import path6 from "path";
3483
3483
 
3484
3484
  // src/outputs.ts
3485
+ import { existsSync as existsSync4 } from "fs";
3485
3486
  import path5 from "path";
3486
3487
 
3487
3488
  // src/lock.ts
@@ -3678,12 +3679,15 @@ function memberPath(file, role, index, total, mediaType) {
3678
3679
  const safeRole = (role ?? fallbackOutputRole(index)).replace(/[^a-zA-Z0-9_-]+/g, "-");
3679
3680
  return `${stem}-${safeRole}${ext}`;
3680
3681
  }
3681
- function isFrameSetEntry(entry) {
3682
- return entry.generator === "frames" && entry.outputs.length > 1;
3682
+ function isMemberSetEntry(entry) {
3683
+ return entry.outputs.length > 1 && entry.outputs.every((output) => !output.mediaType || output.mediaType === "image/png");
3684
+ }
3685
+ function sourceIsStem(source, entry, manifestDir) {
3686
+ return isMemberSetEntry(entry) && !existsSync4(path5.resolve(manifestDir, source));
3683
3687
  }
3684
3688
  function sourceOutputPath(source, entry, index, manifestDir) {
3685
3689
  const absolute = path5.resolve(manifestDir, source);
3686
- if (!isFrameSetEntry(entry)) return absolute;
3690
+ if (!sourceIsStem(source, entry, manifestDir)) return absolute;
3687
3691
  const output = entry.outputs[index];
3688
3692
  return memberPath(absolute, output.role, index, entry.outputs.length, output.mediaType);
3689
3693
  }
@@ -3771,7 +3775,7 @@ function resolveSpecOutputs(spec, lock, manifestDir) {
3771
3775
  // src/manifest.ts
3772
3776
  async function loadManifest(manifestPath) {
3773
3777
  const abs = path6.resolve(manifestPath);
3774
- if (!existsSync4(abs)) throw new Error(`No manifest at ${abs}`);
3778
+ if (!existsSync5(abs)) throw new Error(`No manifest at ${abs}`);
3775
3779
  const input = ManifestInputSchema.safeParse(JSON.parse(await readFile3(abs, "utf8")));
3776
3780
  if (!input.success) {
3777
3781
  const issues = formatManifestIssues(input.error.issues);
@@ -3943,7 +3947,7 @@ async function resolveSpecs(loaded, filter) {
3943
3947
  const abs = path6.resolve(root, rel);
3944
3948
  let hit = styleImageCache.get(abs);
3945
3949
  if (!hit) {
3946
- if (!existsSync4(abs)) throw new Error(`Style image not found: ${abs}`);
3950
+ if (!existsSync5(abs)) throw new Error(`Style image not found: ${abs}`);
3947
3951
  const buf = await readFile3(abs);
3948
3952
  const metadata = imageMetadata(buf);
3949
3953
  if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${abs}`);
@@ -4184,7 +4188,7 @@ function pngPath(file) {
4184
4188
  return extension ? `${file.slice(0, -extension.length)}.png` : `${file}.png`;
4185
4189
  }
4186
4190
  async function optionalRevisionImage(file, label, requiredFormat) {
4187
- if (!existsSync4(file)) return null;
4191
+ if (!existsSync5(file)) return null;
4188
4192
  const bytes = await readFile3(file);
4189
4193
  const metadata = imageMetadata(bytes);
4190
4194
  if (!metadata) throw new Error(`${label} is not a readable PNG or JPEG: ${file}`);
@@ -4349,7 +4353,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
4349
4353
  inputs.push({ id, path: path7.resolve(manifestDir, source) });
4350
4354
  continue;
4351
4355
  }
4352
- if (source && isFrameSetEntry(entry)) {
4356
+ if (source && sourceIsStem(source, entry, manifestDir)) {
4353
4357
  outputs = outputs.map((output) => ({ ...output, absolutePath: sourceOutputPath(source, entry, output.index, manifestDir) }));
4354
4358
  }
4355
4359
  let selected = outputs;
@@ -4474,7 +4478,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
4474
4478
  const cell = cells[id];
4475
4479
  const source = sources[id];
4476
4480
  const entry = lock.entries[`${styleId}/${id}`];
4477
- if (source && !(entry && isFrameSetEntry(entry))) {
4481
+ if (source && !(entry && sourceIsStem(source, entry, manifestDir))) {
4478
4482
  placements.push({ id, path: path7.resolve(manifestDir, source), cell });
4479
4483
  continue;
4480
4484
  }
@@ -4528,11 +4532,11 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
4528
4532
  }
4529
4533
 
4530
4534
  // src/pipeline/plan.ts
4531
- import { existsSync as existsSync8 } from "fs";
4535
+ import { existsSync as existsSync9 } from "fs";
4532
4536
  import path13 from "path";
4533
4537
 
4534
4538
  // src/pipeline/quality-profile.ts
4535
- import { existsSync as existsSync6 } from "fs";
4539
+ import { existsSync as existsSync7 } from "fs";
4536
4540
  import path11 from "path";
4537
4541
 
4538
4542
  // src/pipeline/refine.ts
@@ -5000,7 +5004,7 @@ async function writeArtifactBundle(files, options = {}) {
5000
5004
 
5001
5005
  // src/pipeline/audit.ts
5002
5006
  import { readFile as readFile5 } from "fs/promises";
5003
- import { existsSync as existsSync5 } from "fs";
5007
+ import { existsSync as existsSync6 } from "fs";
5004
5008
  import path9 from "path";
5005
5009
  function colorDistance(a, b) {
5006
5010
  const rmean = (a.r + b.r) / 2;
@@ -5046,7 +5050,7 @@ async function auditStyle(loaded, specs, styleId, lock) {
5046
5050
  const unreadable = [];
5047
5051
  for (const spec of mine) {
5048
5052
  for (const output of resolveSpecOutputs(spec, lock, loaded.root)) {
5049
- if (!existsSync5(output.absolutePath)) {
5053
+ if (!existsSync6(output.absolutePath)) {
5050
5054
  missing.push(output.id);
5051
5055
  continue;
5052
5056
  }
@@ -5076,7 +5080,7 @@ async function auditStyle(loaded, specs, styleId, lock) {
5076
5080
  const refPalettes = [];
5077
5081
  for (const img of style.styleImages) {
5078
5082
  const abs = path9.resolve(loaded.root, img.path);
5079
- if (!existsSync5(abs)) continue;
5083
+ if (!existsSync6(abs)) continue;
5080
5084
  try {
5081
5085
  refPalettes.push(extractPalette(decodePng(await readFile5(abs)), 12));
5082
5086
  } catch {
@@ -5705,7 +5709,7 @@ async function qualitySource(spec, lock) {
5705
5709
  return { items: null, reason: "frame-set quality requires ordered downloaded provider outputs" };
5706
5710
  }
5707
5711
  const source = path11.resolve(spec.root, spec.source);
5708
- return existsSync6(source) ? { items: [{ role: "source", path: source }] } : { items: null, reason: `declared source is missing: ${spec.source}` };
5712
+ return existsSync7(source) ? { items: [{ role: "source", path: source }] } : { items: null, reason: `declared source is missing: ${spec.source}` };
5709
5713
  }
5710
5714
  const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
5711
5715
  if (!entry) return { items: null, reason: "raw provider output is not in the lockfile" };
@@ -5734,7 +5738,7 @@ async function qualitySource(spec, lock) {
5734
5738
  }
5735
5739
  if (role) roles.add(role);
5736
5740
  const source = currentEntryOutputPath(entry, spec, index);
5737
- if (!existsSync6(source)) return { items: null, reason: `raw provider output is missing: ${source}` };
5741
+ if (!existsSync7(source)) return { items: null, reason: `raw provider output is missing: ${source}` };
5738
5742
  if (await sha256File(source) !== output.sha256) {
5739
5743
  return { items: null, reason: "raw provider output was modified after download" };
5740
5744
  }
@@ -5753,7 +5757,7 @@ async function inspectQualityProfile(spec, lock) {
5753
5757
  }
5754
5758
  const sourcePaths = source.items.map((item) => item.path);
5755
5759
  const expectedOutputs = spec.generator === "frames" ? source.items.map((item, index) => qualityFrameOutputPath(output, item.role, index)) : [output];
5756
- if (!existsSync6(record)) {
5760
+ if (!existsSync7(record)) {
5757
5761
  return {
5758
5762
  key,
5759
5763
  state: "needs-refinement",
@@ -5934,7 +5938,7 @@ async function requireApprovedQualitySources(specs, lock, assetIds, outputRoles)
5934
5938
  }
5935
5939
 
5936
5940
  // src/pipeline/revision.ts
5937
- import { existsSync as existsSync7 } from "fs";
5941
+ import { existsSync as existsSync8 } from "fs";
5938
5942
  import path12 from "path";
5939
5943
  async function inspectRevisionReadiness(spec, lock) {
5940
5944
  if (!spec.revision) return null;
@@ -5950,14 +5954,14 @@ async function inspectRevision(spec, lock, seen) {
5950
5954
  reason: `revision source ${spec.styleId}/${revision.sourceAssetId} is not ready: ${dependency.reason}`
5951
5955
  };
5952
5956
  }
5953
- if (!revision.sourceSha256 || !existsSync7(revision.sourceFile)) {
5957
+ if (!revision.sourceSha256 || !existsSync8(revision.sourceFile)) {
5954
5958
  return { ready: false, reason: `revision source is missing: ${revision.sourceFile}` };
5955
5959
  }
5956
5960
  if (await sha256File(revision.sourceFile) !== revision.sourceSha256) {
5957
5961
  return { ready: false, reason: "revision source changed after the manifest was resolved" };
5958
5962
  }
5959
5963
  if (revision.maskFile) {
5960
- if (!revision.maskSha256 || !existsSync7(revision.maskFile)) {
5964
+ if (!revision.maskSha256 || !existsSync8(revision.maskFile)) {
5961
5965
  return { ready: false, reason: `revision mask is missing: ${revision.maskFile}` };
5962
5966
  }
5963
5967
  if (await sha256File(revision.maskFile) !== revision.maskSha256) {
@@ -5988,7 +5992,7 @@ async function inspectParent(spec, lock, seen) {
5988
5992
  }
5989
5993
  if (spec.source) {
5990
5994
  const source = path12.resolve(spec.root, spec.source);
5991
- return existsSync7(source) ? { ready: true, reason: "declared source is present" } : { ready: false, reason: `declared source is missing: ${spec.source}` };
5995
+ return existsSync8(source) ? { ready: true, reason: "declared source is present" } : { ready: false, reason: `declared source is missing: ${spec.source}` };
5992
5996
  }
5993
5997
  const entry = lock.entries[key];
5994
5998
  if (!entry) return { ready: false, reason: "parent is not in the lockfile" };
@@ -6002,7 +6006,7 @@ async function inspectParent(spec, lock, seen) {
6002
6006
  for (let index = 0; index < entry.outputs.length; index++) {
6003
6007
  const output = entry.outputs[index];
6004
6008
  const file = currentEntryOutputPath(entry, spec, index);
6005
- if (!existsSync7(file)) return { ready: false, reason: `parent output is missing: ${file}` };
6009
+ if (!existsSync8(file)) return { ready: false, reason: `parent output is missing: ${file}` };
6006
6010
  if (await sha256File(file) !== output.sha256) {
6007
6011
  return { ready: false, reason: `parent output was modified: ${file}` };
6008
6012
  }
@@ -6057,7 +6061,7 @@ async function buildPlan(specs, lock, opts = {}) {
6057
6061
  state = "missing";
6058
6062
  reason = "--force";
6059
6063
  } else if (!entry && spec.source && spec.generator !== "frames") {
6060
- if (existsSync8(path13.resolve(spec.root, spec.source))) {
6064
+ if (existsSync9(path13.resolve(spec.root, spec.source))) {
6061
6065
  state = "ok";
6062
6066
  reason = `placed from ${spec.source}; not generated`;
6063
6067
  } else {
@@ -6065,7 +6069,7 @@ async function buildPlan(specs, lock, opts = {}) {
6065
6069
  reason = `declared source is missing: ${spec.source}`;
6066
6070
  }
6067
6071
  } else if (!entry) {
6068
- if (existsSync8(spec.outFile)) {
6072
+ if (existsSync9(spec.outFile)) {
6069
6073
  state = "untracked";
6070
6074
  reason = "file on disk, no provenance recorded";
6071
6075
  } else {
@@ -6097,9 +6101,9 @@ async function buildPlan(specs, lock, opts = {}) {
6097
6101
  } else if (entry.status !== "downloaded") {
6098
6102
  state = "in-flight";
6099
6103
  reason = `awaiting ${entry.status}`;
6100
- } else if (entry.outputs.length === 0 || entry.outputs.some((_output, index) => !existsSync8(currentEntryOutputPath(entry, spec, index)))) {
6104
+ } else if (entry.outputs.length === 0 || entry.outputs.some((_output, index) => !existsSync9(currentEntryOutputPath(entry, spec, index)))) {
6101
6105
  state = "orphaned";
6102
- reason = entry.outputs.length === 0 ? "no recorded output files" : `missing on disk: ${entry.outputs.filter((_output, index) => !existsSync8(currentEntryOutputPath(entry, spec, index))).map((o) => o.role ?? "asset").join(", ")}`;
6106
+ reason = entry.outputs.length === 0 ? "no recorded output files" : `missing on disk: ${entry.outputs.filter((_output, index) => !existsSync9(currentEntryOutputPath(entry, spec, index))).map((o) => o.role ?? "asset").join(", ")}`;
6103
6107
  } else if (await anyOutputModified(entry, spec)) {
6104
6108
  state = "orphaned";
6105
6109
  reason = "output modified since download";
@@ -6427,7 +6431,7 @@ async function poll(provider, lock, lockPath, opts = {}) {
6427
6431
  }
6428
6432
 
6429
6433
  // src/pipeline/fetch.ts
6430
- import { existsSync as existsSync9 } from "fs";
6434
+ import { existsSync as existsSync10 } from "fs";
6431
6435
  import { mkdir as mkdir3, readFile as readFile7, rename as rename3, rm as rm4, writeFile as writeFile3 } from "fs/promises";
6432
6436
  import path14 from "path";
6433
6437
 
@@ -6483,7 +6487,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
6483
6487
  if (opts.refresh) return e.outputs.length > 0 && Boolean(e.sourceUrls?.length || e.sourceUrl);
6484
6488
  if (!opts.repair) return false;
6485
6489
  const spec = specByKey.get(key);
6486
- return e.outputs.length === 0 || !spec || e.outputs.some((output) => !existsSync9(resolveOutputPath(output.path, spec.root)));
6490
+ return e.outputs.length === 0 || !spec || e.outputs.some((output) => !existsSync10(resolveOutputPath(output.path, spec.root)));
6487
6491
  });
6488
6492
  const concurrency = Math.min(Math.max(1, opts.concurrency ?? 8), Math.max(1, pending.length));
6489
6493
  let cursor = 0;
@@ -6531,7 +6535,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
6531
6535
  continue;
6532
6536
  }
6533
6537
  }
6534
- if (existsSync9(target)) {
6538
+ if (existsSync10(target)) {
6535
6539
  const currentHash = await sha256File(target);
6536
6540
  if (recorded && currentHash === recorded.sha256) {
6537
6541
  if (!fresh) {
@@ -6642,7 +6646,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
6642
6646
  }
6643
6647
  async function readCachedMedia(cacheDir, hash, mediaType) {
6644
6648
  const file = path14.join(cacheDir, cacheFileName(hash, mediaType));
6645
- if (!existsSync9(file)) return null;
6649
+ if (!existsSync10(file)) return null;
6646
6650
  try {
6647
6651
  const buf = await readFile7(file);
6648
6652
  if (sha256(buf) !== hash) return null;
@@ -6663,7 +6667,7 @@ async function cacheMedia(cacheDir, buf, mediaType, knownHash) {
6663
6667
  try {
6664
6668
  await rename3(tmp, file);
6665
6669
  } catch (err) {
6666
- if (!existsSync9(file)) throw err;
6670
+ if (!existsSync10(file)) throw err;
6667
6671
  } finally {
6668
6672
  await rm4(tmp, { force: true });
6669
6673
  }
@@ -6696,7 +6700,7 @@ async function pushTags(provider, specs, lock, opts = {}) {
6696
6700
  }
6697
6701
 
6698
6702
  // src/pipeline/doctor.ts
6699
- import { constants, existsSync as existsSync10 } from "fs";
6703
+ import { constants, existsSync as existsSync11 } from "fs";
6700
6704
  import { access, stat as stat2 } from "fs/promises";
6701
6705
  import path15 from "path";
6702
6706
  async function doctor(loaded, specs, lock, lockPath, opts = {}) {
@@ -6768,7 +6772,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
6768
6772
  cacheDir,
6769
6773
  cacheFileName(output.sha256, output.mediaType ?? MediaType.PNG)
6770
6774
  );
6771
- if (existsSync10(cached) && await sha256File(cached) === output.sha256) {
6775
+ if (existsSync11(cached) && await sha256File(cached) === output.sha256) {
6772
6776
  hasCache = true;
6773
6777
  break;
6774
6778
  }
@@ -6871,13 +6875,13 @@ async function nearestExistingDirectory(start) {
6871
6875
  }
6872
6876
 
6873
6877
  // src/pipeline/adopt.ts
6874
- import { existsSync as existsSync12 } from "fs";
6878
+ import { existsSync as existsSync13 } from "fs";
6875
6879
  import { readFile as readFile9, writeFile as writeFile5 } from "fs/promises";
6876
6880
  import path16 from "path";
6877
6881
 
6878
6882
  // src/cache.ts
6879
6883
  import { mkdir as mkdir4, readFile as readFile8, rename as rename4, rm as rm5, writeFile as writeFile4 } from "fs/promises";
6880
- import { existsSync as existsSync11 } from "fs";
6884
+ import { existsSync as existsSync12 } from "fs";
6881
6885
  import pathModule from "path";
6882
6886
  import { z as z3 } from "zod";
6883
6887
  var HashCacheSchema = z3.object({
@@ -6893,7 +6897,7 @@ function parseCache(value) {
6893
6897
  return HashCacheSchema.parse(value);
6894
6898
  }
6895
6899
  async function loadCache(path31) {
6896
- if (!existsSync11(path31)) return { version: 1, hashes: {} };
6900
+ if (!existsSync12(path31)) return { version: 1, hashes: {} };
6897
6901
  try {
6898
6902
  const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile8(path31, "utf8")));
6899
6903
  if (!parsed.success) return { version: 1, hashes: {} };
@@ -6949,7 +6953,7 @@ async function adopt(provider, specs, lock, lockPath, opts = {}) {
6949
6953
  const localByHash = /* @__PURE__ */ new Map();
6950
6954
  const unmatchedLocal = [];
6951
6955
  for (const spec of specs) {
6952
- if (!existsSync12(spec.outFile)) {
6956
+ if (!existsSync13(spec.outFile)) {
6953
6957
  unmatchedLocal.push(`${spec.styleId}/${spec.assetId} (no file at ${spec.outFile})`);
6954
6958
  continue;
6955
6959
  }
@@ -7106,12 +7110,12 @@ function formatUnmatchedRemote(objects, limit = 15) {
7106
7110
  }
7107
7111
 
7108
7112
  // src/pick/server.ts
7109
- import { existsSync as existsSync14 } from "fs";
7113
+ import { existsSync as existsSync15 } from "fs";
7110
7114
  import { readFile as readFile12 } from "fs/promises";
7111
7115
 
7112
7116
  // src/pipeline/init.ts
7113
7117
  import { readdir, readFile as readFile10, writeFile as writeFile6 } from "fs/promises";
7114
- import { existsSync as existsSync13 } from "fs";
7118
+ import { existsSync as existsSync14 } from "fs";
7115
7119
  import path17 from "path";
7116
7120
  function pngSize(buf) {
7117
7121
  if (buf.length < 24) return null;
@@ -7204,7 +7208,7 @@ function mode(values) {
7204
7208
  return [...counts.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? 64;
7205
7209
  }
7206
7210
  async function writeManifestFile(target, manifest) {
7207
- if (existsSync13(target)) {
7211
+ if (existsSync14(target)) {
7208
7212
  throw new Error(`${target} already exists \u2014 refusing to overwrite an existing manifest.`);
7209
7213
  }
7210
7214
  await writeFile6(target, JSON.stringify(manifest, null, 2) + "\n");
@@ -7690,7 +7694,7 @@ async function currentArt(entry, spec) {
7690
7694
  const previous = primaryOutput({ ...entry, outputs: entry.supersededOutputs });
7691
7695
  if (!previous) return null;
7692
7696
  const file = resolveOutputPath(previous.path, spec.root);
7693
- if (!existsSync14(file)) return null;
7697
+ if (!existsSync15(file)) return null;
7694
7698
  const contentType = previous.mediaType ?? mediaTypeFromExtension(file);
7695
7699
  if (!contentType) return null;
7696
7700
  let width = entry.width;
@@ -7861,13 +7865,13 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
7861
7865
 
7862
7866
  // src/gallery/snapshot.ts
7863
7867
  import { createHash as createHash3 } from "crypto";
7864
- import { existsSync as existsSync17 } from "fs";
7868
+ import { existsSync as existsSync18 } from "fs";
7865
7869
  import { readFile as readFile16, stat as stat3 } from "fs/promises";
7866
7870
  import path22 from "path";
7867
7871
 
7868
7872
  // src/workspace.ts
7869
7873
  import { mkdir as mkdir5, readFile as readFile13, rename as rename5, rm as rm6, writeFile as writeFile7 } from "fs/promises";
7870
- import { existsSync as existsSync15 } from "fs";
7874
+ import { existsSync as existsSync16 } from "fs";
7871
7875
  import path18 from "path";
7872
7876
  import { z as z4 } from "zod";
7873
7877
  var WorkspaceProjectSchema = z4.object({
@@ -7893,7 +7897,7 @@ ${parsed.error.issues.slice(0, 5).map((i) => ` ${i.path.join(".")}: ${i.message
7893
7897
  );
7894
7898
  }
7895
7899
  async function loadWorkspace(workspacePath) {
7896
- if (!existsSync15(workspacePath)) return { version: 1, projects: [] };
7900
+ if (!existsSync16(workspacePath)) return { version: 1, projects: [] };
7897
7901
  let raw;
7898
7902
  try {
7899
7903
  raw = JSON.parse(await readFile13(workspacePath, "utf8"));
@@ -7945,14 +7949,14 @@ function validateWorkspace(ws, dir) {
7945
7949
  message: `project "${project.id}" stores an absolute path \u2014 the catalog will not resolve correctly if this tree is cloned or moved elsewhere`
7946
7950
  });
7947
7951
  }
7948
- if (!existsSync15(manifestPath)) {
7952
+ if (!existsSync16(manifestPath)) {
7949
7953
  diagnostics.push({
7950
7954
  id: "missing-manifest",
7951
7955
  level: "error",
7952
7956
  message: `project "${project.id}" manifest not found: ${manifestPath}`
7953
7957
  });
7954
7958
  }
7955
- if (!existsSync15(lockPath)) {
7959
+ if (!existsSync16(lockPath)) {
7956
7960
  diagnostics.push({
7957
7961
  id: "missing-lock",
7958
7962
  level: "error",
@@ -8253,7 +8257,7 @@ async function applyManifestEdit(manifestPath, edit) {
8253
8257
 
8254
8258
  // src/pipeline/hand-edit.ts
8255
8259
  import { spawn as spawn2 } from "child_process";
8256
- import { existsSync as existsSync16 } from "fs";
8260
+ import { existsSync as existsSync17 } from "fs";
8257
8261
  import { copyFile, mkdir as mkdir6, readFile as readFile15, rename as rename7, rm as rm8, writeFile as writeFile9 } from "fs/promises";
8258
8262
  import path20 from "path";
8259
8263
  import { z as z6 } from "zod";
@@ -8269,7 +8273,7 @@ function handEditPath(loaded, spec) {
8269
8273
  function handEditBases(spec, lock) {
8270
8274
  const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
8271
8275
  if (entry) {
8272
- if (entry.outputs.length !== 1 && !isFrameSetEntry(entry)) return null;
8276
+ if (entry.outputs.length !== 1 && !isMemberSetEntry(entry)) return null;
8273
8277
  const members = entry.outputs.map((output, index) => ({
8274
8278
  role: entry.outputs.length === 1 ? null : output.role ?? null,
8275
8279
  path: currentEntryOutputPath(entry, spec, index),
@@ -8277,15 +8281,15 @@ function handEditBases(spec, lock) {
8277
8281
  }));
8278
8282
  if (members.some((member) => member.mediaType && member.mediaType !== "image/png")) return null;
8279
8283
  if (entry.outputs.length === 1 && !primaryOutput(entry)) return null;
8280
- if (!members.every((member) => existsSync16(member.path))) return null;
8284
+ if (!members.every((member) => existsSync17(member.path))) return null;
8281
8285
  return members.map(({ role, path: file }) => ({ role, path: file }));
8282
8286
  }
8283
- return existsSync16(spec.outFile) ? [{ role: null, path: spec.outFile }] : null;
8287
+ return existsSync17(spec.outFile) ? [{ role: null, path: spec.outFile }] : null;
8284
8288
  }
8285
8289
  function handEditMembers(loaded, spec, lock, editPath) {
8286
8290
  const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
8287
8291
  const bases = handEditBases(spec, lock);
8288
- if (entry && isFrameSetEntry(entry)) {
8292
+ if (entry && isMemberSetEntry(entry)) {
8289
8293
  return entry.outputs.map((output, index) => ({
8290
8294
  role: output.role ?? null,
8291
8295
  path: memberPath(editPath, output.role, index, entry.outputs.length, output.mediaType),
@@ -8296,9 +8300,14 @@ function handEditMembers(loaded, spec, lock, editPath) {
8296
8300
  }
8297
8301
  async function startHandEdit(loaded, lock, spec, opts = {}) {
8298
8302
  const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
8299
- if (entry && entry.outputs.length > 1 && !isFrameSetEntry(entry)) {
8303
+ if (entry && entry.outputs.length > 1 && !isMemberSetEntry(entry)) {
8300
8304
  throw new ManifestEditError(
8301
- `${spec.styleId}/${spec.assetId} is a structural set of ${entry.outputs.length} outputs; hand edits cover single images and frame sets`
8305
+ `${spec.styleId}/${spec.assetId} is a set of ${entry.outputs.length} outputs that are not all PNG; hand edits cover PNG images and sets of them`
8306
+ );
8307
+ }
8308
+ if (entry && spec.source && isMemberSetEntry(entry) && !sourceIsStem(spec.source, entry, loaded.root)) {
8309
+ throw new ManifestEditError(
8310
+ `${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`
8302
8311
  );
8303
8312
  }
8304
8313
  const existing = spec.source ? path20.resolve(loaded.root, spec.source) : null;
@@ -8306,7 +8315,7 @@ async function startHandEdit(loaded, lock, spec, opts = {}) {
8306
8315
  const members = handEditMembers(loaded, spec, lock, editPath);
8307
8316
  let created = false;
8308
8317
  for (const member of members) {
8309
- if (existsSync16(member.path)) continue;
8318
+ if (existsSync17(member.path)) continue;
8310
8319
  if (!member.base) {
8311
8320
  throw new ManifestEditError(
8312
8321
  `${spec.styleId}/${spec.assetId} has no generated PNG on disk to start from; generate or restore it first`
@@ -8404,18 +8413,18 @@ async function readHandEditCompanion(editPath) {
8404
8413
  var MAX_HAND_EDIT_BYTES = 16 * 1024 * 1024;
8405
8414
  async function saveHandEdit(loaded, lock, spec, input) {
8406
8415
  const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
8407
- const frameSet = entry ? isFrameSetEntry(entry) : false;
8416
+ const memberSet = entry ? isMemberSetEntry(entry) : false;
8408
8417
  const incoming = input.frames ?? (input.png ? [{ role: null, png: input.png }] : []);
8409
8418
  if (!incoming.length) throw new ManifestEditError("the save carried no image");
8410
8419
  if (input.project && input.project.length > MAX_HAND_EDIT_BYTES) {
8411
8420
  throw new ManifestEditError(`the project file is ${input.project.length} bytes; the limit is ${MAX_HAND_EDIT_BYTES}`);
8412
8421
  }
8413
- if (frameSet) {
8422
+ if (memberSet) {
8414
8423
  const expected = entry.outputs.map((output) => output.role ?? null);
8415
8424
  const got = incoming.map((frame) => frame.role);
8416
8425
  if (got.length !== expected.length || expected.some((role, index) => got[index] !== role)) {
8417
8426
  throw new ManifestEditError(
8418
- `${spec.styleId}/${spec.assetId} is a set of ${expected.length} frames (${expected.join(", ")}); the editor returned ${got.length} (${got.map((role) => role ?? "new").join(", ")}) \u2014 keep the frames it opened with`
8427
+ `${spec.styleId}/${spec.assetId} is a set of ${expected.length} members (${expected.join(", ")}); the editor returned ${got.length} (${got.map((role) => role ?? "new").join(", ")}) \u2014 keep the frames it opened with`
8419
8428
  );
8420
8429
  }
8421
8430
  } else if (incoming.length !== 1) {
@@ -8429,7 +8438,7 @@ async function saveHandEdit(loaded, lock, spec, input) {
8429
8438
  validateMedia(frame.png, MediaType.PNG);
8430
8439
  return decodePng(frame.png);
8431
8440
  } catch (error) {
8432
- const which = frameSet ? `frame ${frame.role ?? index}` : "the edit";
8441
+ const which = memberSet ? `member ${frame.role ?? index}` : "the edit";
8433
8442
  throw new ManifestEditError(`${which} is not a valid PNG: ${error instanceof Error ? error.message : String(error)}`);
8434
8443
  }
8435
8444
  });
@@ -8437,7 +8446,7 @@ async function saveHandEdit(loaded, lock, spec, input) {
8437
8446
  const expectedSize = bases?.[0] ? decodePng(await readFile15(bases[0].path)) : { width: spec.width, height: spec.height };
8438
8447
  for (const [index, image] of decoded.entries()) {
8439
8448
  if (image.width !== expectedSize.width || image.height !== expectedSize.height) {
8440
- const which = frameSet ? `frame ${incoming[index].role ?? index}` : "the edit";
8449
+ const which = memberSet ? `member ${incoming[index].role ?? index}` : "the edit";
8441
8450
  throw new ManifestEditError(
8442
8451
  `${which} is ${image.width}\xD7${image.height}; ${spec.styleId}/${spec.assetId} is ${expectedSize.width}\xD7${expectedSize.height}`
8443
8452
  );
@@ -8671,7 +8680,7 @@ async function describeQuality(media, root, inspection) {
8671
8680
  outputPaths.map((file) => describeOutput(media, root, path22.resolve(root, file)))
8672
8681
  );
8673
8682
  const recordPath = path22.resolve(root, inspection.record);
8674
- const recordExists = existsSync17(recordPath);
8683
+ const recordExists = existsSync18(recordPath);
8675
8684
  let options = null;
8676
8685
  let check = null;
8677
8686
  if (recordExists) {
@@ -8753,11 +8762,11 @@ async function buildGallerySnapshot(opts) {
8753
8762
  );
8754
8763
  if (spec.source) {
8755
8764
  const editPath = path22.resolve(root, spec.source);
8756
- const memberCount = isFrameSetEntry(entry) ? entry.outputs.length : 1;
8765
+ const memberCount = sourceIsStem(spec.source, entry, root) ? entry.outputs.length : 1;
8757
8766
  const editShas = await Promise.all(
8758
8767
  Array.from({ length: memberCount }, async (_, index) => {
8759
8768
  const file = sourceOutputPath(spec.source, entry, index, root);
8760
- return { file, sha256: existsSync17(file) ? await sha256File(file) : null };
8769
+ return { file, sha256: existsSync18(file) ? await sha256File(file) : null };
8761
8770
  })
8762
8771
  );
8763
8772
  edits = await Promise.all(editShas.map(({ file, sha256: editSha }) => describeOutput(media, root, file, { sha256: editSha })));
@@ -8765,7 +8774,7 @@ async function buildGallerySnapshot(opts) {
8765
8774
  const companion = await readHandEditCompanion(editPath);
8766
8775
  if (companion) {
8767
8776
  const projectPath = handEditProjectPath(editPath);
8768
- const project = companion.project && existsSync17(projectPath) ? await fileInfo(projectPath) : null;
8777
+ const project = companion.project && existsSync18(projectPath) ? await fileInfo(projectPath) : null;
8769
8778
  let projectUrl = null;
8770
8779
  if (project) {
8771
8780
  const id = galleryMediaId(projectPath);
@@ -8802,7 +8811,7 @@ async function buildGallerySnapshot(opts) {
8802
8811
  }
8803
8812
  } else if (spec.source) {
8804
8813
  outputs = [await describeOutput(media, root, path22.resolve(root, spec.source))];
8805
- } else if (!entry && existsSync17(spec.outFile)) {
8814
+ } else if (!entry && existsSync18(spec.outFile)) {
8806
8815
  outputs = [await describeOutput(media, root, spec.outFile)];
8807
8816
  } else {
8808
8817
  outputs = [];
@@ -9355,6 +9364,7 @@ function renderGallery(snapshot, opts = {}) {
9355
9364
  .sheet.editor-host { width:min(1400px, 96vw); grid-template-rows:auto 1fr; }
9356
9365
  .sheet.editor-host .rbar b { font:650 12.5px/1.4 var(--mono); color:var(--text); }
9357
9366
  .sheet.editor-host .rbar .acts { margin-left:auto; display:flex; gap:8px; align-items:center; }
9367
+ .sheet.editor-host .rbar .chip { color:var(--dim); font-size:12.5px; display:inline-flex; align-items:center; gap:5px; margin-right:6px; }
9358
9368
  .sheet.editor-host .rbar .st { display:inline-flex; align-items:center; gap:6px; font:650 11.5px/1 var(--mono); }
9359
9369
  .sheet.editor-host .rbar .msg { color:var(--bad); font-size:12.5px; }
9360
9370
  .sheet.editor-host .stage { position:relative; min-height:0; }
@@ -9879,15 +9889,20 @@ function openEditorSheet(item) {
9879
9889
  const bar = el('div', 'rbar');
9880
9890
  const close = el('button', null, 'Close'); close.type = 'button';
9881
9891
  close.onclick = () => closeEditorSheet();
9882
- const title = el('b', null, (item.project ? item.project + ':' : '') + item.styleId + '/' + item.assetId + ' \xB7 ' + item.width + '\xD7' + item.height + (item.outputs.length > 1 ? ' \xB7 ' + item.outputs.length + ' frames' : ''));
9892
+ const title = el('b', null, (item.project ? item.project + ':' : '') + item.styleId + '/' + item.assetId + ' \xB7 ' + item.width + '\xD7' + item.height + (item.outputs.length > 1 ? ' \xB7 ' + item.outputs.length + ' ' + memberNoun(item, item.outputs.length) : ''));
9883
9893
  const status = el('span', 'st');
9884
9894
  const acts = el('div', 'acts');
9885
9895
  const msg = el('span', 'msg');
9896
+ const compare = el('label', 'chip'); compare.hidden = true;
9897
+ compare.title = 'The generated art on a locked layer at 50%, for comparison; never part of the saved file';
9898
+ const compareBox = el('input'); compareBox.type = 'checkbox'; compareBox.checked = true;
9899
+ compareBox.onchange = () => showReference(compareBox.checked);
9900
+ compare.append(compareBox, document.createTextNode(' show generated'));
9886
9901
  const save = el('button', 'primary', 'Save to project'); save.type = 'button';
9887
9902
  save.onclick = () => requestEditorSave(false);
9888
9903
  const saveClose = el('button', null, 'Save & close'); saveClose.type = 'button';
9889
9904
  saveClose.onclick = () => requestEditorSave(true);
9890
- acts.append(msg, save, saveClose);
9905
+ acts.append(msg, compare, save, saveClose);
9891
9906
  bar.append(close, title, status, acts);
9892
9907
  const stage = el('div', 'stage');
9893
9908
  const frame = el('iframe');
@@ -9900,7 +9915,7 @@ function openEditorSheet(item) {
9900
9915
  stage.append(frame, loading);
9901
9916
  panel.append(bar, stage);
9902
9917
  host.append(scrim, panel);
9903
- SHEET = { item, frame, loading, status, msg, save, saveClose, dirty: false, opened: false, ready: null, pending: null, requests: 0, source: src, sentProject: false, roles: null };
9918
+ SHEET = { item, frame, loading, status, msg, save, saveClose, compare, dirty: false, opened: false, ready: null, pending: null, requests: 0, source: src, sentProject: false, sentReference: false, reference: false, roles: null };
9904
9919
  sheetStatus('cool', 'loading');
9905
9920
  sheetButtons();
9906
9921
  close.focus({ preventScroll: true });
@@ -9916,11 +9931,15 @@ async function sendOpen() {
9916
9931
  const projectUrl = sources.fromEdit && item.editMeta && item.editMeta.projectUrl && SHEET.ready.version >= 2 ? item.editMeta.projectUrl : null;
9917
9932
  const set = sources.files.length > 1;
9918
9933
  if (set && SHEET.ready.version < 3) {
9919
- SHEET.msg.textContent = 'This editor build cannot open frame sets; reinstall it with pixelkiln tools install editor.';
9934
+ SHEET.msg.textContent = 'This editor build cannot open sets; reinstall it with pixelkiln tools install editor.';
9920
9935
  sheetStatus('bad', 'unsupported');
9921
9936
  return;
9922
9937
  }
9938
+ // When an edit is being opened, the generated art rides along as a
9939
+ // reference layer: what the editor shows through at 50% for comparison.
9940
+ const referenceFiles = sources.fromEdit && SHEET.ready.version >= 4 && item.outputs.every((o) => o.exists && o.url) ? item.outputs : null;
9923
9941
  const pngs = [];
9942
+ const reference = [];
9924
9943
  let pxo = null;
9925
9944
  try {
9926
9945
  for (const file of sources.files) {
@@ -9932,6 +9951,10 @@ async function sendOpen() {
9932
9951
  const p = await fetch(projectUrl, { cache: 'no-store' });
9933
9952
  if (p.ok) pxo = await p.arrayBuffer();
9934
9953
  }
9954
+ for (const file of referenceFiles || []) {
9955
+ const res = await fetch(file.url, { cache: 'no-store' });
9956
+ if (res.ok) reference.push({ role: file.role || null, png: await res.arrayBuffer() });
9957
+ }
9935
9958
  } catch (err) {
9936
9959
  if (SHEET) { SHEET.msg.textContent = 'Could not load the image: ' + err.message; sheetStatus('bad', 'failed'); }
9937
9960
  return;
@@ -9948,16 +9971,26 @@ async function sendOpen() {
9948
9971
  const transfer = [pngs[0]];
9949
9972
  if (set) {
9950
9973
  message.frames = pngs.map((png, i) => ({ role: SHEET.roles[i], png }));
9951
- message.fps = item.fps || 12;
9974
+ // A tile set is not an animation; a slow rate keeps its timeline still-ish.
9975
+ message.fps = isFrameSet(item) ? (item.fps || 12) : 1;
9952
9976
  for (const png of pngs.slice(1)) transfer.push(png);
9953
9977
  // The first frame is both png (older editors) and frames[0]; copy it so both transfer.
9954
9978
  message.png = pngs[0].slice(0);
9955
9979
  transfer.push(message.png);
9956
9980
  }
9957
9981
  if (pxo) { message.pxo = pxo; transfer.push(pxo); }
9982
+ if (reference.length === (referenceFiles || []).length && reference.length) {
9983
+ message.reference = reference;
9984
+ for (const r of reference) transfer.push(r.png);
9985
+ }
9986
+ SHEET.sentReference = !!message.reference;
9958
9987
  SHEET.frame.contentWindow.postMessage(message, location.origin, transfer);
9959
9988
  sheetStatus('cool', 'opening');
9960
9989
  }
9990
+ function showReference(visible) {
9991
+ if (!SHEET || !SHEET.opened || !SHEET.reference) return;
9992
+ SHEET.frame.contentWindow.postMessage({ type: 'pixelkiln:reference', visible }, location.origin);
9993
+ }
9961
9994
  function requestEditorSave(close) {
9962
9995
  if (!SHEET || !SHEET.opened || SHEET.pending) return;
9963
9996
  const request = 'save-' + (++SHEET.requests);
@@ -9981,10 +10014,13 @@ async function onEditorMessage(m) {
9981
10014
  case 'pixelkiln:opened':
9982
10015
  SHEET.opened = true;
9983
10016
  SHEET.loading.remove();
9984
- if (m.source === 'pxo') sheetStatus('ok', 'editing the edit file' + (m.frames > 1 ? 's' : '') + ' with ' + (m.layers === 1 ? 'the layer' : m.layers + ' layers') + (m.frames > 1 ? ' and ' + m.frames + ' frames' : '') + ' restored');
10017
+ SHEET.reference = !!m.reference;
10018
+ SHEET.compare.hidden = !SHEET.reference;
10019
+ if (m.source === 'pxo') sheetStatus('ok', 'editing the edit file' + (m.frames > 1 ? 's' : '') + ' with ' + (m.layers === 1 ? 'the layer' : m.layers + ' layers') + (m.frames > 1 ? ' and ' + m.frames + ' ' + memberNoun(SHEET.item, m.frames) : '') + ' restored');
9985
10020
  else if (SHEET.sentProject) sheetStatus('warn', 'editing the flattened edit file' + (m.frames > 1 ? 's' : '') + '; the layer file could not be opened');
9986
- else if (m.source === 'frames') sheetStatus('ok', (SHEET.item.edits.length ? 'editing the edit files' : 'editing a copy of the generated frames') + ' as ' + m.frames + ' frames');
10021
+ else if (m.source === 'frames') sheetStatus('ok', (SHEET.item.edits.length ? 'editing the edit files' : 'editing a copy of the generated ' + memberNoun(SHEET.item, m.frames)) + ' as ' + m.frames + ' frames' + (isFrameSet(SHEET.item) ? '' : ', one per ' + memberNoun(SHEET.item, 1)));
9987
10022
  else sheetStatus('ok', SHEET.item.edit && SHEET.item.edit.exists ? 'editing the edit file' : 'editing a copy of the generated art');
10023
+ if (SHEET.reference) SHEET.status.append(document.createTextNode(' \xB7 generated art shown through at 50%'));
9988
10024
  sheetButtons();
9989
10025
  break;
9990
10026
  case 'pixelkiln:dirty':
@@ -10010,7 +10046,7 @@ async function onEditorMessage(m) {
10010
10046
  if (saved) SHEET.item = saved;
10011
10047
  SHEET.dirty = false;
10012
10048
  SHEET.pending = null;
10013
- ui.notice = { id: SHEET.item.id, text: 'Saved ' + (saved && saved.edits.length > 1 ? saved.edits.length + ' frames to ' + saved.source : saved && saved.edit ? saved.edit.path : 'the edit') + '. mount and pack place ' + (saved && saved.edits.length > 1 ? 'them' : 'it') + ' in place of the generated art.' };
10049
+ ui.notice = { id: SHEET.item.id, text: 'Saved ' + (saved && saved.edits.length > 1 ? saved.edits.length + ' ' + memberNoun(saved, saved.edits.length) + ' to ' + saved.source : saved && saved.edit ? saved.edit.path : 'the edit') + '. mount and pack place ' + (saved && saved.edits.length > 1 ? 'them' : 'it') + ' in place of the generated art.' };
10014
10050
  render();
10015
10051
  if (pending.close) { closeEditorSheet(true); return; }
10016
10052
  sheetStatus('ok', 'saved');
@@ -10583,16 +10619,18 @@ async function postHandEdit(item, action, extra) {
10583
10619
  if (item.project) body.project = item.project;
10584
10620
  snap = await postEdit(body);
10585
10621
  }
10586
- // A single PNG, or an ordered frame set: both are hand-editable, one file per
10587
- // member. Structural sets (tiles, characters) are not.
10588
- const editableOutputs = (item) => item.outputs.length && (item.outputs.length === 1 || isFrameSet(item)) &&
10589
- item.outputs.every((o) => o.exists && o.mediaType === 'image/png');
10622
+ // A single PNG or a set of them \u2014 frames, tiles \u2014 is hand-editable, one file
10623
+ // per member; a set with a GIF in it is not.
10624
+ const editableOutputs = (item) => item.outputs.length && item.outputs.every((o) => o.exists && o.mediaType === 'image/png');
10625
+ // What the members of a set are called: frames for an animation, tiles for a tile set.
10626
+ const memberNoun = (item, n) => isFrameSet(item) ? (n === 1 ? 'frame' : 'frames')
10627
+ : item.outputs.every((o) => o.role && /^tile/i.test(o.role)) ? (n === 1 ? 'tile' : 'tiles') : (n === 1 ? 'member' : 'members');
10590
10628
  function handEditSection(item) {
10591
10629
  const canStart = EDITABLE && item.declared && item.asset && projectOf(item)?.manifestSha256 && editableOutputs(item);
10592
10630
  if (!item.edit && !canStart) return null;
10593
10631
  const set = item.edits.length > 1;
10594
10632
  const s = el('section', 'meta');
10595
- s.append(el('h3', null, set ? 'Hand edit (' + item.edits.length + ' frames)' : 'Hand edit'));
10633
+ s.append(el('h3', null, set ? 'Hand edit (' + item.edits.length + ' ' + memberNoun(item, item.edits.length) + ')' : 'Hand edit'));
10596
10634
  if (item.edit) {
10597
10635
  const pair = el('div', 'pair');
10598
10636
  const shared = displayScale(item.width, item.height, 200, 160);
@@ -10608,7 +10646,7 @@ function handEditSection(item) {
10608
10646
  };
10609
10647
  // For a set, the member the preview is on; the strip there steps through them.
10610
10648
  const at = set ? Math.min(ui.member, item.edits.length - 1) : 0;
10611
- const which = set ? ' \xB7 ' + (item.outputs[at].role || 'frame ' + (at + 1)) : '';
10649
+ const which = set ? ' \xB7 ' + (item.outputs[at].role || memberNoun(item, 1) + ' ' + (at + 1)) : '';
10612
10650
  pair.append(fig(item.outputs[at], 'generated' + which), fig(item.edits[at], 'edit' + which));
10613
10651
  s.append(pair);
10614
10652
  const dl = el('dl');
@@ -10617,9 +10655,9 @@ function handEditSection(item) {
10617
10655
  row(dl, 'status', st);
10618
10656
  row(dl, 'why', EDIT_STATUS_TEXT[item.editStatus] || '');
10619
10657
  if (set) {
10620
- row(dl, 'files', item.source + ' \u2014 one per frame: ' + item.edits.map((e) => e.path.split('/').pop()).join(', '), { mono: true, copy: item.edits.map((e) => e.absolutePath).join('\\n') });
10658
+ row(dl, 'files', item.source + ' \u2014 one per ' + memberNoun(item, 1) + ': ' + item.edits.map((e) => e.path.split('/').pop()).join(', '), { mono: true, copy: item.edits.map((e) => e.absolutePath).join('\\n') });
10621
10659
  const differing = item.editChanged.filter(Boolean).length;
10622
- row(dl, 'changed', differing + ' of ' + item.edits.length + ' frames differ from the generated art' +
10660
+ row(dl, 'changed', differing + ' of ' + item.edits.length + ' ' + memberNoun(item, item.edits.length) + ' differ from the generated art' +
10623
10661
  (differing ? ': ' + item.edits.filter((e, i) => item.editChanged[i]).map((e) => item.outputs[item.edits.indexOf(e)].role || e.path.split('/').pop()).join(', ') : ''));
10624
10662
  } else {
10625
10663
  row(dl, 'file', item.edit.path, { mono: true, copy: item.edit.absolutePath });
@@ -10633,8 +10671,8 @@ function handEditSection(item) {
10633
10671
  }
10634
10672
  s.append(dl);
10635
10673
  } else {
10636
- s.append(el('div', 'state-dim', isFrameSet(item)
10637
- ? 'Touch the frames up in your own editor or in the browser, where they open as one animation. The generated files stay untouched; the edit is one sibling file per frame that the manifest points at, and mount and pack place those instead.'
10674
+ s.append(el('div', 'state-dim', item.outputs.length > 1
10675
+ ? 'Touch the ' + memberNoun(item, 2) + ' up in your own editor or in the browser, where they open as ' + (isFrameSet(item) ? 'one animation' : 'the frames of one project') + '. The generated files stay untouched; the edit is one sibling file per ' + memberNoun(item, 1) + ' that the manifest points at, and mount, pack, and export place those instead.'
10638
10676
  : 'Touch it up in your own editor. The generated file stays untouched; the edit is a sibling file the manifest points at, and mount and pack place that instead.'));
10639
10677
  }
10640
10678
  if (EDITABLE && item.declared && projectOf(item)?.manifestSha256) {
@@ -10665,8 +10703,9 @@ function handEditSection(item) {
10665
10703
  go('Open in desktop editor', 'start-edit', { open: true }, 'Opened ' + item.edit.path + ' in your editor. Save there, then Refresh.');
10666
10704
  go('Detach edit', 'detach-edit', {}, 'Detached. The file' + (set ? 's are' : ' is') + ' still at ' + item.source + '; the generated art is placed again.');
10667
10705
  } else if (canStart) {
10668
- go('Edit by hand', 'start-edit', { open: true }, 'Created the edit file' + (isFrameSet(item) ? 's and opened the first frame' : ' and opened it') + ' in your editor. Save there, then Refresh to see it here.');
10669
- go('Create edit file' + (isFrameSet(item) ? 's' : '') + ' only', 'start-edit', { open: false }, 'Created the edit file' + (isFrameSet(item) ? 's' : '') + ' and declared ' + (isFrameSet(item) ? 'them' : 'it') + ' in the manifest.');
10706
+ const many = item.outputs.length > 1;
10707
+ go('Edit by hand', 'start-edit', { open: true }, 'Created the edit file' + (many ? 's and opened the first ' + memberNoun(item, 1) : ' and opened it') + ' in your editor. Save there, then Refresh to see it here.');
10708
+ go('Create edit file' + (many ? 's' : '') + ' only', 'start-edit', { open: false }, 'Created the edit file' + (many ? 's' : '') + ' and declared ' + (many ? 'them' : 'it') + ' in the manifest.');
10670
10709
  }
10671
10710
  acts.append(msg);
10672
10711
  s.append(acts);
@@ -11969,7 +12008,7 @@ import path24 from "path";
11969
12008
 
11970
12009
  // src/editor/install.ts
11971
12010
  import { createHash as createHash4 } from "crypto";
11972
- import { existsSync as existsSync18 } from "fs";
12011
+ import { existsSync as existsSync19 } from "fs";
11973
12012
  import { mkdir as mkdir7, readFile as readFile18, rename as rename8, rm as rm9, stat as stat4, writeFile as writeFile10 } from "fs/promises";
11974
12013
  import os2 from "os";
11975
12014
  import path23 from "path";
@@ -11979,8 +12018,8 @@ var pin_default = {
11979
12018
  pixelorama: "v1.2.2",
11980
12019
  godot: "4.7.2",
11981
12020
  extensionsApi: 9,
11982
- protocol: 3,
11983
- release: "editor-pixelorama-v1.2.2-pk.14",
12021
+ protocol: 4,
12022
+ release: "editor-pixelorama-v1.2.2-pk.18",
11984
12023
  files: {
11985
12024
  "index.apple-touch-icon.png": {
11986
12025
  sha256: "ee5434873e0fed91e9a888c3ea5a8b79beac25999308be7a6eb41bf4ce640d27",
@@ -11995,7 +12034,7 @@ var pin_default = {
11995
12034
  bytes: 7298
11996
12035
  },
11997
12036
  "index.html": {
11998
- sha256: "2974146c89f4df9f796e840947c334aaf3e4fa3dd18bcd7e08db2eabfd0f086e",
12037
+ sha256: "d9adf06036660c4b76ef5386e872bc03dc0d5b0628fd6ac660d480893ee1f47a",
11999
12038
  bytes: 5442
12000
12039
  },
12001
12040
  "index.icon.png": {
@@ -12007,8 +12046,8 @@ var pin_default = {
12007
12046
  bytes: 279815
12008
12047
  },
12009
12048
  "index.pck": {
12010
- sha256: "736bd383c82fdadf31cc894caf08490b99c5df572aa20db683972c35c7a0465b",
12011
- bytes: 6429628
12049
+ sha256: "a41d9c3a39aa99ea5ee49ddcedfeb8b092006aa75c0de67139f13297c17f42e4",
12050
+ bytes: 6431692
12012
12051
  },
12013
12052
  "index.png": {
12014
12053
  sha256: "6e3ca3eb4a99e4c07bdd89ebc873b440ec0ffd66ce1955160da9e7ed80d538d7",
@@ -12068,7 +12107,7 @@ async function editorStatus(opts = {}) {
12068
12107
  for (const [name, expected] of Object.entries(pin.files)) {
12069
12108
  totalBytes += expected.bytes;
12070
12109
  const file = path23.join(dir, name);
12071
- const info = existsSync18(file) ? await stat4(file).catch(() => null) : null;
12110
+ const info = existsSync19(file) ? await stat4(file).catch(() => null) : null;
12072
12111
  if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
12073
12112
  installedBytes += expected.bytes;
12074
12113
  } else {
@@ -12263,7 +12302,7 @@ function createGalleryEditorHandlers(opts = {}) {
12263
12302
 
12264
12303
  // src/pipeline/salvage.ts
12265
12304
  import { readFile as readFile20 } from "fs/promises";
12266
- import { existsSync as existsSync19 } from "fs";
12305
+ import { existsSync as existsSync20 } from "fs";
12267
12306
  import path25 from "path";
12268
12307
  function providerClaimId(provider, objectId) {
12269
12308
  return `${encodeURIComponent(provider)}:${encodeURIComponent(objectId)}`;
@@ -12271,7 +12310,7 @@ function providerClaimId(provider, objectId) {
12271
12310
  async function loadClaims(lockPaths, opts = {}) {
12272
12311
  const claimed = /* @__PURE__ */ new Set();
12273
12312
  for (const p of lockPaths) {
12274
- if (!existsSync19(p)) throw new Error(`Claim lockfile not found: ${p}`);
12313
+ if (!existsSync20(p)) throw new Error(`Claim lockfile not found: ${p}`);
12275
12314
  let parsed;
12276
12315
  try {
12277
12316
  parsed = parseLock(JSON.parse(await readFile20(p, "utf8")));
@@ -12332,7 +12371,7 @@ async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, cla
12332
12371
  const siblings = [];
12333
12372
  for (const siblingManifestPath of siblingManifestPaths) {
12334
12373
  if (path25.resolve(siblingManifestPath) === own) continue;
12335
- if (!existsSync19(siblingManifestPath)) continue;
12374
+ if (!existsSync20(siblingManifestPath)) continue;
12336
12375
  try {
12337
12376
  const { manifest } = await loadManifest(siblingManifestPath);
12338
12377
  siblings.push({ label: path25.basename(path25.dirname(siblingManifestPath)), manifest });
@@ -12554,11 +12593,11 @@ async function workspaceStatus(ws, dir) {
12554
12593
  }
12555
12594
 
12556
12595
  // src/pipeline/cache-health.ts
12557
- import { existsSync as existsSync20 } from "fs";
12596
+ import { existsSync as existsSync21 } from "fs";
12558
12597
  import { readFile as readFile21, readdir as readdir2, rm as rm10 } from "fs/promises";
12559
12598
  import path26 from "path";
12560
12599
  async function inspectCaches(lock, lockPath, options = {}) {
12561
- if (options.prune && !existsSync20(lockPath)) {
12600
+ if (options.prune && !existsSync21(lockPath)) {
12562
12601
  throw new Error(`Refusing to prune without an existing lockfile at ${path26.resolve(lockPath)}`);
12563
12602
  }
12564
12603
  const contentDir = path26.resolve(path26.dirname(lockPath), ".pixelkiln", "cache");
@@ -12603,7 +12642,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
12603
12642
  async function inspectContentCache(contentDir, referenced) {
12604
12643
  const report = {
12605
12644
  path: contentDir,
12606
- exists: existsSync20(contentDir),
12645
+ exists: existsSync21(contentDir),
12607
12646
  files: 0,
12608
12647
  bytes: 0,
12609
12648
  valid: 0,
@@ -12667,7 +12706,7 @@ async function inspectContentCache(contentDir, referenced) {
12667
12706
  async function inspectRemoteHashCache(remotePath) {
12668
12707
  const report = {
12669
12708
  path: remotePath,
12670
- exists: existsSync20(remotePath),
12709
+ exists: existsSync21(remotePath),
12671
12710
  entries: 0,
12672
12711
  valid: 0,
12673
12712
  invalidIds: [],
@@ -12717,8 +12756,11 @@ function normalizeTileRules(raw) {
12717
12756
  };
12718
12757
  }
12719
12758
  function exportTileset(entry, spec, options) {
12720
- const outputs = resolveSpecEntryOutputs(entry, spec);
12759
+ let outputs = resolveSpecEntryOutputs(entry, spec);
12721
12760
  if (!outputs.length) throw new Error(`${spec.styleId}/${spec.assetId} has no downloaded outputs`);
12761
+ if (spec.source && sourceIsStem(spec.source, entry, options.manifestDir)) {
12762
+ outputs = outputs.map((output) => ({ ...output, absolutePath: sourceOutputPath(spec.source, entry, output.index, options.manifestDir) }));
12763
+ }
12722
12764
  const packed = packSprites(
12723
12765
  outputs.map((output) => ({
12724
12766
  id: output.role ?? fallbackOutputRole(output.index),
@@ -13212,7 +13254,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
13212
13254
 
13213
13255
  // src/recipes.ts
13214
13256
  import path28 from "path";
13215
- import { existsSync as existsSync21 } from "fs";
13257
+ import { existsSync as existsSync22 } from "fs";
13216
13258
  import { readdir as readdir3, readFile as readFile23 } from "fs/promises";
13217
13259
  import { fileURLToPath } from "url";
13218
13260
  import { z as z9 } from "zod";
@@ -13415,7 +13457,7 @@ async function readRecipeFile(recipePath, bundled) {
13415
13457
  return { recipe: parsed.data, path: absolute, dir: path28.dirname(absolute), bundled };
13416
13458
  }
13417
13459
  async function walkRecipeFiles(root) {
13418
- if (!existsSync21(root)) return [];
13460
+ if (!existsSync22(root)) return [];
13419
13461
  const out = [];
13420
13462
  for (const entry of await readdir3(root, { withFileTypes: true })) {
13421
13463
  const resolved = path28.join(root, entry.name);
@@ -13453,7 +13495,7 @@ function parseSelector(selector) {
13453
13495
  }
13454
13496
  async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
13455
13497
  const local = path28.resolve(target);
13456
- if (existsSync21(local)) {
13498
+ if (existsSync22(local)) {
13457
13499
  const recipePath = path28.basename(local) === RECIPE_FILE ? local : path28.join(local, RECIPE_FILE);
13458
13500
  return readRecipeFile(recipePath, false);
13459
13501
  }
@@ -13469,7 +13511,7 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
13469
13511
  }
13470
13512
  async function verifyFile(root, file) {
13471
13513
  const absolute = path28.resolve(root, ...file.path.split("/"));
13472
- if (!existsSync21(absolute)) {
13514
+ if (!existsSync22(absolute)) {
13473
13515
  return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
13474
13516
  }
13475
13517
  const actualSha256 = await sha256File(absolute);
@@ -13547,7 +13589,7 @@ async function installRecipe(target, options = {}) {
13547
13589
  })));
13548
13590
  if (!options.force) {
13549
13591
  for (const file of files) {
13550
- if (!existsSync21(file.path)) continue;
13592
+ if (!existsSync22(file.path)) continue;
13551
13593
  const current = await readFile23(file.path);
13552
13594
  if (!current.equals(file.data)) {
13553
13595
  throw new Error(
@@ -13572,7 +13614,7 @@ async function installRecipe(target, options = {}) {
13572
13614
 
13573
13615
  // src/pipeline/quality-regression.ts
13574
13616
  import path29 from "path";
13575
- import { existsSync as existsSync22 } from "fs";
13617
+ import { existsSync as existsSync23 } from "fs";
13576
13618
  import { readFile as readFile24 } from "fs/promises";
13577
13619
  import { z as z10 } from "zod";
13578
13620
  var SHA256_RE2 = /^[0-9a-f]{64}$/;
@@ -13815,7 +13857,7 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
13815
13857
  cases
13816
13858
  });
13817
13859
  const data = Buffer.from(JSON.stringify(baseline, null, 2) + "\n");
13818
- if (existsSync22(absolute) && !options.force) {
13860
+ if (existsSync23(absolute) && !options.force) {
13819
13861
  const current = await readFile24(absolute);
13820
13862
  if (!current.equals(data)) {
13821
13863
  throw new Error(`Quality baseline already exists with different content: ${absolute}. Pass --force to replace it.`);
@@ -14056,7 +14098,7 @@ async function provenanceFile(id, file) {
14056
14098
  return {
14057
14099
  id,
14058
14100
  path: absolute,
14059
- sha256: existsSync23(absolute) ? await sha256File(absolute) : null,
14101
+ sha256: existsSync24(absolute) ? await sha256File(absolute) : null,
14060
14102
  included: true
14061
14103
  };
14062
14104
  }
@@ -14696,7 +14738,7 @@ async function confirm(question, auto) {
14696
14738
  return answer === "y" || answer === "yes";
14697
14739
  }
14698
14740
  async function requireCompleteWorkspaceClaims(workspacePath) {
14699
- if (!existsSync23(workspacePath)) {
14741
+ if (!existsSync24(workspacePath)) {
14700
14742
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
14701
14743
  }
14702
14744
  const dir = path30.dirname(path30.resolve(workspacePath));
@@ -14955,7 +14997,7 @@ async function main() {
14955
14997
  if (args.command === "init") {
14956
14998
  if (!args.from) throw new Error("init needs --from <dir> pointing at your existing PNGs.");
14957
14999
  const root = path30.resolve(args.from);
14958
- if (!existsSync23(root)) throw new Error(`No directory at ${root}`);
15000
+ if (!existsSync24(root)) throw new Error(`No directory at ${root}`);
14959
15001
  const generator = args.generator ?? "map";
14960
15002
  if (generator !== "1dir" && generator !== "map") {
14961
15003
  throw new Error(`--generator must be "1dir" or "map", got "${args.generator}".`);
@@ -15075,7 +15117,7 @@ async function main() {
15075
15117
  log(` registered "${id}" in ${path30.relative(process.cwd(), workspacePath)}`);
15076
15118
  log(` manifest: ${project.manifest}`);
15077
15119
  log(` lock: ${project.lock}`);
15078
- if (!existsSync23(lockPath)) {
15120
+ if (!existsSync24(lockPath)) {
15079
15121
  log(
15080
15122
  ` warning: no lockfile there yet \u2014 this project contributes no claims until one is generated`
15081
15123
  );
@@ -15099,7 +15141,7 @@ async function main() {
15099
15141
  return;
15100
15142
  }
15101
15143
  if (args.subcommand === "list") {
15102
- if (!existsSync23(workspacePath)) {
15144
+ if (!existsSync24(workspacePath)) {
15103
15145
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
15104
15146
  }
15105
15147
  const ws = await loadWorkspace(workspacePath);
@@ -15119,7 +15161,7 @@ async function main() {
15119
15161
  return;
15120
15162
  }
15121
15163
  if (args.subcommand === "status") {
15122
- if (!existsSync23(workspacePath)) {
15164
+ if (!existsSync24(workspacePath)) {
15123
15165
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
15124
15166
  }
15125
15167
  const ws = await loadWorkspace(workspacePath);
@@ -15175,7 +15217,7 @@ async function main() {
15175
15217
  }
15176
15218
  if (args.command === "gallery" && args.workspace) {
15177
15219
  const workspacePath = path30.resolve(args.workspace);
15178
- if (!existsSync23(workspacePath)) {
15220
+ if (!existsSync24(workspacePath)) {
15179
15221
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
15180
15222
  }
15181
15223
  const filter = { styles: args.styles, assets: args.assets };
@@ -15210,7 +15252,7 @@ async function main() {
15210
15252
  });
15211
15253
  return;
15212
15254
  }
15213
- if (!existsSync23(path30.resolve(args.manifest))) {
15255
+ if (!existsSync24(path30.resolve(args.manifest))) {
15214
15256
  throw new Error(
15215
15257
  `No manifest at ${path30.resolve(args.manifest)}. Pass --manifest, or run \`pixelkiln init --from <dir>\`.`
15216
15258
  );
@@ -15316,13 +15358,14 @@ async function main() {
15316
15358
  }
15317
15359
  const started = await startHandEdit(loaded, lock, spec);
15318
15360
  const set = started.members.length > 1;
15319
- log(` ${started.created ? "created" : "found"} ${started.source}${set ? ` (${started.members.length} frames)` : ""}${started.declared ? " and declared it as the asset's source" : ""}`);
15361
+ const kind = spec.generator === "frames" ? "frames" : "members";
15362
+ log(` ${started.created ? "created" : "found"} ${started.source}${set ? ` (${started.members.length} ${kind})` : ""}${started.declared ? " and declared it as the asset's source" : ""}`);
15320
15363
  if (set) for (const member of started.members) log(` ${member.role ?? ""} ${path30.relative(process.cwd(), member.path)}`);
15321
15364
  if (args.noOpen) {
15322
15365
  log(` edit ${set ? "them" : "it"} with your own tool, then run pixelkiln plan; mount and pack place ${set ? "them" : "it"} in place of the generated art`);
15323
15366
  } else {
15324
15367
  const command = openInEditor(started.members[0].path);
15325
- log(` opened ${set ? "the first frame " : ""}with: ${command}${process.env.PIXELKILN_EDITOR ? "" : " (set PIXELKILN_EDITOR to choose the program)"}`);
15368
+ log(` opened ${set ? `the first ${kind === "frames" ? "frame" : "member"} ` : ""}with: ${command}${process.env.PIXELKILN_EDITOR ? "" : " (set PIXELKILN_EDITOR to choose the program)"}`);
15326
15369
  }
15327
15370
  return;
15328
15371
  }
@@ -15458,7 +15501,7 @@ async function main() {
15458
15501
  let intact = true;
15459
15502
  for (const output of entry.outputs) {
15460
15503
  const file = resolveOutputPath(output.path, item.spec.root);
15461
- if (!existsSync23(file) || await sha256File(file) !== output.sha256) {
15504
+ if (!existsSync24(file) || await sha256File(file) !== output.sha256) {
15462
15505
  intact = false;
15463
15506
  break;
15464
15507
  }
@@ -15840,7 +15883,7 @@ async function main() {
15840
15883
  const lockPaths = [
15841
15884
  .../* @__PURE__ */ new Set([
15842
15885
  ...workspaceLockPaths,
15843
- ...existsSync23(ownLock) ? [ownLock] : [],
15886
+ ...existsSync24(ownLock) ? [ownLock] : [],
15844
15887
  ...args.claims.map((c) => path30.resolve(c))
15845
15888
  ])
15846
15889
  ];