pixelkiln 0.29.0 → 0.30.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 = [];
@@ -9879,7 +9888,7 @@ function openEditorSheet(item) {
9879
9888
  const bar = el('div', 'rbar');
9880
9889
  const close = el('button', null, 'Close'); close.type = 'button';
9881
9890
  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' : ''));
9891
+ 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
9892
  const status = el('span', 'st');
9884
9893
  const acts = el('div', 'acts');
9885
9894
  const msg = el('span', 'msg');
@@ -9916,7 +9925,7 @@ async function sendOpen() {
9916
9925
  const projectUrl = sources.fromEdit && item.editMeta && item.editMeta.projectUrl && SHEET.ready.version >= 2 ? item.editMeta.projectUrl : null;
9917
9926
  const set = sources.files.length > 1;
9918
9927
  if (set && SHEET.ready.version < 3) {
9919
- SHEET.msg.textContent = 'This editor build cannot open frame sets; reinstall it with pixelkiln tools install editor.';
9928
+ SHEET.msg.textContent = 'This editor build cannot open sets; reinstall it with pixelkiln tools install editor.';
9920
9929
  sheetStatus('bad', 'unsupported');
9921
9930
  return;
9922
9931
  }
@@ -9948,7 +9957,8 @@ async function sendOpen() {
9948
9957
  const transfer = [pngs[0]];
9949
9958
  if (set) {
9950
9959
  message.frames = pngs.map((png, i) => ({ role: SHEET.roles[i], png }));
9951
- message.fps = item.fps || 12;
9960
+ // A tile set is not an animation; a slow rate keeps its timeline still-ish.
9961
+ message.fps = isFrameSet(item) ? (item.fps || 12) : 1;
9952
9962
  for (const png of pngs.slice(1)) transfer.push(png);
9953
9963
  // The first frame is both png (older editors) and frames[0]; copy it so both transfer.
9954
9964
  message.png = pngs[0].slice(0);
@@ -9981,9 +9991,9 @@ async function onEditorMessage(m) {
9981
9991
  case 'pixelkiln:opened':
9982
9992
  SHEET.opened = true;
9983
9993
  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');
9994
+ 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
9995
  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');
9996
+ 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
9997
  else sheetStatus('ok', SHEET.item.edit && SHEET.item.edit.exists ? 'editing the edit file' : 'editing a copy of the generated art');
9988
9998
  sheetButtons();
9989
9999
  break;
@@ -10010,7 +10020,7 @@ async function onEditorMessage(m) {
10010
10020
  if (saved) SHEET.item = saved;
10011
10021
  SHEET.dirty = false;
10012
10022
  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.' };
10023
+ 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
10024
  render();
10015
10025
  if (pending.close) { closeEditorSheet(true); return; }
10016
10026
  sheetStatus('ok', 'saved');
@@ -10583,16 +10593,18 @@ async function postHandEdit(item, action, extra) {
10583
10593
  if (item.project) body.project = item.project;
10584
10594
  snap = await postEdit(body);
10585
10595
  }
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');
10596
+ // A single PNG or a set of them \u2014 frames, tiles \u2014 is hand-editable, one file
10597
+ // per member; a set with a GIF in it is not.
10598
+ const editableOutputs = (item) => item.outputs.length && item.outputs.every((o) => o.exists && o.mediaType === 'image/png');
10599
+ // What the members of a set are called: frames for an animation, tiles for a tile set.
10600
+ const memberNoun = (item, n) => isFrameSet(item) ? (n === 1 ? 'frame' : 'frames')
10601
+ : item.outputs.every((o) => o.role && /^tile/i.test(o.role)) ? (n === 1 ? 'tile' : 'tiles') : (n === 1 ? 'member' : 'members');
10590
10602
  function handEditSection(item) {
10591
10603
  const canStart = EDITABLE && item.declared && item.asset && projectOf(item)?.manifestSha256 && editableOutputs(item);
10592
10604
  if (!item.edit && !canStart) return null;
10593
10605
  const set = item.edits.length > 1;
10594
10606
  const s = el('section', 'meta');
10595
- s.append(el('h3', null, set ? 'Hand edit (' + item.edits.length + ' frames)' : 'Hand edit'));
10607
+ s.append(el('h3', null, set ? 'Hand edit (' + item.edits.length + ' ' + memberNoun(item, item.edits.length) + ')' : 'Hand edit'));
10596
10608
  if (item.edit) {
10597
10609
  const pair = el('div', 'pair');
10598
10610
  const shared = displayScale(item.width, item.height, 200, 160);
@@ -10608,7 +10620,7 @@ function handEditSection(item) {
10608
10620
  };
10609
10621
  // For a set, the member the preview is on; the strip there steps through them.
10610
10622
  const at = set ? Math.min(ui.member, item.edits.length - 1) : 0;
10611
- const which = set ? ' \xB7 ' + (item.outputs[at].role || 'frame ' + (at + 1)) : '';
10623
+ const which = set ? ' \xB7 ' + (item.outputs[at].role || memberNoun(item, 1) + ' ' + (at + 1)) : '';
10612
10624
  pair.append(fig(item.outputs[at], 'generated' + which), fig(item.edits[at], 'edit' + which));
10613
10625
  s.append(pair);
10614
10626
  const dl = el('dl');
@@ -10617,9 +10629,9 @@ function handEditSection(item) {
10617
10629
  row(dl, 'status', st);
10618
10630
  row(dl, 'why', EDIT_STATUS_TEXT[item.editStatus] || '');
10619
10631
  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') });
10632
+ 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
10633
  const differing = item.editChanged.filter(Boolean).length;
10622
- row(dl, 'changed', differing + ' of ' + item.edits.length + ' frames differ from the generated art' +
10634
+ row(dl, 'changed', differing + ' of ' + item.edits.length + ' ' + memberNoun(item, item.edits.length) + ' differ from the generated art' +
10623
10635
  (differing ? ': ' + item.edits.filter((e, i) => item.editChanged[i]).map((e) => item.outputs[item.edits.indexOf(e)].role || e.path.split('/').pop()).join(', ') : ''));
10624
10636
  } else {
10625
10637
  row(dl, 'file', item.edit.path, { mono: true, copy: item.edit.absolutePath });
@@ -10633,8 +10645,8 @@ function handEditSection(item) {
10633
10645
  }
10634
10646
  s.append(dl);
10635
10647
  } 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.'
10648
+ s.append(el('div', 'state-dim', item.outputs.length > 1
10649
+ ? '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
10650
  : '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
10651
  }
10640
10652
  if (EDITABLE && item.declared && projectOf(item)?.manifestSha256) {
@@ -10665,8 +10677,9 @@ function handEditSection(item) {
10665
10677
  go('Open in desktop editor', 'start-edit', { open: true }, 'Opened ' + item.edit.path + ' in your editor. Save there, then Refresh.');
10666
10678
  go('Detach edit', 'detach-edit', {}, 'Detached. The file' + (set ? 's are' : ' is') + ' still at ' + item.source + '; the generated art is placed again.');
10667
10679
  } 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.');
10680
+ const many = item.outputs.length > 1;
10681
+ 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.');
10682
+ 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
10683
  }
10671
10684
  acts.append(msg);
10672
10685
  s.append(acts);
@@ -11969,7 +11982,7 @@ import path24 from "path";
11969
11982
 
11970
11983
  // src/editor/install.ts
11971
11984
  import { createHash as createHash4 } from "crypto";
11972
- import { existsSync as existsSync18 } from "fs";
11985
+ import { existsSync as existsSync19 } from "fs";
11973
11986
  import { mkdir as mkdir7, readFile as readFile18, rename as rename8, rm as rm9, stat as stat4, writeFile as writeFile10 } from "fs/promises";
11974
11987
  import os2 from "os";
11975
11988
  import path23 from "path";
@@ -12068,7 +12081,7 @@ async function editorStatus(opts = {}) {
12068
12081
  for (const [name, expected] of Object.entries(pin.files)) {
12069
12082
  totalBytes += expected.bytes;
12070
12083
  const file = path23.join(dir, name);
12071
- const info = existsSync18(file) ? await stat4(file).catch(() => null) : null;
12084
+ const info = existsSync19(file) ? await stat4(file).catch(() => null) : null;
12072
12085
  if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
12073
12086
  installedBytes += expected.bytes;
12074
12087
  } else {
@@ -12263,7 +12276,7 @@ function createGalleryEditorHandlers(opts = {}) {
12263
12276
 
12264
12277
  // src/pipeline/salvage.ts
12265
12278
  import { readFile as readFile20 } from "fs/promises";
12266
- import { existsSync as existsSync19 } from "fs";
12279
+ import { existsSync as existsSync20 } from "fs";
12267
12280
  import path25 from "path";
12268
12281
  function providerClaimId(provider, objectId) {
12269
12282
  return `${encodeURIComponent(provider)}:${encodeURIComponent(objectId)}`;
@@ -12271,7 +12284,7 @@ function providerClaimId(provider, objectId) {
12271
12284
  async function loadClaims(lockPaths, opts = {}) {
12272
12285
  const claimed = /* @__PURE__ */ new Set();
12273
12286
  for (const p of lockPaths) {
12274
- if (!existsSync19(p)) throw new Error(`Claim lockfile not found: ${p}`);
12287
+ if (!existsSync20(p)) throw new Error(`Claim lockfile not found: ${p}`);
12275
12288
  let parsed;
12276
12289
  try {
12277
12290
  parsed = parseLock(JSON.parse(await readFile20(p, "utf8")));
@@ -12332,7 +12345,7 @@ async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, cla
12332
12345
  const siblings = [];
12333
12346
  for (const siblingManifestPath of siblingManifestPaths) {
12334
12347
  if (path25.resolve(siblingManifestPath) === own) continue;
12335
- if (!existsSync19(siblingManifestPath)) continue;
12348
+ if (!existsSync20(siblingManifestPath)) continue;
12336
12349
  try {
12337
12350
  const { manifest } = await loadManifest(siblingManifestPath);
12338
12351
  siblings.push({ label: path25.basename(path25.dirname(siblingManifestPath)), manifest });
@@ -12554,11 +12567,11 @@ async function workspaceStatus(ws, dir) {
12554
12567
  }
12555
12568
 
12556
12569
  // src/pipeline/cache-health.ts
12557
- import { existsSync as existsSync20 } from "fs";
12570
+ import { existsSync as existsSync21 } from "fs";
12558
12571
  import { readFile as readFile21, readdir as readdir2, rm as rm10 } from "fs/promises";
12559
12572
  import path26 from "path";
12560
12573
  async function inspectCaches(lock, lockPath, options = {}) {
12561
- if (options.prune && !existsSync20(lockPath)) {
12574
+ if (options.prune && !existsSync21(lockPath)) {
12562
12575
  throw new Error(`Refusing to prune without an existing lockfile at ${path26.resolve(lockPath)}`);
12563
12576
  }
12564
12577
  const contentDir = path26.resolve(path26.dirname(lockPath), ".pixelkiln", "cache");
@@ -12603,7 +12616,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
12603
12616
  async function inspectContentCache(contentDir, referenced) {
12604
12617
  const report = {
12605
12618
  path: contentDir,
12606
- exists: existsSync20(contentDir),
12619
+ exists: existsSync21(contentDir),
12607
12620
  files: 0,
12608
12621
  bytes: 0,
12609
12622
  valid: 0,
@@ -12667,7 +12680,7 @@ async function inspectContentCache(contentDir, referenced) {
12667
12680
  async function inspectRemoteHashCache(remotePath) {
12668
12681
  const report = {
12669
12682
  path: remotePath,
12670
- exists: existsSync20(remotePath),
12683
+ exists: existsSync21(remotePath),
12671
12684
  entries: 0,
12672
12685
  valid: 0,
12673
12686
  invalidIds: [],
@@ -12717,8 +12730,11 @@ function normalizeTileRules(raw) {
12717
12730
  };
12718
12731
  }
12719
12732
  function exportTileset(entry, spec, options) {
12720
- const outputs = resolveSpecEntryOutputs(entry, spec);
12733
+ let outputs = resolveSpecEntryOutputs(entry, spec);
12721
12734
  if (!outputs.length) throw new Error(`${spec.styleId}/${spec.assetId} has no downloaded outputs`);
12735
+ if (spec.source && sourceIsStem(spec.source, entry, options.manifestDir)) {
12736
+ outputs = outputs.map((output) => ({ ...output, absolutePath: sourceOutputPath(spec.source, entry, output.index, options.manifestDir) }));
12737
+ }
12722
12738
  const packed = packSprites(
12723
12739
  outputs.map((output) => ({
12724
12740
  id: output.role ?? fallbackOutputRole(output.index),
@@ -13212,7 +13228,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
13212
13228
 
13213
13229
  // src/recipes.ts
13214
13230
  import path28 from "path";
13215
- import { existsSync as existsSync21 } from "fs";
13231
+ import { existsSync as existsSync22 } from "fs";
13216
13232
  import { readdir as readdir3, readFile as readFile23 } from "fs/promises";
13217
13233
  import { fileURLToPath } from "url";
13218
13234
  import { z as z9 } from "zod";
@@ -13415,7 +13431,7 @@ async function readRecipeFile(recipePath, bundled) {
13415
13431
  return { recipe: parsed.data, path: absolute, dir: path28.dirname(absolute), bundled };
13416
13432
  }
13417
13433
  async function walkRecipeFiles(root) {
13418
- if (!existsSync21(root)) return [];
13434
+ if (!existsSync22(root)) return [];
13419
13435
  const out = [];
13420
13436
  for (const entry of await readdir3(root, { withFileTypes: true })) {
13421
13437
  const resolved = path28.join(root, entry.name);
@@ -13453,7 +13469,7 @@ function parseSelector(selector) {
13453
13469
  }
13454
13470
  async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
13455
13471
  const local = path28.resolve(target);
13456
- if (existsSync21(local)) {
13472
+ if (existsSync22(local)) {
13457
13473
  const recipePath = path28.basename(local) === RECIPE_FILE ? local : path28.join(local, RECIPE_FILE);
13458
13474
  return readRecipeFile(recipePath, false);
13459
13475
  }
@@ -13469,7 +13485,7 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
13469
13485
  }
13470
13486
  async function verifyFile(root, file) {
13471
13487
  const absolute = path28.resolve(root, ...file.path.split("/"));
13472
- if (!existsSync21(absolute)) {
13488
+ if (!existsSync22(absolute)) {
13473
13489
  return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
13474
13490
  }
13475
13491
  const actualSha256 = await sha256File(absolute);
@@ -13547,7 +13563,7 @@ async function installRecipe(target, options = {}) {
13547
13563
  })));
13548
13564
  if (!options.force) {
13549
13565
  for (const file of files) {
13550
- if (!existsSync21(file.path)) continue;
13566
+ if (!existsSync22(file.path)) continue;
13551
13567
  const current = await readFile23(file.path);
13552
13568
  if (!current.equals(file.data)) {
13553
13569
  throw new Error(
@@ -13572,7 +13588,7 @@ async function installRecipe(target, options = {}) {
13572
13588
 
13573
13589
  // src/pipeline/quality-regression.ts
13574
13590
  import path29 from "path";
13575
- import { existsSync as existsSync22 } from "fs";
13591
+ import { existsSync as existsSync23 } from "fs";
13576
13592
  import { readFile as readFile24 } from "fs/promises";
13577
13593
  import { z as z10 } from "zod";
13578
13594
  var SHA256_RE2 = /^[0-9a-f]{64}$/;
@@ -13815,7 +13831,7 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
13815
13831
  cases
13816
13832
  });
13817
13833
  const data = Buffer.from(JSON.stringify(baseline, null, 2) + "\n");
13818
- if (existsSync22(absolute) && !options.force) {
13834
+ if (existsSync23(absolute) && !options.force) {
13819
13835
  const current = await readFile24(absolute);
13820
13836
  if (!current.equals(data)) {
13821
13837
  throw new Error(`Quality baseline already exists with different content: ${absolute}. Pass --force to replace it.`);
@@ -14056,7 +14072,7 @@ async function provenanceFile(id, file) {
14056
14072
  return {
14057
14073
  id,
14058
14074
  path: absolute,
14059
- sha256: existsSync23(absolute) ? await sha256File(absolute) : null,
14075
+ sha256: existsSync24(absolute) ? await sha256File(absolute) : null,
14060
14076
  included: true
14061
14077
  };
14062
14078
  }
@@ -14696,7 +14712,7 @@ async function confirm(question, auto) {
14696
14712
  return answer === "y" || answer === "yes";
14697
14713
  }
14698
14714
  async function requireCompleteWorkspaceClaims(workspacePath) {
14699
- if (!existsSync23(workspacePath)) {
14715
+ if (!existsSync24(workspacePath)) {
14700
14716
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
14701
14717
  }
14702
14718
  const dir = path30.dirname(path30.resolve(workspacePath));
@@ -14955,7 +14971,7 @@ async function main() {
14955
14971
  if (args.command === "init") {
14956
14972
  if (!args.from) throw new Error("init needs --from <dir> pointing at your existing PNGs.");
14957
14973
  const root = path30.resolve(args.from);
14958
- if (!existsSync23(root)) throw new Error(`No directory at ${root}`);
14974
+ if (!existsSync24(root)) throw new Error(`No directory at ${root}`);
14959
14975
  const generator = args.generator ?? "map";
14960
14976
  if (generator !== "1dir" && generator !== "map") {
14961
14977
  throw new Error(`--generator must be "1dir" or "map", got "${args.generator}".`);
@@ -15075,7 +15091,7 @@ async function main() {
15075
15091
  log(` registered "${id}" in ${path30.relative(process.cwd(), workspacePath)}`);
15076
15092
  log(` manifest: ${project.manifest}`);
15077
15093
  log(` lock: ${project.lock}`);
15078
- if (!existsSync23(lockPath)) {
15094
+ if (!existsSync24(lockPath)) {
15079
15095
  log(
15080
15096
  ` warning: no lockfile there yet \u2014 this project contributes no claims until one is generated`
15081
15097
  );
@@ -15099,7 +15115,7 @@ async function main() {
15099
15115
  return;
15100
15116
  }
15101
15117
  if (args.subcommand === "list") {
15102
- if (!existsSync23(workspacePath)) {
15118
+ if (!existsSync24(workspacePath)) {
15103
15119
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
15104
15120
  }
15105
15121
  const ws = await loadWorkspace(workspacePath);
@@ -15119,7 +15135,7 @@ async function main() {
15119
15135
  return;
15120
15136
  }
15121
15137
  if (args.subcommand === "status") {
15122
- if (!existsSync23(workspacePath)) {
15138
+ if (!existsSync24(workspacePath)) {
15123
15139
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
15124
15140
  }
15125
15141
  const ws = await loadWorkspace(workspacePath);
@@ -15175,7 +15191,7 @@ async function main() {
15175
15191
  }
15176
15192
  if (args.command === "gallery" && args.workspace) {
15177
15193
  const workspacePath = path30.resolve(args.workspace);
15178
- if (!existsSync23(workspacePath)) {
15194
+ if (!existsSync24(workspacePath)) {
15179
15195
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
15180
15196
  }
15181
15197
  const filter = { styles: args.styles, assets: args.assets };
@@ -15210,7 +15226,7 @@ async function main() {
15210
15226
  });
15211
15227
  return;
15212
15228
  }
15213
- if (!existsSync23(path30.resolve(args.manifest))) {
15229
+ if (!existsSync24(path30.resolve(args.manifest))) {
15214
15230
  throw new Error(
15215
15231
  `No manifest at ${path30.resolve(args.manifest)}. Pass --manifest, or run \`pixelkiln init --from <dir>\`.`
15216
15232
  );
@@ -15316,13 +15332,14 @@ async function main() {
15316
15332
  }
15317
15333
  const started = await startHandEdit(loaded, lock, spec);
15318
15334
  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" : ""}`);
15335
+ const kind = spec.generator === "frames" ? "frames" : "members";
15336
+ log(` ${started.created ? "created" : "found"} ${started.source}${set ? ` (${started.members.length} ${kind})` : ""}${started.declared ? " and declared it as the asset's source" : ""}`);
15320
15337
  if (set) for (const member of started.members) log(` ${member.role ?? ""} ${path30.relative(process.cwd(), member.path)}`);
15321
15338
  if (args.noOpen) {
15322
15339
  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
15340
  } else {
15324
15341
  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)"}`);
15342
+ log(` opened ${set ? `the first ${kind === "frames" ? "frame" : "member"} ` : ""}with: ${command}${process.env.PIXELKILN_EDITOR ? "" : " (set PIXELKILN_EDITOR to choose the program)"}`);
15326
15343
  }
15327
15344
  return;
15328
15345
  }
@@ -15458,7 +15475,7 @@ async function main() {
15458
15475
  let intact = true;
15459
15476
  for (const output of entry.outputs) {
15460
15477
  const file = resolveOutputPath(output.path, item.spec.root);
15461
- if (!existsSync23(file) || await sha256File(file) !== output.sha256) {
15478
+ if (!existsSync24(file) || await sha256File(file) !== output.sha256) {
15462
15479
  intact = false;
15463
15480
  break;
15464
15481
  }
@@ -15840,7 +15857,7 @@ async function main() {
15840
15857
  const lockPaths = [
15841
15858
  .../* @__PURE__ */ new Set([
15842
15859
  ...workspaceLockPaths,
15843
- ...existsSync23(ownLock) ? [ownLock] : [],
15860
+ ...existsSync24(ownLock) ? [ownLock] : [],
15844
15861
  ...args.claims.map((c) => path30.resolve(c))
15845
15862
  ])
15846
15863
  ];