pixelkiln 0.28.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/index.cjs CHANGED
@@ -34,17 +34,23 @@ __export(index_exports, {
34
34
  ComfyUIProvider: () => ComfyUIProvider,
35
35
  DEFAULT_QUALITY_TOLERANCES: () => DEFAULT_QUALITY_TOLERANCES,
36
36
  DEFAULT_RATE_LIMIT: () => DEFAULT_RATE_LIMIT,
37
+ EDITOR_PIN: () => EDITOR_PIN,
38
+ EDITOR_RELEASE_BASE: () => EDITOR_RELEASE_BASE,
39
+ EditorInstallError: () => EditorInstallError,
37
40
  FAKE_PNG: () => FAKE_PNG,
38
41
  FakeProvider: () => FakeProvider,
39
42
  GenerateRequestError: () => GenerateRequestError,
40
43
  GenerateRequestSchema: () => GenerateRequestSchema,
41
44
  GeneratorSchema: () => GeneratorSchema,
42
45
  GridConfidenceSchema: () => GridConfidenceSchema,
46
+ HAND_EDIT_DIR: () => HAND_EDIT_DIR,
47
+ HandEditCompanionSchema: () => HandEditCompanionSchema,
43
48
  ImageQualityMetricsSchema: () => ImageQualityMetricsSchema,
44
49
  InheritedStyleSchema: () => InheritedStyleSchema,
45
50
  LockEntrySchema: () => LockEntrySchema,
46
51
  LockSchema: () => LockSchema,
47
52
  MAX_DOWNLOAD_BYTES: () => MAX_DOWNLOAD_BYTES,
53
+ MAX_HAND_EDIT_BYTES: () => MAX_HAND_EDIT_BYTES,
48
54
  MAX_RETRIES: () => MAX_RETRIES,
49
55
  ManifestDriftError: () => ManifestDriftError,
50
56
  ManifestEditError: () => ManifestEditError,
@@ -91,12 +97,19 @@ __export(index_exports, {
91
97
  countNumberedDescriptions: () => countNumberedDescriptions,
92
98
  createArtifactBundleManifest: () => createArtifactBundleManifest,
93
99
  createGalleryEditHandler: () => createGalleryEditHandler,
100
+ createGalleryEditorHandlers: () => createGalleryEditorHandlers,
94
101
  createGenerateHandlers: () => createGenerateHandlers,
95
102
  createProvider: () => createProvider,
96
103
  currentEntryOutputPath: () => currentEntryOutputPath,
97
104
  currentOutputPath: () => currentOutputPath,
105
+ detachHandEdit: () => detachHandEdit,
98
106
  detectMediaType: () => detectMediaType,
99
107
  doctor: () => doctor,
108
+ editorBaseUrl: () => editorBaseUrl,
109
+ editorDir: () => editorDir,
110
+ editorRoute: () => editorRoute,
111
+ editorStatus: () => editorStatus,
112
+ editorToolsRoot: () => editorToolsRoot,
100
113
  evaluateAudit: () => evaluateAudit,
101
114
  expectedOutputPath: () => expectedOutputPath,
102
115
  exportTileset: () => exportTileset,
@@ -108,13 +121,22 @@ __export(index_exports, {
108
121
  galleryMediaRoute: () => galleryMediaRoute,
109
122
  generationCost: () => generationCost,
110
123
  groupOrphansByStyle: () => groupOrphansByStyle,
124
+ handEditBase: () => handEditBase,
125
+ handEditBases: () => handEditBases,
126
+ handEditCompanionPath: () => handEditCompanionPath,
127
+ handEditMembers: () => handEditMembers,
128
+ handEditPath: () => handEditPath,
129
+ handEditProjectPath: () => handEditProjectPath,
111
130
  hex: () => hex,
112
131
  idFromPrompt: () => idFromPrompt,
113
132
  imageMetadata: () => imageMetadata,
114
133
  inspectCaches: () => inspectCaches,
115
134
  inspectQualityProfile: () => inspectQualityProfile,
116
135
  inspectRevisionReadiness: () => inspectRevisionReadiness,
136
+ installEditor: () => installEditor,
117
137
  installRecipe: () => installRecipe,
138
+ isFrameSetEntry: () => isFrameSetEntry,
139
+ isMemberSetEntry: () => isMemberSetEntry,
118
140
  isSensitiveSourceUrl: () => isSensitiveSourceUrl,
119
141
  listBundledRecipes: () => listBundledRecipes,
120
142
  loadClaims: () => loadClaims,
@@ -129,12 +151,14 @@ __export(index_exports, {
129
151
  measureImageQuality: () => measureImageQuality,
130
152
  mediaExtension: () => mediaExtension,
131
153
  mediaTypeFromExtension: () => mediaTypeFromExtension,
154
+ memberPath: () => memberPath,
132
155
  mergePalettes: () => mergePalettes,
133
156
  mountSprites: () => mountSprites,
134
157
  mountStyle: () => mountStyle,
135
158
  normalizeLockOutputPaths: () => normalizeLockOutputPaths,
136
159
  normalizeRefinementPalette: () => normalizeRefinementPalette,
137
160
  normalizeTileRules: () => normalizeTileRules,
161
+ openInEditor: () => openInEditor,
138
162
  outliers: () => outliers,
139
163
  outputId: () => outputId,
140
164
  packSprites: () => packSprites,
@@ -154,6 +178,7 @@ __export(index_exports, {
154
178
  qualityRecordPath: () => qualityRecordPath,
155
179
  quantizeToPalette: () => quantizeToPalette,
156
180
  readArtifactBundleManifest: () => readArtifactBundleManifest,
181
+ readHandEditCompanion: () => readHandEditCompanion,
157
182
  readQualityBaseline: () => readQualityBaseline,
158
183
  recipeDigest: () => recipeDigest,
159
184
  refineAsset: () => refineAsset,
@@ -186,6 +211,7 @@ __export(index_exports, {
186
211
  retryAfterMs: () => retryAfterMs,
187
212
  runPicker: () => runPicker,
188
213
  runSalvage: () => runSalvage,
214
+ saveHandEdit: () => saveHandEdit,
189
215
  saveLock: () => saveLock,
190
216
  saveWorkspace: () => saveWorkspace,
191
217
  scanAssets: () => scanAssets,
@@ -197,8 +223,11 @@ __export(index_exports, {
197
223
  shouldRetry: () => shouldRetry,
198
224
  slugify: () => slugify,
199
225
  snapshotQualityBaseline: () => snapshotQualityBaseline,
226
+ sourceIsStem: () => sourceIsStem,
227
+ sourceOutputPath: () => sourceOutputPath,
200
228
  specHash: () => specHash,
201
229
  spendByUnit: () => spendByUnit,
230
+ startHandEdit: () => startHandEdit,
202
231
  styleImagesBase64: () => styleImagesBase64,
203
232
  submit: () => submit,
204
233
  summarize: () => summarize,
@@ -768,11 +797,11 @@ var PixelLabClient = class {
768
797
  * common than the failure mode of retrying (a duplicate object), and a
769
798
  * duplicate is visible and free to delete whereas a silent gap is neither.
770
799
  */
771
- async request(path27, init, attempt = 0) {
800
+ async request(path29, init, attempt = 0) {
772
801
  const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
773
802
  let res;
774
803
  try {
775
- res = await fetch(`${BASE}${path27}`, {
804
+ res = await fetch(`${BASE}${path29}`, {
776
805
  ...init,
777
806
  signal: init?.signal ?? AbortSignal.timeout(this.timeoutMs),
778
807
  headers: {
@@ -784,24 +813,24 @@ var PixelLabClient = class {
784
813
  } catch (err) {
785
814
  if (attempt < MAX_RETRIES) {
786
815
  await sleep(backoffMs(attempt));
787
- return this.request(path27, init, attempt + 1);
816
+ return this.request(path29, init, attempt + 1);
788
817
  }
789
818
  throw err;
790
819
  }
791
820
  if (!res.ok && shouldRetry(res.status) && attempt < MAX_RETRIES) {
792
821
  const waitMs = retryAfterMs(res.headers.get("retry-after")) ?? backoffMs(attempt);
793
822
  await sleep(waitMs);
794
- return this.request(path27, init, attempt + 1);
823
+ return this.request(path29, init, attempt + 1);
795
824
  }
796
825
  const text = await res.text();
797
826
  if (!res.ok) {
798
- throw new PixelLabError(`${init?.method ?? "GET"} ${path27} \u2192 ${res.status}`, res.status, text);
827
+ throw new PixelLabError(`${init?.method ?? "GET"} ${path29} \u2192 ${res.status}`, res.status, text);
799
828
  }
800
829
  if (!text) return {};
801
830
  try {
802
831
  return JSON.parse(text);
803
832
  } catch {
804
- throw new Error(`${init?.method ?? "GET"} ${path27} returned invalid JSON`);
833
+ throw new Error(`${init?.method ?? "GET"} ${path29} returned invalid JSON`);
805
834
  }
806
835
  }
807
836
  async balance() {
@@ -1852,9 +1881,9 @@ var RetroDiffusionClient = class {
1852
1881
  }
1853
1882
  return balance;
1854
1883
  }
1855
- async call(path27, init = {}) {
1884
+ async call(path29, init = {}) {
1856
1885
  if (!this.token) throw new Error("RD_API_KEY is not set");
1857
- const response = await this.request(`${this.baseUrl}${path27}`, {
1886
+ const response = await this.request(`${this.baseUrl}${path29}`, {
1858
1887
  ...init,
1859
1888
  headers: {
1860
1889
  "Content-Type": "application/json",
@@ -2657,10 +2686,10 @@ var import_node_fs2 = require("fs");
2657
2686
  function sha256(data) {
2658
2687
  return (0, import_node_crypto2.createHash)("sha256").update(data).digest("hex");
2659
2688
  }
2660
- async function sha256File(path27) {
2689
+ async function sha256File(path29) {
2661
2690
  return new Promise((resolve, reject) => {
2662
2691
  const hash = (0, import_node_crypto2.createHash)("sha256");
2663
- const stream = (0, import_node_fs2.createReadStream)(path27);
2692
+ const stream = (0, import_node_fs2.createReadStream)(path29);
2664
2693
  stream.on("data", (chunk2) => hash.update(chunk2));
2665
2694
  stream.on("error", reject);
2666
2695
  stream.on("end", () => resolve(hash.digest("hex")));
@@ -2953,12 +2982,7 @@ var ComfyUIProvider = class _ComfyUIProvider {
2953
2982
  }
2954
2983
  validate(spec, styleImages) {
2955
2984
  const options = resolvedOptions(spec);
2956
- if (spec.source && !spec.revision) {
2957
- if (spec.generator === "frames") {
2958
- throw new Error("ComfyUI frame sets require generated provider outputs, not asset.source");
2959
- }
2960
- return;
2961
- }
2985
+ if (spec.source && !spec.revision && spec.generator !== "frames") return;
2962
2986
  if (spec.width < 16 || spec.height < 16 || spec.width > 4096 || spec.height > 4096) {
2963
2987
  throw new Error("ComfyUI output dimensions must be between 16 and 4096 pixels");
2964
2988
  }
@@ -3792,10 +3816,11 @@ registerProvider({
3792
3816
 
3793
3817
  // src/manifest.ts
3794
3818
  var import_promises3 = require("fs/promises");
3795
- var import_node_fs4 = require("fs");
3819
+ var import_node_fs5 = require("fs");
3796
3820
  var import_node_path5 = __toESM(require("path"), 1);
3797
3821
 
3798
3822
  // src/outputs.ts
3823
+ var import_node_fs4 = require("fs");
3799
3824
  var import_node_path4 = __toESM(require("path"), 1);
3800
3825
 
3801
3826
  // src/lock.ts
@@ -3985,13 +4010,31 @@ function resolveOutputPath(recordedPath, manifestDir) {
3985
4010
  return import_node_path4.default.resolve(manifestDir, recordedPath.split(/[\\/]/).join(import_node_path4.default.sep));
3986
4011
  }
3987
4012
  function expectedOutputPath(spec, role, index, total, mediaType) {
3988
- const originalExt = import_node_path4.default.extname(spec.outFile);
4013
+ return memberPath(spec.outFile, role, index, total, mediaType);
4014
+ }
4015
+ function memberPath(file, role, index, total, mediaType) {
4016
+ const originalExt = import_node_path4.default.extname(file);
3989
4017
  const ext = mediaType ? mediaExtension(mediaType) : originalExt || ".png";
3990
- const stem = originalExt ? spec.outFile.slice(0, -originalExt.length) : spec.outFile;
4018
+ const stem = originalExt ? file.slice(0, -originalExt.length) : file;
3991
4019
  if (total === 1) return `${stem}${ext}`;
3992
4020
  const safeRole = (role ?? fallbackOutputRole(index)).replace(/[^a-zA-Z0-9_-]+/g, "-");
3993
4021
  return `${stem}-${safeRole}${ext}`;
3994
4022
  }
4023
+ function isMemberSetEntry(entry) {
4024
+ return entry.outputs.length > 1 && entry.outputs.every((output) => !output.mediaType || output.mediaType === "image/png");
4025
+ }
4026
+ function isFrameSetEntry(entry) {
4027
+ return entry.generator === "frames" && isMemberSetEntry(entry);
4028
+ }
4029
+ function sourceIsStem(source, entry, manifestDir) {
4030
+ return isMemberSetEntry(entry) && !(0, import_node_fs4.existsSync)(import_node_path4.default.resolve(manifestDir, source));
4031
+ }
4032
+ function sourceOutputPath(source, entry, index, manifestDir) {
4033
+ const absolute = import_node_path4.default.resolve(manifestDir, source);
4034
+ if (!sourceIsStem(source, entry, manifestDir)) return absolute;
4035
+ const output = entry.outputs[index];
4036
+ return memberPath(absolute, output.role, index, entry.outputs.length, output.mediaType);
4037
+ }
3995
4038
  function currentOutputPath(output, spec, index, total) {
3996
4039
  return expectedOutputPath(spec, output.role, index, total, output.mediaType);
3997
4040
  }
@@ -4082,7 +4125,7 @@ function resolveSpecOutputs(spec, lock, manifestDir) {
4082
4125
  // src/manifest.ts
4083
4126
  async function loadManifest(manifestPath) {
4084
4127
  const abs = import_node_path5.default.resolve(manifestPath);
4085
- if (!(0, import_node_fs4.existsSync)(abs)) throw new Error(`No manifest at ${abs}`);
4128
+ if (!(0, import_node_fs5.existsSync)(abs)) throw new Error(`No manifest at ${abs}`);
4086
4129
  const input = ManifestInputSchema.safeParse(JSON.parse(await (0, import_promises3.readFile)(abs, "utf8")));
4087
4130
  if (!input.success) {
4088
4131
  const issues = formatManifestIssues(input.error.issues);
@@ -4254,7 +4297,7 @@ async function resolveSpecs(loaded, filter) {
4254
4297
  const abs = import_node_path5.default.resolve(root, rel);
4255
4298
  let hit = styleImageCache.get(abs);
4256
4299
  if (!hit) {
4257
- if (!(0, import_node_fs4.existsSync)(abs)) throw new Error(`Style image not found: ${abs}`);
4300
+ if (!(0, import_node_fs5.existsSync)(abs)) throw new Error(`Style image not found: ${abs}`);
4258
4301
  const buf = await (0, import_promises3.readFile)(abs);
4259
4302
  const metadata = imageMetadata(buf);
4260
4303
  if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${abs}`);
@@ -4495,7 +4538,7 @@ function pngPath(file) {
4495
4538
  return extension ? `${file.slice(0, -extension.length)}.png` : `${file}.png`;
4496
4539
  }
4497
4540
  async function optionalRevisionImage(file, label, requiredFormat) {
4498
- if (!(0, import_node_fs4.existsSync)(file)) return null;
4541
+ if (!(0, import_node_fs5.existsSync)(file)) return null;
4499
4542
  const bytes = await (0, import_promises3.readFile)(file);
4500
4543
  const metadata = imageMetadata(bytes);
4501
4544
  if (!metadata) throw new Error(`${label} is not a readable PNG or JPEG: ${file}`);
@@ -5047,7 +5090,7 @@ async function writeArtifactBundle(files, options = {}) {
5047
5090
 
5048
5091
  // src/recipes.ts
5049
5092
  var import_node_path7 = __toESM(require("path"), 1);
5050
- var import_node_fs5 = require("fs");
5093
+ var import_node_fs6 = require("fs");
5051
5094
  var import_promises5 = require("fs/promises");
5052
5095
  var import_node_url = require("url");
5053
5096
  var import_zod3 = require("zod");
@@ -5251,7 +5294,7 @@ async function readRecipeFile(recipePath, bundled) {
5251
5294
  return { recipe: parsed.data, path: absolute, dir: import_node_path7.default.dirname(absolute), bundled };
5252
5295
  }
5253
5296
  async function walkRecipeFiles(root) {
5254
- if (!(0, import_node_fs5.existsSync)(root)) return [];
5297
+ if (!(0, import_node_fs6.existsSync)(root)) return [];
5255
5298
  const out = [];
5256
5299
  for (const entry of await (0, import_promises5.readdir)(root, { withFileTypes: true })) {
5257
5300
  const resolved = import_node_path7.default.join(root, entry.name);
@@ -5289,7 +5332,7 @@ function parseSelector(selector) {
5289
5332
  }
5290
5333
  async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
5291
5334
  const local = import_node_path7.default.resolve(target);
5292
- if ((0, import_node_fs5.existsSync)(local)) {
5335
+ if ((0, import_node_fs6.existsSync)(local)) {
5293
5336
  const recipePath = import_node_path7.default.basename(local) === RECIPE_FILE ? local : import_node_path7.default.join(local, RECIPE_FILE);
5294
5337
  return readRecipeFile(recipePath, false);
5295
5338
  }
@@ -5305,7 +5348,7 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
5305
5348
  }
5306
5349
  async function verifyFile(root, file) {
5307
5350
  const absolute = import_node_path7.default.resolve(root, ...file.path.split("/"));
5308
- if (!(0, import_node_fs5.existsSync)(absolute)) {
5351
+ if (!(0, import_node_fs6.existsSync)(absolute)) {
5309
5352
  return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
5310
5353
  }
5311
5354
  const actualSha256 = await sha256File(absolute);
@@ -5383,7 +5426,7 @@ async function installRecipe(target, options = {}) {
5383
5426
  })));
5384
5427
  if (!options.force) {
5385
5428
  for (const file of files) {
5386
- if (!(0, import_node_fs5.existsSync)(file.path)) continue;
5429
+ if (!(0, import_node_fs6.existsSync)(file.path)) continue;
5387
5430
  const current = await (0, import_promises5.readFile)(file.path);
5388
5431
  if (!current.equals(file.data)) {
5389
5432
  throw new Error(
@@ -5407,11 +5450,11 @@ async function installRecipe(target, options = {}) {
5407
5450
  }
5408
5451
 
5409
5452
  // src/pipeline/plan.ts
5410
- var import_node_fs9 = require("fs");
5453
+ var import_node_fs10 = require("fs");
5411
5454
  var import_node_path12 = __toESM(require("path"), 1);
5412
5455
 
5413
5456
  // src/pipeline/quality-profile.ts
5414
- var import_node_fs7 = require("fs");
5457
+ var import_node_fs8 = require("fs");
5415
5458
  var import_node_path10 = __toESM(require("path"), 1);
5416
5459
 
5417
5460
  // src/pipeline/refine.ts
@@ -5423,7 +5466,7 @@ var import_node_util = require("util");
5423
5466
 
5424
5467
  // src/pipeline/audit.ts
5425
5468
  var import_promises6 = require("fs/promises");
5426
- var import_node_fs6 = require("fs");
5469
+ var import_node_fs7 = require("fs");
5427
5470
  var import_node_path8 = __toESM(require("path"), 1);
5428
5471
  function colorDistance(a, b) {
5429
5472
  const rmean = (a.r + b.r) / 2;
@@ -5469,7 +5512,7 @@ async function auditStyle(loaded, specs, styleId, lock) {
5469
5512
  const unreadable = [];
5470
5513
  for (const spec of mine) {
5471
5514
  for (const output of resolveSpecOutputs(spec, lock, loaded.root)) {
5472
- if (!(0, import_node_fs6.existsSync)(output.absolutePath)) {
5515
+ if (!(0, import_node_fs7.existsSync)(output.absolutePath)) {
5473
5516
  missing.push(output.id);
5474
5517
  continue;
5475
5518
  }
@@ -5499,7 +5542,7 @@ async function auditStyle(loaded, specs, styleId, lock) {
5499
5542
  const refPalettes = [];
5500
5543
  for (const img of style.styleImages) {
5501
5544
  const abs = import_node_path8.default.resolve(loaded.root, img.path);
5502
- if (!(0, import_node_fs6.existsSync)(abs)) continue;
5545
+ if (!(0, import_node_fs7.existsSync)(abs)) continue;
5503
5546
  try {
5504
5547
  refPalettes.push(extractPalette(decodePng(await (0, import_promises6.readFile)(abs)), 12));
5505
5548
  } catch {
@@ -6128,7 +6171,7 @@ async function qualitySource(spec, lock) {
6128
6171
  return { items: null, reason: "frame-set quality requires ordered downloaded provider outputs" };
6129
6172
  }
6130
6173
  const source = import_node_path10.default.resolve(spec.root, spec.source);
6131
- return (0, import_node_fs7.existsSync)(source) ? { items: [{ role: "source", path: source }] } : { items: null, reason: `declared source is missing: ${spec.source}` };
6174
+ return (0, import_node_fs8.existsSync)(source) ? { items: [{ role: "source", path: source }] } : { items: null, reason: `declared source is missing: ${spec.source}` };
6132
6175
  }
6133
6176
  const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
6134
6177
  if (!entry) return { items: null, reason: "raw provider output is not in the lockfile" };
@@ -6157,7 +6200,7 @@ async function qualitySource(spec, lock) {
6157
6200
  }
6158
6201
  if (role) roles.add(role);
6159
6202
  const source = currentEntryOutputPath(entry, spec, index);
6160
- if (!(0, import_node_fs7.existsSync)(source)) return { items: null, reason: `raw provider output is missing: ${source}` };
6203
+ if (!(0, import_node_fs8.existsSync)(source)) return { items: null, reason: `raw provider output is missing: ${source}` };
6161
6204
  if (await sha256File(source) !== output.sha256) {
6162
6205
  return { items: null, reason: "raw provider output was modified after download" };
6163
6206
  }
@@ -6176,7 +6219,7 @@ async function inspectQualityProfile(spec, lock) {
6176
6219
  }
6177
6220
  const sourcePaths = source.items.map((item) => item.path);
6178
6221
  const expectedOutputs = spec.generator === "frames" ? source.items.map((item, index) => qualityFrameOutputPath(output, item.role, index)) : [output];
6179
- if (!(0, import_node_fs7.existsSync)(record)) {
6222
+ if (!(0, import_node_fs8.existsSync)(record)) {
6180
6223
  return {
6181
6224
  key,
6182
6225
  state: "needs-refinement",
@@ -6357,7 +6400,7 @@ async function requireApprovedQualitySources(specs, lock, assetIds, outputRoles)
6357
6400
  }
6358
6401
 
6359
6402
  // src/pipeline/revision.ts
6360
- var import_node_fs8 = require("fs");
6403
+ var import_node_fs9 = require("fs");
6361
6404
  var import_node_path11 = __toESM(require("path"), 1);
6362
6405
  async function inspectRevisionReadiness(spec, lock) {
6363
6406
  if (!spec.revision) return null;
@@ -6373,14 +6416,14 @@ async function inspectRevision(spec, lock, seen) {
6373
6416
  reason: `revision source ${spec.styleId}/${revision.sourceAssetId} is not ready: ${dependency.reason}`
6374
6417
  };
6375
6418
  }
6376
- if (!revision.sourceSha256 || !(0, import_node_fs8.existsSync)(revision.sourceFile)) {
6419
+ if (!revision.sourceSha256 || !(0, import_node_fs9.existsSync)(revision.sourceFile)) {
6377
6420
  return { ready: false, reason: `revision source is missing: ${revision.sourceFile}` };
6378
6421
  }
6379
6422
  if (await sha256File(revision.sourceFile) !== revision.sourceSha256) {
6380
6423
  return { ready: false, reason: "revision source changed after the manifest was resolved" };
6381
6424
  }
6382
6425
  if (revision.maskFile) {
6383
- if (!revision.maskSha256 || !(0, import_node_fs8.existsSync)(revision.maskFile)) {
6426
+ if (!revision.maskSha256 || !(0, import_node_fs9.existsSync)(revision.maskFile)) {
6384
6427
  return { ready: false, reason: `revision mask is missing: ${revision.maskFile}` };
6385
6428
  }
6386
6429
  if (await sha256File(revision.maskFile) !== revision.maskSha256) {
@@ -6411,7 +6454,7 @@ async function inspectParent(spec, lock, seen) {
6411
6454
  }
6412
6455
  if (spec.source) {
6413
6456
  const source = import_node_path11.default.resolve(spec.root, spec.source);
6414
- return (0, import_node_fs8.existsSync)(source) ? { ready: true, reason: "declared source is present" } : { ready: false, reason: `declared source is missing: ${spec.source}` };
6457
+ return (0, import_node_fs9.existsSync)(source) ? { ready: true, reason: "declared source is present" } : { ready: false, reason: `declared source is missing: ${spec.source}` };
6415
6458
  }
6416
6459
  const entry = lock.entries[key];
6417
6460
  if (!entry) return { ready: false, reason: "parent is not in the lockfile" };
@@ -6425,7 +6468,7 @@ async function inspectParent(spec, lock, seen) {
6425
6468
  for (let index = 0; index < entry.outputs.length; index++) {
6426
6469
  const output = entry.outputs[index];
6427
6470
  const file = currentEntryOutputPath(entry, spec, index);
6428
- if (!(0, import_node_fs8.existsSync)(file)) return { ready: false, reason: `parent output is missing: ${file}` };
6471
+ if (!(0, import_node_fs9.existsSync)(file)) return { ready: false, reason: `parent output is missing: ${file}` };
6429
6472
  if (await sha256File(file) !== output.sha256) {
6430
6473
  return { ready: false, reason: `parent output was modified: ${file}` };
6431
6474
  }
@@ -6479,8 +6522,8 @@ async function buildPlan(specs, lock, opts = {}) {
6479
6522
  } else if (opts.force) {
6480
6523
  state = "missing";
6481
6524
  reason = "--force";
6482
- } else if (!entry && spec.source) {
6483
- if ((0, import_node_fs9.existsSync)(import_node_path12.default.resolve(spec.root, spec.source))) {
6525
+ } else if (!entry && spec.source && spec.generator !== "frames") {
6526
+ if ((0, import_node_fs10.existsSync)(import_node_path12.default.resolve(spec.root, spec.source))) {
6484
6527
  state = "ok";
6485
6528
  reason = `placed from ${spec.source}; not generated`;
6486
6529
  } else {
@@ -6488,7 +6531,7 @@ async function buildPlan(specs, lock, opts = {}) {
6488
6531
  reason = `declared source is missing: ${spec.source}`;
6489
6532
  }
6490
6533
  } else if (!entry) {
6491
- if ((0, import_node_fs9.existsSync)(spec.outFile)) {
6534
+ if ((0, import_node_fs10.existsSync)(spec.outFile)) {
6492
6535
  state = "untracked";
6493
6536
  reason = "file on disk, no provenance recorded";
6494
6537
  } else {
@@ -6520,9 +6563,9 @@ async function buildPlan(specs, lock, opts = {}) {
6520
6563
  } else if (entry.status !== "downloaded") {
6521
6564
  state = "in-flight";
6522
6565
  reason = `awaiting ${entry.status}`;
6523
- } else if (entry.outputs.length === 0 || entry.outputs.some((_output, index) => !(0, import_node_fs9.existsSync)(currentEntryOutputPath(entry, spec, index)))) {
6566
+ } else if (entry.outputs.length === 0 || entry.outputs.some((_output, index) => !(0, import_node_fs10.existsSync)(currentEntryOutputPath(entry, spec, index)))) {
6524
6567
  state = "orphaned";
6525
- reason = entry.outputs.length === 0 ? "no recorded output files" : `missing on disk: ${entry.outputs.filter((_output, index) => !(0, import_node_fs9.existsSync)(currentEntryOutputPath(entry, spec, index))).map((o) => o.role ?? "asset").join(", ")}`;
6568
+ reason = entry.outputs.length === 0 ? "no recorded output files" : `missing on disk: ${entry.outputs.filter((_output, index) => !(0, import_node_fs10.existsSync)(currentEntryOutputPath(entry, spec, index))).map((o) => o.role ?? "asset").join(", ")}`;
6526
6569
  } else if (await anyOutputModified(entry, spec)) {
6527
6570
  state = "orphaned";
6528
6571
  reason = "output modified since download";
@@ -6588,7 +6631,7 @@ function summarize(plan) {
6588
6631
 
6589
6632
  // src/pipeline/quality-regression.ts
6590
6633
  var import_node_path13 = __toESM(require("path"), 1);
6591
- var import_node_fs10 = require("fs");
6634
+ var import_node_fs11 = require("fs");
6592
6635
  var import_promises8 = require("fs/promises");
6593
6636
  var import_zod4 = require("zod");
6594
6637
  var SHA256_RE2 = /^[0-9a-f]{64}$/;
@@ -6831,7 +6874,7 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
6831
6874
  cases
6832
6875
  });
6833
6876
  const data = Buffer.from(JSON.stringify(baseline, null, 2) + "\n");
6834
- if ((0, import_node_fs10.existsSync)(absolute) && !options.force) {
6877
+ if ((0, import_node_fs11.existsSync)(absolute) && !options.force) {
6835
6878
  const current = await (0, import_promises8.readFile)(absolute);
6836
6879
  if (!current.equals(data)) {
6837
6880
  throw new Error(`Quality baseline already exists with different content: ${absolute}. Pass --force to replace it.`);
@@ -6968,13 +7011,13 @@ async function checkQualityBaseline(baselinePath) {
6968
7011
  }
6969
7012
 
6970
7013
  // src/pipeline/cache-health.ts
6971
- var import_node_fs12 = require("fs");
7014
+ var import_node_fs13 = require("fs");
6972
7015
  var import_promises10 = require("fs/promises");
6973
7016
  var import_node_path15 = __toESM(require("path"), 1);
6974
7017
 
6975
7018
  // src/cache.ts
6976
7019
  var import_promises9 = require("fs/promises");
6977
- var import_node_fs11 = require("fs");
7020
+ var import_node_fs12 = require("fs");
6978
7021
  var import_node_path14 = __toESM(require("path"), 1);
6979
7022
  var import_zod5 = require("zod");
6980
7023
  var HashCacheSchema = import_zod5.z.object({
@@ -6989,10 +7032,10 @@ function isSha256Hash(value) {
6989
7032
  function parseCache(value) {
6990
7033
  return HashCacheSchema.parse(value);
6991
7034
  }
6992
- async function loadCache(path27) {
6993
- if (!(0, import_node_fs11.existsSync)(path27)) return { version: 1, hashes: {} };
7035
+ async function loadCache(path29) {
7036
+ if (!(0, import_node_fs12.existsSync)(path29)) return { version: 1, hashes: {} };
6994
7037
  try {
6995
- const parsed = HashCacheSchema.safeParse(JSON.parse(await (0, import_promises9.readFile)(path27, "utf8")));
7038
+ const parsed = HashCacheSchema.safeParse(JSON.parse(await (0, import_promises9.readFile)(path29, "utf8")));
6996
7039
  if (!parsed.success) return { version: 1, hashes: {} };
6997
7040
  return {
6998
7041
  version: 1,
@@ -7004,18 +7047,18 @@ async function loadCache(path27) {
7004
7047
  return { version: 1, hashes: {} };
7005
7048
  }
7006
7049
  }
7007
- async function saveCache(path27, cache) {
7050
+ async function saveCache(path29, cache) {
7008
7051
  const sorted = {};
7009
7052
  for (const key of Object.keys(cache.hashes).sort()) {
7010
7053
  const hash = cache.hashes[key];
7011
7054
  if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
7012
7055
  sorted[key] = hash;
7013
7056
  }
7014
- await (0, import_promises9.mkdir)(import_node_path14.default.dirname(import_node_path14.default.resolve(path27)), { recursive: true });
7015
- const tmp = `${path27}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
7057
+ await (0, import_promises9.mkdir)(import_node_path14.default.dirname(import_node_path14.default.resolve(path29)), { recursive: true });
7058
+ const tmp = `${path29}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
7016
7059
  try {
7017
7060
  await (0, import_promises9.writeFile)(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
7018
- await (0, import_promises9.rename)(tmp, path27);
7061
+ await (0, import_promises9.rename)(tmp, path29);
7019
7062
  } finally {
7020
7063
  await (0, import_promises9.rm)(tmp, { force: true });
7021
7064
  }
@@ -7037,7 +7080,7 @@ function cachePathFor(lockPath) {
7037
7080
 
7038
7081
  // src/pipeline/cache-health.ts
7039
7082
  async function inspectCaches(lock, lockPath, options = {}) {
7040
- if (options.prune && !(0, import_node_fs12.existsSync)(lockPath)) {
7083
+ if (options.prune && !(0, import_node_fs13.existsSync)(lockPath)) {
7041
7084
  throw new Error(`Refusing to prune without an existing lockfile at ${import_node_path15.default.resolve(lockPath)}`);
7042
7085
  }
7043
7086
  const contentDir = import_node_path15.default.resolve(import_node_path15.default.dirname(lockPath), ".pixelkiln", "cache");
@@ -7082,7 +7125,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
7082
7125
  async function inspectContentCache(contentDir, referenced) {
7083
7126
  const report = {
7084
7127
  path: contentDir,
7085
- exists: (0, import_node_fs12.existsSync)(contentDir),
7128
+ exists: (0, import_node_fs13.existsSync)(contentDir),
7086
7129
  files: 0,
7087
7130
  bytes: 0,
7088
7131
  valid: 0,
@@ -7146,7 +7189,7 @@ async function inspectContentCache(contentDir, referenced) {
7146
7189
  async function inspectRemoteHashCache(remotePath) {
7147
7190
  const report = {
7148
7191
  path: remotePath,
7149
- exists: (0, import_node_fs12.existsSync)(remotePath),
7192
+ exists: (0, import_node_fs13.existsSync)(remotePath),
7150
7193
  entries: 0,
7151
7194
  valid: 0,
7152
7195
  invalidIds: [],
@@ -7170,7 +7213,7 @@ async function inspectRemoteHashCache(remotePath) {
7170
7213
  }
7171
7214
 
7172
7215
  // src/pipeline/pack.ts
7173
- var import_node_fs13 = require("fs");
7216
+ var import_node_fs14 = require("fs");
7174
7217
  var import_node_path16 = __toESM(require("path"), 1);
7175
7218
  function resolvePackInputs(raw, inputsFilePath) {
7176
7219
  if (!Array.isArray(raw) || !raw.length) {
@@ -7203,7 +7246,7 @@ function packSprites(inputs, options = {}) {
7203
7246
  for (const input of ordered) {
7204
7247
  let sourceHash = null;
7205
7248
  try {
7206
- const bytes = (0, import_node_fs13.readFileSync)(input.path);
7249
+ const bytes = (0, import_node_fs14.readFileSync)(input.path);
7207
7250
  sourceHash = sha256(bytes);
7208
7251
  const png = decodePng(bytes);
7209
7252
  sprites.push({ id: input.id, width: png.width, height: png.height, pixels: png.pixels });
@@ -7274,12 +7317,15 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
7274
7317
  for (const [key, entry] of entries) {
7275
7318
  const id = key.slice(prefix.length);
7276
7319
  locked.add(id);
7277
- const outputs = resolveEntryOutputs(entry, id, manifestDir);
7320
+ let outputs = resolveEntryOutputs(entry, id, manifestDir);
7278
7321
  const source = sources[id];
7279
7322
  if (source && outputs.length <= 1) {
7280
7323
  inputs.push({ id, path: import_node_path16.default.resolve(manifestDir, source) });
7281
7324
  continue;
7282
7325
  }
7326
+ if (source && sourceIsStem(source, entry, manifestDir)) {
7327
+ outputs = outputs.map((output) => ({ ...output, absolutePath: sourceOutputPath(source, entry, output.index, manifestDir) }));
7328
+ }
7283
7329
  let selected = outputs;
7284
7330
  if (options.outputRoles?.length) {
7285
7331
  selected = outputs.filter(
@@ -7332,7 +7378,7 @@ function mountSprites(placements, options) {
7332
7378
  for (const p of placements) {
7333
7379
  let sourceHash = null;
7334
7380
  try {
7335
- const bytes = (0, import_node_fs13.readFileSync)(p.path);
7381
+ const bytes = (0, import_node_fs14.readFileSync)(p.path);
7336
7382
  sourceHash = sha256(bytes);
7337
7383
  const png = decodePng(bytes);
7338
7384
  if (png.width > cellWidth || png.height > cellHeight) {
@@ -7401,11 +7447,21 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
7401
7447
  for (const id of ids) {
7402
7448
  const cell = cells[id];
7403
7449
  const source = sources[id];
7404
- if (source) {
7450
+ const entry = lock.entries[`${styleId}/${id}`];
7451
+ if (source && !(entry && sourceIsStem(source, entry, manifestDir))) {
7405
7452
  placements.push({ id, path: import_node_path16.default.resolve(manifestDir, source), cell });
7406
7453
  continue;
7407
7454
  }
7408
- const entry = lock.entries[`${styleId}/${id}`];
7455
+ if (source && entry) {
7456
+ const selection2 = selectEntryOutput(entry, outputRoles[id]);
7457
+ if (!selection2.ok) {
7458
+ skipped.push({ id, reason: selection2.reason });
7459
+ continue;
7460
+ }
7461
+ const index = entry.outputs.findIndex((output) => output.path === selection2.output.path);
7462
+ placements.push({ id, path: sourceOutputPath(source, entry, Math.max(index, 0), manifestDir), cell });
7463
+ continue;
7464
+ }
7409
7465
  if (!entry) {
7410
7466
  skipped.push({
7411
7467
  id,
@@ -7426,7 +7482,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
7426
7482
  `Nothing to mount for style "${styleId}": ${ids.length} asset(s) declare a cell, but none has generated output or a \`source\`. Run \`pixelkiln gen --style ${styleId}\`, or point each at a file.${detail}`
7427
7483
  );
7428
7484
  }
7429
- const basePng = mount.base ? (0, import_node_fs13.readFileSync)(import_node_path16.default.resolve(manifestDir, mount.base)) : void 0;
7485
+ const basePng = mount.base ? (0, import_node_fs14.readFileSync)(import_node_path16.default.resolve(manifestDir, mount.base)) : void 0;
7430
7486
  const mounted = mountSprites(placements, {
7431
7487
  cellWidth: mount.cellWidth,
7432
7488
  cellHeight: mount.cellHeight,
@@ -7472,8 +7528,11 @@ function normalizeTileRules(raw) {
7472
7528
  };
7473
7529
  }
7474
7530
  function exportTileset(entry, spec, options) {
7475
- const outputs = resolveSpecEntryOutputs(entry, spec);
7531
+ let outputs = resolveSpecEntryOutputs(entry, spec);
7476
7532
  if (!outputs.length) throw new Error(`${spec.styleId}/${spec.assetId} has no downloaded outputs`);
7533
+ if (spec.source && sourceIsStem(spec.source, entry, options.manifestDir)) {
7534
+ outputs = outputs.map((output) => ({ ...output, absolutePath: sourceOutputPath(spec.source, entry, output.index, options.manifestDir) }));
7535
+ }
7477
7536
  const packed = packSprites(
7478
7537
  outputs.map((output) => ({
7479
7538
  id: output.role ?? fallbackOutputRole(output.index),
@@ -7965,7 +8024,7 @@ async function poll(provider, lock, lockPath, opts = {}) {
7965
8024
  }
7966
8025
 
7967
8026
  // src/pipeline/fetch.ts
7968
- var import_node_fs14 = require("fs");
8027
+ var import_node_fs15 = require("fs");
7969
8028
  var import_promises11 = require("fs/promises");
7970
8029
  var import_node_path17 = __toESM(require("path"), 1);
7971
8030
  async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
@@ -7982,7 +8041,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
7982
8041
  if (opts.refresh) return e.outputs.length > 0 && Boolean(e.sourceUrls?.length || e.sourceUrl);
7983
8042
  if (!opts.repair) return false;
7984
8043
  const spec = specByKey.get(key);
7985
- return e.outputs.length === 0 || !spec || e.outputs.some((output) => !(0, import_node_fs14.existsSync)(resolveOutputPath(output.path, spec.root)));
8044
+ return e.outputs.length === 0 || !spec || e.outputs.some((output) => !(0, import_node_fs15.existsSync)(resolveOutputPath(output.path, spec.root)));
7986
8045
  });
7987
8046
  const concurrency = Math.min(Math.max(1, opts.concurrency ?? 8), Math.max(1, pending.length));
7988
8047
  let cursor = 0;
@@ -8030,7 +8089,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
8030
8089
  continue;
8031
8090
  }
8032
8091
  }
8033
- if ((0, import_node_fs14.existsSync)(target)) {
8092
+ if ((0, import_node_fs15.existsSync)(target)) {
8034
8093
  const currentHash = await sha256File(target);
8035
8094
  if (recorded && currentHash === recorded.sha256) {
8036
8095
  if (!fresh) {
@@ -8141,7 +8200,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
8141
8200
  }
8142
8201
  async function readCachedMedia(cacheDir, hash, mediaType) {
8143
8202
  const file = import_node_path17.default.join(cacheDir, cacheFileName(hash, mediaType));
8144
- if (!(0, import_node_fs14.existsSync)(file)) return null;
8203
+ if (!(0, import_node_fs15.existsSync)(file)) return null;
8145
8204
  try {
8146
8205
  const buf = await (0, import_promises11.readFile)(file);
8147
8206
  if (sha256(buf) !== hash) return null;
@@ -8162,7 +8221,7 @@ async function cacheMedia(cacheDir, buf, mediaType, knownHash) {
8162
8221
  try {
8163
8222
  await (0, import_promises11.rename)(tmp, file);
8164
8223
  } catch (err) {
8165
- if (!(0, import_node_fs14.existsSync)(file)) throw err;
8224
+ if (!(0, import_node_fs15.existsSync)(file)) throw err;
8166
8225
  } finally {
8167
8226
  await (0, import_promises11.rm)(tmp, { force: true });
8168
8227
  }
@@ -8195,7 +8254,7 @@ async function pushTags(provider, specs, lock, opts = {}) {
8195
8254
  }
8196
8255
 
8197
8256
  // src/pipeline/doctor.ts
8198
- var import_node_fs15 = require("fs");
8257
+ var import_node_fs16 = require("fs");
8199
8258
  var import_promises12 = require("fs/promises");
8200
8259
  var import_node_path18 = __toESM(require("path"), 1);
8201
8260
  async function doctor(loaded, specs, lock, lockPath, opts = {}) {
@@ -8215,7 +8274,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
8215
8274
  for (const dir of dirs) {
8216
8275
  const ancestor = await nearestExistingDirectory(dir);
8217
8276
  try {
8218
- await (0, import_promises12.access)(ancestor, import_node_fs15.constants.W_OK);
8277
+ await (0, import_promises12.access)(ancestor, import_node_fs16.constants.W_OK);
8219
8278
  } catch {
8220
8279
  unwritable.push(dir);
8221
8280
  }
@@ -8267,7 +8326,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
8267
8326
  cacheDir,
8268
8327
  cacheFileName(output.sha256, output.mediaType ?? MediaType.PNG)
8269
8328
  );
8270
- if ((0, import_node_fs15.existsSync)(cached) && await sha256File(cached) === output.sha256) {
8329
+ if ((0, import_node_fs16.existsSync)(cached) && await sha256File(cached) === output.sha256) {
8271
8330
  hasCache = true;
8272
8331
  break;
8273
8332
  }
@@ -8370,7 +8429,7 @@ async function nearestExistingDirectory(start) {
8370
8429
  }
8371
8430
 
8372
8431
  // src/pipeline/adopt.ts
8373
- var import_node_fs16 = require("fs");
8432
+ var import_node_fs17 = require("fs");
8374
8433
  var import_promises13 = require("fs/promises");
8375
8434
  var import_node_path19 = __toESM(require("path"), 1);
8376
8435
  async function adopt(provider, specs, lock, lockPath, opts = {}) {
@@ -8383,7 +8442,7 @@ async function adopt(provider, specs, lock, lockPath, opts = {}) {
8383
8442
  const localByHash = /* @__PURE__ */ new Map();
8384
8443
  const unmatchedLocal = [];
8385
8444
  for (const spec of specs) {
8386
- if (!(0, import_node_fs16.existsSync)(spec.outFile)) {
8445
+ if (!(0, import_node_fs17.existsSync)(spec.outFile)) {
8387
8446
  unmatchedLocal.push(`${spec.styleId}/${spec.assetId} (no file at ${spec.outFile})`);
8388
8447
  continue;
8389
8448
  }
@@ -8505,12 +8564,12 @@ async function tagAdopted(provider, specs, lock, opts = {}) {
8505
8564
  }
8506
8565
 
8507
8566
  // src/pick/server.ts
8508
- var import_node_fs18 = require("fs");
8567
+ var import_node_fs19 = require("fs");
8509
8568
  var import_promises16 = require("fs/promises");
8510
8569
 
8511
8570
  // src/pipeline/init.ts
8512
8571
  var import_promises14 = require("fs/promises");
8513
- var import_node_fs17 = require("fs");
8572
+ var import_node_fs18 = require("fs");
8514
8573
  var import_node_path20 = __toESM(require("path"), 1);
8515
8574
  function pngSize(buf) {
8516
8575
  if (buf.length < 24) return null;
@@ -9083,7 +9142,7 @@ async function currentArt(entry, spec) {
9083
9142
  const previous = primaryOutput({ ...entry, outputs: entry.supersededOutputs });
9084
9143
  if (!previous) return null;
9085
9144
  const file = resolveOutputPath(previous.path, spec.root);
9086
- if (!(0, import_node_fs18.existsSync)(file)) return null;
9145
+ if (!(0, import_node_fs19.existsSync)(file)) return null;
9087
9146
  const contentType = previous.mediaType ?? mediaTypeFromExtension(file);
9088
9147
  if (!contentType) return null;
9089
9148
  let width = entry.width;
@@ -9254,13 +9313,13 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
9254
9313
 
9255
9314
  // src/gallery/snapshot.ts
9256
9315
  var import_node_crypto6 = require("crypto");
9257
- var import_node_fs21 = require("fs");
9316
+ var import_node_fs22 = require("fs");
9258
9317
  var import_promises20 = require("fs/promises");
9259
9318
  var import_node_path25 = __toESM(require("path"), 1);
9260
9319
 
9261
9320
  // src/workspace.ts
9262
9321
  var import_promises17 = require("fs/promises");
9263
- var import_node_fs19 = require("fs");
9322
+ var import_node_fs20 = require("fs");
9264
9323
  var import_node_path21 = __toESM(require("path"), 1);
9265
9324
  var import_zod6 = require("zod");
9266
9325
  var WorkspaceProjectSchema = import_zod6.z.object({
@@ -9286,7 +9345,7 @@ ${parsed.error.issues.slice(0, 5).map((i) => ` ${i.path.join(".")}: ${i.message
9286
9345
  );
9287
9346
  }
9288
9347
  async function loadWorkspace(workspacePath) {
9289
- if (!(0, import_node_fs19.existsSync)(workspacePath)) return { version: 1, projects: [] };
9348
+ if (!(0, import_node_fs20.existsSync)(workspacePath)) return { version: 1, projects: [] };
9290
9349
  let raw;
9291
9350
  try {
9292
9351
  raw = JSON.parse(await (0, import_promises17.readFile)(workspacePath, "utf8"));
@@ -9338,14 +9397,14 @@ function validateWorkspace(ws, dir) {
9338
9397
  message: `project "${project.id}" stores an absolute path \u2014 the catalog will not resolve correctly if this tree is cloned or moved elsewhere`
9339
9398
  });
9340
9399
  }
9341
- if (!(0, import_node_fs19.existsSync)(manifestPath)) {
9400
+ if (!(0, import_node_fs20.existsSync)(manifestPath)) {
9342
9401
  diagnostics.push({
9343
9402
  id: "missing-manifest",
9344
9403
  level: "error",
9345
9404
  message: `project "${project.id}" manifest not found: ${manifestPath}`
9346
9405
  });
9347
9406
  }
9348
- if (!(0, import_node_fs19.existsSync)(lockPath)) {
9407
+ if (!(0, import_node_fs20.existsSync)(lockPath)) {
9349
9408
  diagnostics.push({
9350
9409
  id: "missing-lock",
9351
9410
  level: "error",
@@ -9646,7 +9705,7 @@ async function applyManifestEdit(manifestPath, edit) {
9646
9705
 
9647
9706
  // src/pipeline/hand-edit.ts
9648
9707
  var import_node_child_process3 = require("child_process");
9649
- var import_node_fs20 = require("fs");
9708
+ var import_node_fs21 = require("fs");
9650
9709
  var import_promises19 = require("fs/promises");
9651
9710
  var import_node_path23 = __toESM(require("path"), 1);
9652
9711
  var import_zod8 = require("zod");
@@ -9659,36 +9718,63 @@ function handEditPath(loaded, spec) {
9659
9718
  const file = inside ? relative : import_node_path23.default.basename(spec.outFile);
9660
9719
  return import_node_path23.default.join(outDir, HAND_EDIT_DIR, file.replace(/\.gif$/i, ".png"));
9661
9720
  }
9662
- function handEditBase(spec, lock) {
9721
+ function handEditBases(spec, lock) {
9663
9722
  const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
9664
9723
  if (entry) {
9665
- if (entry.outputs.length !== 1) return null;
9666
- const primary = primaryOutput(entry);
9667
- if (!primary || primary.mediaType && primary.mediaType !== "image/png") return null;
9668
- const file = currentEntryOutputPath(entry, spec, 0);
9669
- return (0, import_node_fs20.existsSync)(file) ? file : null;
9724
+ if (entry.outputs.length !== 1 && !isMemberSetEntry(entry)) return null;
9725
+ const members = entry.outputs.map((output, index) => ({
9726
+ role: entry.outputs.length === 1 ? null : output.role ?? null,
9727
+ path: currentEntryOutputPath(entry, spec, index),
9728
+ mediaType: output.mediaType
9729
+ }));
9730
+ if (members.some((member) => member.mediaType && member.mediaType !== "image/png")) return null;
9731
+ if (entry.outputs.length === 1 && !primaryOutput(entry)) return null;
9732
+ if (!members.every((member) => (0, import_node_fs21.existsSync)(member.path))) return null;
9733
+ return members.map(({ role, path: file }) => ({ role, path: file }));
9734
+ }
9735
+ return (0, import_node_fs21.existsSync)(spec.outFile) ? [{ role: null, path: spec.outFile }] : null;
9736
+ }
9737
+ function handEditBase(spec, lock) {
9738
+ const bases = handEditBases(spec, lock);
9739
+ return bases && bases.length === 1 ? bases[0].path : null;
9740
+ }
9741
+ function handEditMembers(loaded, spec, lock, editPath) {
9742
+ const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
9743
+ const bases = handEditBases(spec, lock);
9744
+ if (entry && isMemberSetEntry(entry)) {
9745
+ return entry.outputs.map((output, index) => ({
9746
+ role: output.role ?? null,
9747
+ path: memberPath(editPath, output.role, index, entry.outputs.length, output.mediaType),
9748
+ base: bases?.[index]?.path ?? null
9749
+ }));
9670
9750
  }
9671
- return (0, import_node_fs20.existsSync)(spec.outFile) ? spec.outFile : null;
9751
+ return [{ role: null, path: editPath, base: bases?.[0]?.path ?? null }];
9672
9752
  }
9673
9753
  async function startHandEdit(loaded, lock, spec, opts = {}) {
9674
9754
  const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
9675
- if (entry && entry.outputs.length > 1) {
9755
+ if (entry && entry.outputs.length > 1 && !isMemberSetEntry(entry)) {
9676
9756
  throw new ManifestEditError(
9677
- `${spec.styleId}/${spec.assetId} has ${entry.outputs.length} outputs; hand edits currently cover single-image assets`
9757
+ `${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`
9758
+ );
9759
+ }
9760
+ if (entry && spec.source && isMemberSetEntry(entry) && !sourceIsStem(spec.source, entry, loaded.root)) {
9761
+ throw new ManifestEditError(
9762
+ `${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`
9678
9763
  );
9679
9764
  }
9680
9765
  const existing = spec.source ? import_node_path23.default.resolve(loaded.root, spec.source) : null;
9681
9766
  const editPath = existing ?? handEditPath(loaded, spec);
9767
+ const members = handEditMembers(loaded, spec, lock, editPath);
9682
9768
  let created = false;
9683
- if (!(0, import_node_fs20.existsSync)(editPath)) {
9684
- const base = handEditBase(spec, lock);
9685
- if (!base) {
9769
+ for (const member of members) {
9770
+ if ((0, import_node_fs21.existsSync)(member.path)) continue;
9771
+ if (!member.base) {
9686
9772
  throw new ManifestEditError(
9687
9773
  `${spec.styleId}/${spec.assetId} has no generated PNG on disk to start from; generate or restore it first`
9688
9774
  );
9689
9775
  }
9690
- await (0, import_promises19.mkdir)(import_node_path23.default.dirname(editPath), { recursive: true });
9691
- await (0, import_promises19.copyFile)(base, editPath);
9776
+ await (0, import_promises19.mkdir)(import_node_path23.default.dirname(member.path), { recursive: true });
9777
+ await (0, import_promises19.copyFile)(member.base, member.path);
9692
9778
  created = true;
9693
9779
  }
9694
9780
  const source = portableOutputPath(editPath, loaded.root);
@@ -9705,7 +9791,7 @@ async function startHandEdit(loaded, lock, spec, opts = {}) {
9705
9791
  declared = result.changed;
9706
9792
  manifestSha256 = result.sha256;
9707
9793
  }
9708
- return { editPath, source, created, declared, manifestSha256 };
9794
+ return { editPath, members, source, created, declared, manifestSha256 };
9709
9795
  }
9710
9796
  async function detachHandEdit(loaded, spec, opts = {}) {
9711
9797
  if (!spec.source) {
@@ -9719,19 +9805,44 @@ async function detachHandEdit(loaded, spec, opts = {}) {
9719
9805
  });
9720
9806
  return { manifestSha256: result.sha256, changed: result.changed };
9721
9807
  }
9722
- var HandEditCompanionSchema = import_zod8.z.object({
9723
- version: import_zod8.z.literal(1),
9724
- /** sha256 of the generated art the edit started from; null when it started from untracked art. */
9808
+ var CompanionOutputSchema = import_zod8.z.object({
9809
+ /** Output role for a set member; null for a single image. */
9810
+ role: import_zod8.z.string().nullable(),
9811
+ /** sha256 of the generated file this member started from; null when it started from untracked art. */
9725
9812
  basedOn: import_zod8.z.string().regex(/^[0-9a-f]{64}$/).nullable(),
9726
- /** sha256 of the edit PNG as saved, so a later change by another tool is visible. */
9727
- sha256: import_zod8.z.string().regex(/^[0-9a-f]{64}$/),
9813
+ /** sha256 of the member as saved, so a later change by another tool is visible. */
9814
+ sha256: import_zod8.z.string().regex(/^[0-9a-f]{64}$/)
9815
+ }).strict();
9816
+ var CompanionV2Schema = import_zod8.z.object({
9817
+ version: import_zod8.z.literal(2),
9728
9818
  /** Editor identity the bridge reported, e.g. `pixelorama@v1.2.2-stable`. */
9729
9819
  editor: import_zod8.z.string().min(1),
9730
9820
  protocol: import_zod8.z.number().int().positive(),
9731
9821
  savedAt: import_zod8.z.string().datetime(),
9732
9822
  /** Basename of the layered project file beside the edit, when one was kept. */
9823
+ project: import_zod8.z.string().min(1).nullable(),
9824
+ /** One entry per file, in output order. */
9825
+ outputs: import_zod8.z.array(CompanionOutputSchema).min(1)
9826
+ }).strict();
9827
+ var CompanionV1Schema = import_zod8.z.object({
9828
+ version: import_zod8.z.literal(1),
9829
+ basedOn: import_zod8.z.string().regex(/^[0-9a-f]{64}$/).nullable(),
9830
+ sha256: import_zod8.z.string().regex(/^[0-9a-f]{64}$/),
9831
+ editor: import_zod8.z.string().min(1),
9832
+ protocol: import_zod8.z.number().int().positive(),
9833
+ savedAt: import_zod8.z.string().datetime(),
9733
9834
  project: import_zod8.z.string().min(1).nullable()
9734
9835
  }).strict();
9836
+ var HandEditCompanionSchema = import_zod8.z.union([CompanionV2Schema, CompanionV1Schema]).transform(
9837
+ (value) => value.version === 2 ? value : {
9838
+ version: 2,
9839
+ editor: value.editor,
9840
+ protocol: value.protocol,
9841
+ savedAt: value.savedAt,
9842
+ project: value.project,
9843
+ outputs: [{ role: null, basedOn: value.basedOn, sha256: value.sha256 }]
9844
+ }
9845
+ );
9735
9846
  function handEditCompanionPath(editPath) {
9736
9847
  return editPath.replace(/\.png$/i, "") + ".edit.json";
9737
9848
  }
@@ -9753,45 +9864,79 @@ async function readHandEditCompanion(editPath) {
9753
9864
  }
9754
9865
  var MAX_HAND_EDIT_BYTES = 16 * 1024 * 1024;
9755
9866
  async function saveHandEdit(loaded, lock, spec, input) {
9756
- if (input.png.length > MAX_HAND_EDIT_BYTES) {
9757
- throw new ManifestEditError(`the edit is ${input.png.length} bytes; the limit is ${MAX_HAND_EDIT_BYTES}`);
9758
- }
9867
+ const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
9868
+ const memberSet = entry ? isMemberSetEntry(entry) : false;
9869
+ const incoming = input.frames ?? (input.png ? [{ role: null, png: input.png }] : []);
9870
+ if (!incoming.length) throw new ManifestEditError("the save carried no image");
9759
9871
  if (input.project && input.project.length > MAX_HAND_EDIT_BYTES) {
9760
9872
  throw new ManifestEditError(`the project file is ${input.project.length} bytes; the limit is ${MAX_HAND_EDIT_BYTES}`);
9761
9873
  }
9762
- let decoded;
9763
- try {
9764
- validateMedia(input.png, MediaType.PNG);
9765
- decoded = decodePng(input.png);
9766
- } catch (error) {
9767
- throw new ManifestEditError(`the edit is not a valid PNG: ${error instanceof Error ? error.message : String(error)}`);
9874
+ if (memberSet) {
9875
+ const expected = entry.outputs.map((output) => output.role ?? null);
9876
+ const got = incoming.map((frame) => frame.role);
9877
+ if (got.length !== expected.length || expected.some((role, index) => got[index] !== role)) {
9878
+ throw new ManifestEditError(
9879
+ `${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`
9880
+ );
9881
+ }
9882
+ } else if (incoming.length !== 1) {
9883
+ throw new ManifestEditError(`${spec.styleId}/${spec.assetId} is one image; the editor returned ${incoming.length} frames \u2014 keep a single frame`);
9768
9884
  }
9769
- const base = handEditBase(spec, lock);
9770
- const expected = base ? decodePng(await (0, import_promises19.readFile)(base)) : { width: spec.width, height: spec.height };
9771
- if (decoded.width !== expected.width || decoded.height !== expected.height) {
9772
- throw new ManifestEditError(
9773
- `the edit is ${decoded.width}\xD7${decoded.height}; ${spec.styleId}/${spec.assetId} is ${expected.width}\xD7${expected.height}`
9774
- );
9885
+ const decoded = incoming.map((frame, index) => {
9886
+ if (frame.png.length > MAX_HAND_EDIT_BYTES) {
9887
+ throw new ManifestEditError(`the edit is ${frame.png.length} bytes; the limit is ${MAX_HAND_EDIT_BYTES}`);
9888
+ }
9889
+ try {
9890
+ validateMedia(frame.png, MediaType.PNG);
9891
+ return decodePng(frame.png);
9892
+ } catch (error) {
9893
+ const which = memberSet ? `member ${frame.role ?? index}` : "the edit";
9894
+ throw new ManifestEditError(`${which} is not a valid PNG: ${error instanceof Error ? error.message : String(error)}`);
9895
+ }
9896
+ });
9897
+ const bases = handEditBases(spec, lock);
9898
+ const expectedSize = bases?.[0] ? decodePng(await (0, import_promises19.readFile)(bases[0].path)) : { width: spec.width, height: spec.height };
9899
+ for (const [index, image] of decoded.entries()) {
9900
+ if (image.width !== expectedSize.width || image.height !== expectedSize.height) {
9901
+ const which = memberSet ? `member ${incoming[index].role ?? index}` : "the edit";
9902
+ throw new ManifestEditError(
9903
+ `${which} is ${image.width}\xD7${image.height}; ${spec.styleId}/${spec.assetId} is ${expectedSize.width}\xD7${expectedSize.height}`
9904
+ );
9905
+ }
9775
9906
  }
9776
9907
  const started = await startHandEdit(loaded, lock, spec, { expectedSha256: input.expectedSha256 });
9777
- const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
9778
- const basedOn = entry ? primaryOutput(entry)?.sha256 ?? null : null;
9779
- const digest2 = sha256(input.png);
9780
9908
  const projectPath = input.project ? handEditProjectPath(started.editPath) : null;
9781
9909
  const companion = {
9782
- version: 1,
9783
- basedOn,
9784
- sha256: digest2,
9910
+ version: 2,
9785
9911
  editor: input.editor,
9786
9912
  protocol: input.protocol,
9787
9913
  savedAt: (input.now ?? /* @__PURE__ */ new Date()).toISOString(),
9788
- project: projectPath ? import_node_path23.default.basename(projectPath) : null
9914
+ project: projectPath ? import_node_path23.default.basename(projectPath) : null,
9915
+ outputs: incoming.map((frame, index) => ({
9916
+ role: frame.role,
9917
+ basedOn: basedOnHash(entry, index),
9918
+ sha256: sha256(frame.png)
9919
+ }))
9789
9920
  };
9790
- await replaceFile(started.editPath, input.png);
9921
+ for (const [index, member] of started.members.entries()) {
9922
+ await replaceFile(member.path, incoming[index].png);
9923
+ }
9791
9924
  if (projectPath && input.project) await replaceFile(projectPath, input.project);
9792
9925
  const companionPath = handEditCompanionPath(started.editPath);
9793
9926
  await replaceFile(companionPath, JSON.stringify(companion, null, 2) + "\n");
9794
- return { ...started, sha256: digest2, basedOn, companionPath, projectPath };
9927
+ return {
9928
+ ...started,
9929
+ sha256: companion.outputs[0].sha256,
9930
+ basedOn: companion.outputs[0].basedOn,
9931
+ companion,
9932
+ companionPath,
9933
+ projectPath
9934
+ };
9935
+ }
9936
+ function basedOnHash(entry, index) {
9937
+ if (!entry) return null;
9938
+ if (entry.outputs.length === 1) return primaryOutput(entry)?.sha256 ?? null;
9939
+ return entry.outputs[index]?.sha256 ?? null;
9795
9940
  }
9796
9941
  async function replaceFile(file, bytes) {
9797
9942
  const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
@@ -9838,7 +9983,10 @@ var HandEditRequestSchema = import_zod9.z.discriminatedUnion("action", [
9838
9983
  assetId: import_zod9.z.string().min(1),
9839
9984
  styleId: import_zod9.z.string().min(1),
9840
9985
  expectedSha256: import_zod9.z.string().regex(/^[0-9a-f]{64}$/),
9841
- png: Base64.min(1),
9986
+ /** The flattened image; for a frame set, `frames` instead. */
9987
+ png: Base64.min(1).optional(),
9988
+ /** Every frame of a set, tagged with the role it was opened under (null for one added in the editor). */
9989
+ frames: import_zod9.z.array(import_zod9.z.object({ role: import_zod9.z.string().min(1).nullable(), png: Base64.min(1) }).strict()).min(1).max(256).optional(),
9842
9990
  /** The editor's layered project file, kept beside the edit. */
9843
9991
  pxo: Base64.optional(),
9844
9992
  /** Editor identity from the bridge's ready message. */
@@ -9861,18 +10009,23 @@ function createGalleryEditHandler(opts) {
9861
10009
  const started = await startHandEdit(ctx.loaded, ctx.lock, spec, { expectedSha256: request.expectedSha256 });
9862
10010
  log(` hand edit ${started.created ? "created" : "found"}: ${started.source}${started.declared ? " (declared in the manifest)" : ""}`);
9863
10011
  if (request.open) {
9864
- const command = (opts.openEditor ?? openInEditor)(started.editPath);
10012
+ const command = (opts.openEditor ?? openInEditor)(started.members[0].path);
9865
10013
  log(` opened with: ${command}`);
9866
10014
  }
9867
10015
  } else if (request.action === "save-edit") {
10016
+ if (request.png !== void 0 === (request.frames !== void 0)) {
10017
+ throw new ManifestEditError("invalid edit: send png for one image or frames for a set, not both");
10018
+ }
9868
10019
  const saved = await saveHandEdit(ctx.loaded, ctx.lock, spec, {
9869
- png: Buffer.from(request.png, "base64"),
10020
+ ...request.png !== void 0 ? { png: Buffer.from(request.png, "base64") } : {},
10021
+ ...request.frames ? { frames: request.frames.map((frame) => ({ role: frame.role, png: Buffer.from(frame.png, "base64") })) } : {},
9870
10022
  ...request.pxo ? { project: Buffer.from(request.pxo, "base64") } : {},
9871
10023
  editor: request.editor,
9872
10024
  protocol: request.protocol,
9873
10025
  expectedSha256: request.expectedSha256
9874
10026
  });
9875
- log(` hand edit saved from ${request.editor}: ${saved.source}${saved.declared ? " (declared in the manifest)" : ""}${saved.projectPath ? " + project file" : ""}`);
10027
+ const what = saved.members.length > 1 ? `${saved.source} (${saved.members.length} frames)` : saved.source;
10028
+ log(` hand edit saved from ${request.editor}: ${what}${saved.declared ? " (declared in the manifest)" : ""}${saved.projectPath ? " + project file" : ""}`);
9876
10029
  } else {
9877
10030
  const result2 = await detachHandEdit(ctx.loaded, spec, { expectedSha256: request.expectedSha256 });
9878
10031
  if (result2.changed) log(` hand edit detached: ${request.styleId}/${request.assetId} (file kept)`);
@@ -9920,6 +10073,18 @@ function matchesFilter(filter, styleId, assetId) {
9920
10073
  if (filter?.assets?.length && !filter.assets.includes(assetId)) return false;
9921
10074
  return true;
9922
10075
  }
10076
+ var MAX_COMPARED_BYTES = 8 * 1024 * 1024;
10077
+ async function samePixels(a, b) {
10078
+ try {
10079
+ const [left, right] = await Promise.all([(0, import_promises20.readFile)(a), (0, import_promises20.readFile)(b)]);
10080
+ if (left.length > MAX_COMPARED_BYTES || right.length > MAX_COMPARED_BYTES) return false;
10081
+ const x = decodePng(left);
10082
+ const y = decodePng(right);
10083
+ return x.width === y.width && x.height === y.height && x.pixels.equals(y.pixels);
10084
+ } catch {
10085
+ return false;
10086
+ }
10087
+ }
9923
10088
  function metadataFps(entry) {
9924
10089
  if (!entry) return null;
9925
10090
  const namespace = entry.providerMetadata?.[entry.provider];
@@ -9967,7 +10132,7 @@ async function describeQuality(media, root, inspection) {
9967
10132
  outputPaths.map((file) => describeOutput(media, root, import_node_path25.default.resolve(root, file)))
9968
10133
  );
9969
10134
  const recordPath = import_node_path25.default.resolve(root, inspection.record);
9970
- const recordExists = (0, import_node_fs21.existsSync)(recordPath);
10135
+ const recordExists = (0, import_node_fs22.existsSync)(recordPath);
9971
10136
  let options = null;
9972
10137
  let check = null;
9973
10138
  if (recordExists) {
@@ -10037,6 +10202,8 @@ async function buildGallerySnapshot(opts) {
10037
10202
  const asset = loaded.manifest.assets[spec.assetId] ?? null;
10038
10203
  let outputs;
10039
10204
  let edit = null;
10205
+ let edits = [];
10206
+ let editChanged = [];
10040
10207
  let editStatus = null;
10041
10208
  let editMeta = null;
10042
10209
  if (entry && entry.outputs.length) {
@@ -10047,13 +10214,19 @@ async function buildGallerySnapshot(opts) {
10047
10214
  );
10048
10215
  if (spec.source) {
10049
10216
  const editPath = import_node_path25.default.resolve(root, spec.source);
10050
- const editSha = (0, import_node_fs21.existsSync)(editPath) ? await sha256File(editPath) : null;
10051
- edit = await describeOutput(media, root, editPath, { sha256: editSha });
10052
- const generated = outputs[0];
10217
+ const memberCount = sourceIsStem(spec.source, entry, root) ? entry.outputs.length : 1;
10218
+ const editShas = await Promise.all(
10219
+ Array.from({ length: memberCount }, async (_, index) => {
10220
+ const file = sourceOutputPath(spec.source, entry, index, root);
10221
+ return { file, sha256: (0, import_node_fs22.existsSync)(file) ? await sha256File(file) : null };
10222
+ })
10223
+ );
10224
+ edits = await Promise.all(editShas.map(({ file, sha256: editSha }) => describeOutput(media, root, file, { sha256: editSha })));
10225
+ edit = edits[0] ?? null;
10053
10226
  const companion = await readHandEditCompanion(editPath);
10054
10227
  if (companion) {
10055
10228
  const projectPath = handEditProjectPath(editPath);
10056
- const project = companion.project && (0, import_node_fs21.existsSync)(projectPath) ? await fileInfo(projectPath) : null;
10229
+ const project = companion.project && (0, import_node_fs22.existsSync)(projectPath) ? await fileInfo(projectPath) : null;
10057
10230
  let projectUrl = null;
10058
10231
  if (project) {
10059
10232
  const id = galleryMediaId(projectPath);
@@ -10063,18 +10236,34 @@ async function buildGallerySnapshot(opts) {
10063
10236
  editMeta = {
10064
10237
  editor: companion.editor,
10065
10238
  savedAt: companion.savedAt,
10066
- basedOn: companion.basedOn,
10067
- changedSince: editSha !== null && editSha !== companion.sha256,
10239
+ basedOn: companion.outputs[0]?.basedOn ?? null,
10240
+ changedSince: editShas.some(({ sha256: editSha }, index) => {
10241
+ const recorded = companion.outputs[index]?.sha256;
10242
+ return editSha !== null && recorded !== void 0 && editSha !== recorded;
10243
+ }),
10068
10244
  project: project ? portableOutputPath(projectPath, root) : null,
10069
10245
  projectUrl
10070
10246
  };
10071
10247
  }
10072
- const regenerated = companion ? generated?.sha256 !== void 0 && generated.sha256 !== null && companion.basedOn !== generated.sha256 : Boolean(generated?.modifiedAt && edit.modifiedAt && generated.modifiedAt > edit.modifiedAt && generated.exists);
10073
- editStatus = !edit.exists ? "missing" : generated && editSha === generated.sha256 ? "same" : regenerated ? "regenerated-since" : "edited";
10248
+ const regenerated = edits.some((member, index) => {
10249
+ const generated = outputs[index];
10250
+ if (companion) {
10251
+ const recorded = companion.outputs[index];
10252
+ return recorded !== void 0 && generated.sha256 !== null && recorded.basedOn !== generated.sha256;
10253
+ }
10254
+ return Boolean(generated.modifiedAt && member.modifiedAt && generated.modifiedAt > member.modifiedAt && generated.exists);
10255
+ });
10256
+ editChanged = await Promise.all(edits.map(async (member, index) => {
10257
+ const generated = outputs[index];
10258
+ if (!member.exists || !generated.exists) return true;
10259
+ if (member.sha256 === generated.sha256) return false;
10260
+ return !await samePixels(member.absolutePath, generated.absolutePath);
10261
+ }));
10262
+ editStatus = edits.some((member) => !member.exists) ? "missing" : editChanged.every((changed) => !changed) ? "same" : regenerated ? "regenerated-since" : "edited";
10074
10263
  }
10075
10264
  } else if (spec.source) {
10076
10265
  outputs = [await describeOutput(media, root, import_node_path25.default.resolve(root, spec.source))];
10077
- } else if (!entry && (0, import_node_fs21.existsSync)(spec.outFile)) {
10266
+ } else if (!entry && (0, import_node_fs22.existsSync)(spec.outFile)) {
10078
10267
  outputs = [await describeOutput(media, root, spec.outFile)];
10079
10268
  } else {
10080
10269
  outputs = [];
@@ -10119,6 +10308,8 @@ async function buildGallerySnapshot(opts) {
10119
10308
  asset,
10120
10309
  source: spec.source ?? null,
10121
10310
  edit,
10311
+ edits,
10312
+ editChanged,
10122
10313
  editStatus,
10123
10314
  editMeta,
10124
10315
  upstreamUrl: entry?.provider === "pixellab" ? pixelLabObjectUrl(entry.generator, entry.objectId) : null,
@@ -10176,6 +10367,8 @@ async function buildGallerySnapshot(opts) {
10176
10367
  asset: null,
10177
10368
  source: null,
10178
10369
  edit: null,
10370
+ edits: [],
10371
+ editChanged: [],
10179
10372
  editStatus: null,
10180
10373
  editMeta: null,
10181
10374
  upstreamUrl: entry.provider === "pixellab" ? pixelLabObjectUrl(entry.generator, entry.objectId) : null,
@@ -11104,10 +11297,15 @@ window.addEventListener('message', (e) => {
11104
11297
  // declares the file the same way pixelkiln edit does.
11105
11298
 
11106
11299
  let SHEET = null;
11107
- const editorSource = (item) => item.edit && item.edit.exists ? item.edit
11108
- : item.outputs.length === 1 && item.outputs[0].exists && item.outputs[0].mediaType === 'image/png' ? item.outputs[0] : null;
11109
- const canEditInBrowser = (item) => EDITOR && EDITABLE && item.declared && item.asset && projectOf(item)?.manifestSha256 && editorSource(item) &&
11110
- !(item.outputs.length > 1);
11300
+ // What the editor gets: the edit files when they are all there, else the
11301
+ // generated outputs (one PNG, or every frame of a set).
11302
+ const editorSources = (item) => {
11303
+ if (item.edits.length && item.edits.every((e) => e.exists)) return { files: item.edits, fromEdit: true };
11304
+ if (editableOutputs(item)) return { files: item.outputs, fromEdit: false };
11305
+ return null;
11306
+ };
11307
+ const editorSource = (item) => { const s = editorSources(item); return s ? s.files[0] : null; };
11308
+ const canEditInBrowser = (item) => EDITOR && EDITABLE && item.declared && item.asset && projectOf(item)?.manifestSha256 && editorSources(item);
11111
11309
  const toBase64 = (buf) => new Promise((resolve, reject) => {
11112
11310
  const r = new FileReader();
11113
11311
  r.onload = () => resolve(String(r.result).slice(String(r.result).indexOf(',') + 1));
@@ -11136,7 +11334,7 @@ function openEditorSheet(item) {
11136
11334
  const bar = el('div', 'rbar');
11137
11335
  const close = el('button', null, 'Close'); close.type = 'button';
11138
11336
  close.onclick = () => closeEditorSheet();
11139
- const title = el('b', null, (item.project ? item.project + ':' : '') + item.styleId + '/' + item.assetId + ' \xB7 ' + item.width + '\xD7' + item.height);
11337
+ 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) : ''));
11140
11338
  const status = el('span', 'st');
11141
11339
  const acts = el('div', 'acts');
11142
11340
  const msg = el('span', 'msg');
@@ -11157,23 +11355,34 @@ function openEditorSheet(item) {
11157
11355
  stage.append(frame, loading);
11158
11356
  panel.append(bar, stage);
11159
11357
  host.append(scrim, panel);
11160
- SHEET = { item, frame, loading, status, msg, save, saveClose, dirty: false, opened: false, ready: null, pending: null, requests: 0, source: src, sentProject: false };
11358
+ SHEET = { item, frame, loading, status, msg, save, saveClose, dirty: false, opened: false, ready: null, pending: null, requests: 0, source: src, sentProject: false, roles: null };
11161
11359
  sheetStatus('cool', 'loading');
11162
11360
  sheetButtons();
11163
11361
  close.focus({ preventScroll: true });
11164
11362
  }
11165
- // The edit file is what the editor gets; when a browser save kept the layered
11166
- // project beside it, that goes along too and the editor restores the layers,
11167
- // falling back to the flattened PNG if the file cannot be read.
11363
+ // The edit files are what the editor gets; when a browser save kept the
11364
+ // layered project beside them, that goes along too and the editor restores
11365
+ // the layers, falling back to the flattened PNGs if the file cannot be read.
11366
+ // A frame set is sent as frames, one per member, and comes back the same way.
11168
11367
  async function sendOpen() {
11169
- const { item, source } = SHEET;
11368
+ const { item } = SHEET;
11369
+ const sources = editorSources(item);
11170
11370
  const style = snap.styles.find((s) => s.id === item.styleId && s.project === item.project);
11171
- const projectUrl = source === item.edit && item.editMeta && item.editMeta.projectUrl && SHEET.ready.version >= 2 ? item.editMeta.projectUrl : null;
11172
- let png, pxo = null;
11371
+ const projectUrl = sources.fromEdit && item.editMeta && item.editMeta.projectUrl && SHEET.ready.version >= 2 ? item.editMeta.projectUrl : null;
11372
+ const set = sources.files.length > 1;
11373
+ if (set && SHEET.ready.version < 3) {
11374
+ SHEET.msg.textContent = 'This editor build cannot open sets; reinstall it with pixelkiln tools install editor.';
11375
+ sheetStatus('bad', 'unsupported');
11376
+ return;
11377
+ }
11378
+ const pngs = [];
11379
+ let pxo = null;
11173
11380
  try {
11174
- const res = await fetch(source.url, { cache: 'no-store' });
11175
- if (!res.ok) throw new Error(await res.text());
11176
- png = await res.arrayBuffer();
11381
+ for (const file of sources.files) {
11382
+ const res = await fetch(file.url, { cache: 'no-store' });
11383
+ if (!res.ok) throw new Error(await res.text());
11384
+ pngs.push(await res.arrayBuffer());
11385
+ }
11177
11386
  if (projectUrl) {
11178
11387
  const p = await fetch(projectUrl, { cache: 'no-store' });
11179
11388
  if (p.ok) pxo = await p.arrayBuffer();
@@ -11185,12 +11394,22 @@ async function sendOpen() {
11185
11394
  if (!SHEET) return;
11186
11395
  const request = 'open-' + (++SHEET.requests);
11187
11396
  SHEET.sentProject = !!pxo;
11397
+ SHEET.roles = set ? item.outputs.map((o) => o.role || null) : null;
11188
11398
  const message = {
11189
11399
  type: 'pixelkiln:open', request,
11190
11400
  asset: { key: item.styleId + '/' + item.assetId, id: item.id, name: item.assetId, width: item.width, height: item.height },
11191
- png, palette: style ? style.palette : [],
11401
+ png: pngs[0], palette: style ? style.palette : [],
11192
11402
  };
11193
- const transfer = [png];
11403
+ const transfer = [pngs[0]];
11404
+ if (set) {
11405
+ message.frames = pngs.map((png, i) => ({ role: SHEET.roles[i], png }));
11406
+ // A tile set is not an animation; a slow rate keeps its timeline still-ish.
11407
+ message.fps = isFrameSet(item) ? (item.fps || 12) : 1;
11408
+ for (const png of pngs.slice(1)) transfer.push(png);
11409
+ // The first frame is both png (older editors) and frames[0]; copy it so both transfer.
11410
+ message.png = pngs[0].slice(0);
11411
+ transfer.push(message.png);
11412
+ }
11194
11413
  if (pxo) { message.pxo = pxo; transfer.push(pxo); }
11195
11414
  SHEET.frame.contentWindow.postMessage(message, location.origin, transfer);
11196
11415
  sheetStatus('cool', 'opening');
@@ -11218,8 +11437,9 @@ async function onEditorMessage(m) {
11218
11437
  case 'pixelkiln:opened':
11219
11438
  SHEET.opened = true;
11220
11439
  SHEET.loading.remove();
11221
- if (m.source === 'pxo') sheetStatus('ok', 'editing the edit file with its ' + (m.layers === 1 ? 'layer' : m.layers + ' layers') + ' restored');
11222
- else if (SHEET.sentProject) sheetStatus('warn', 'editing the flattened edit file; its layer file could not be opened');
11440
+ 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');
11441
+ else if (SHEET.sentProject) sheetStatus('warn', 'editing the flattened edit file' + (m.frames > 1 ? 's' : '') + '; the layer file could not be opened');
11442
+ 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)));
11223
11443
  else sheetStatus('ok', SHEET.item.edit && SHEET.item.edit.exists ? 'editing the edit file' : 'editing a copy of the generated art');
11224
11444
  sheetButtons();
11225
11445
  break;
@@ -11232,14 +11452,21 @@ async function onEditorMessage(m) {
11232
11452
  const pending = SHEET.pending;
11233
11453
  if (!pending || pending.request !== m.request) return;
11234
11454
  try {
11235
- const body = { png: await toBase64(m.png), editor: SHEET.ready.editor, protocol: SHEET.ready.version };
11455
+ const body = { editor: SHEET.ready.editor, protocol: SHEET.ready.version };
11456
+ if (SHEET.roles) {
11457
+ if (!Array.isArray(m.frames)) throw new Error('the editor returned no frames');
11458
+ body.frames = [];
11459
+ for (const f of m.frames) body.frames.push({ role: f.role === undefined ? null : f.role, png: await toBase64(f.png) });
11460
+ } else {
11461
+ body.png = await toBase64(m.png);
11462
+ }
11236
11463
  if (m.pxo && m.pxo.byteLength) body.pxo = await toBase64(m.pxo);
11237
11464
  await postHandEdit(SHEET.item, 'save-edit', body);
11238
11465
  const saved = snap.items.find((i) => i.id === SHEET.item.id);
11239
11466
  if (saved) SHEET.item = saved;
11240
11467
  SHEET.dirty = false;
11241
11468
  SHEET.pending = null;
11242
- ui.notice = { id: SHEET.item.id, text: 'Saved ' + (saved && saved.edit ? saved.edit.path : 'the edit') + '. mount and pack place it in place of the generated art.' };
11469
+ 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.' };
11243
11470
  render();
11244
11471
  if (pending.close) { closeEditorSheet(true); return; }
11245
11472
  sheetStatus('ok', 'saved');
@@ -11387,7 +11614,7 @@ function clearFilters() {
11387
11614
  function thumb(item) {
11388
11615
  const cell = el('div', 'cell');
11389
11616
  // A hand edit is what ships, so it is what the card shows.
11390
- const shown = item.edit && item.edit.url && item.editStatus === 'edited' ? [item.edit] : item.outputs.filter((o) => o.url);
11617
+ const shown = item.edit && item.editStatus === 'edited' && item.edits.every((e) => e.url) ? item.edits : item.outputs.filter((o) => o.url);
11391
11618
  if (!shown.length) {
11392
11619
  cell.classList.add('none');
11393
11620
  cell.append(el('span', null, item.state === 'missing' ? 'not generated yet'
@@ -11801,7 +12028,7 @@ function openCompare() {
11801
12028
  // ---- hand edits -------------------------------------------------------------
11802
12029
 
11803
12030
  const EDIT_STATUS_TEXT = {
11804
- same: 'A copy of the generated art, not changed yet. Open it in your editor and save.',
12031
+ same: 'The generated art\u2019s pixels, not changed yet. Open it in your editor and save.',
11805
12032
  edited: 'Differs from the generated art. mount and pack place this file; the generated file stays as the record.',
11806
12033
  'regenerated-since': 'The generated art changed after this edit was saved \u2014 the edit is based on an older generation.',
11807
12034
  missing: 'The declared file is not on disk.',
@@ -11812,12 +12039,18 @@ async function postHandEdit(item, action, extra) {
11812
12039
  if (item.project) body.project = item.project;
11813
12040
  snap = await postEdit(body);
11814
12041
  }
12042
+ // A single PNG or a set of them \u2014 frames, tiles \u2014 is hand-editable, one file
12043
+ // per member; a set with a GIF in it is not.
12044
+ const editableOutputs = (item) => item.outputs.length && item.outputs.every((o) => o.exists && o.mediaType === 'image/png');
12045
+ // What the members of a set are called: frames for an animation, tiles for a tile set.
12046
+ const memberNoun = (item, n) => isFrameSet(item) ? (n === 1 ? 'frame' : 'frames')
12047
+ : item.outputs.every((o) => o.role && /^tile/i.test(o.role)) ? (n === 1 ? 'tile' : 'tiles') : (n === 1 ? 'member' : 'members');
11815
12048
  function handEditSection(item) {
11816
- const canStart = EDITABLE && item.declared && item.asset && projectOf(item)?.manifestSha256 &&
11817
- item.outputs.length === 1 && item.outputs[0].exists && item.outputs[0].mediaType === 'image/png';
12049
+ const canStart = EDITABLE && item.declared && item.asset && projectOf(item)?.manifestSha256 && editableOutputs(item);
11818
12050
  if (!item.edit && !canStart) return null;
12051
+ const set = item.edits.length > 1;
11819
12052
  const s = el('section', 'meta');
11820
- s.append(el('h3', null, 'Hand edit'));
12053
+ s.append(el('h3', null, set ? 'Hand edit (' + item.edits.length + ' ' + memberNoun(item, item.edits.length) + ')' : 'Hand edit'));
11821
12054
  if (item.edit) {
11822
12055
  const pair = el('div', 'pair');
11823
12056
  const shared = displayScale(item.width, item.height, 200, 160);
@@ -11831,24 +12064,36 @@ function handEditSection(item) {
11831
12064
  f.append(el('figcaption', null, label));
11832
12065
  return f;
11833
12066
  };
11834
- pair.append(fig(item.outputs[0], 'generated'), fig(item.edit, 'edit'));
12067
+ // For a set, the member the preview is on; the strip there steps through them.
12068
+ const at = set ? Math.min(ui.member, item.edits.length - 1) : 0;
12069
+ const which = set ? ' \xB7 ' + (item.outputs[at].role || memberNoun(item, 1) + ' ' + (at + 1)) : '';
12070
+ pair.append(fig(item.outputs[at], 'generated' + which), fig(item.edits[at], 'edit' + which));
11835
12071
  s.append(pair);
11836
12072
  const dl = el('dl');
11837
12073
  const tone = item.editStatus === 'edited' ? 'ok' : item.editStatus === 'same' ? 'dim' : 'warn';
11838
12074
  const st = el('span', 'state-' + tone); st.append(el('i', 'dot ' + tone), document.createTextNode(' ' + item.editStatus));
11839
12075
  row(dl, 'status', st);
11840
12076
  row(dl, 'why', EDIT_STATUS_TEXT[item.editStatus] || '');
11841
- row(dl, 'file', item.edit.path, { mono: true, copy: item.edit.absolutePath });
11842
- if (item.edit.sha256) row(dl, 'sha256', item.edit.sha256.slice(0, 16) + '\u2026', { mono: true, copy: item.edit.sha256 });
11843
- row(dl, 'saved', fmtWhen(item.edit.modifiedAt));
12077
+ if (set) {
12078
+ 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') });
12079
+ const differing = item.editChanged.filter(Boolean).length;
12080
+ row(dl, 'changed', differing + ' of ' + item.edits.length + ' ' + memberNoun(item, item.edits.length) + ' differ from the generated art' +
12081
+ (differing ? ': ' + item.edits.filter((e, i) => item.editChanged[i]).map((e) => item.outputs[item.edits.indexOf(e)].role || e.path.split('/').pop()).join(', ') : ''));
12082
+ } else {
12083
+ row(dl, 'file', item.edit.path, { mono: true, copy: item.edit.absolutePath });
12084
+ if (item.edit.sha256) row(dl, 'sha256', item.edit.sha256.slice(0, 16) + '\u2026', { mono: true, copy: item.edit.sha256 });
12085
+ }
12086
+ row(dl, 'saved', fmtWhen(item.edits.reduce((latest, e) => e.modifiedAt && (!latest || e.modifiedAt > latest) ? e.modifiedAt : latest, null)));
11844
12087
  if (item.editMeta) {
11845
- row(dl, 'editor', item.editMeta.editor + ', ' + fmtWhen(item.editMeta.savedAt) + (item.editMeta.changedSince ? ' \u2014 the file changed since' : ''));
11846
- if (item.editMeta.basedOn) row(dl, 'based on', item.editMeta.basedOn.slice(0, 16) + '\u2026', { mono: true, copy: item.editMeta.basedOn });
12088
+ row(dl, 'editor', item.editMeta.editor + ', ' + fmtWhen(item.editMeta.savedAt) + (item.editMeta.changedSince ? ' \u2014 a file changed since' : ''));
12089
+ if (item.editMeta.basedOn && !set) row(dl, 'based on', item.editMeta.basedOn.slice(0, 16) + '\u2026', { mono: true, copy: item.editMeta.basedOn });
11847
12090
  if (item.editMeta.project) row(dl, 'layers', item.editMeta.project, { mono: true });
11848
12091
  }
11849
12092
  s.append(dl);
11850
12093
  } else {
11851
- s.append(el('div', 'state-dim', '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.'));
12094
+ s.append(el('div', 'state-dim', item.outputs.length > 1
12095
+ ? '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.'
12096
+ : '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.'));
11852
12097
  }
11853
12098
  if (EDITABLE && item.declared && projectOf(item)?.manifestSha256) {
11854
12099
  const acts = el('div', 'hand-actions');
@@ -11876,10 +12121,11 @@ function handEditSection(item) {
11876
12121
  }
11877
12122
  if (item.edit) {
11878
12123
  go('Open in desktop editor', 'start-edit', { open: true }, 'Opened ' + item.edit.path + ' in your editor. Save there, then Refresh.');
11879
- go('Detach edit', 'detach-edit', {}, 'Detached. The file is still at ' + item.edit.path + '; the generated art is placed again.');
12124
+ go('Detach edit', 'detach-edit', {}, 'Detached. The file' + (set ? 's are' : ' is') + ' still at ' + item.source + '; the generated art is placed again.');
11880
12125
  } else if (canStart) {
11881
- go('Edit by hand', 'start-edit', { open: true }, 'Created the edit file and opened it in your editor. Save there, then Refresh to see it here.');
11882
- go('Create edit file only', 'start-edit', { open: false }, 'Created the edit file and declared it in the manifest.');
12126
+ const many = item.outputs.length > 1;
12127
+ 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.');
12128
+ 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.');
11883
12129
  }
11884
12130
  acts.append(msg);
11885
12131
  s.append(acts);
@@ -12632,11 +12878,305 @@ if (ui.compare.length >= 2 && !ui.open) openCompare();
12632
12878
  }
12633
12879
  var escapeHtml2 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
12634
12880
 
12881
+ // tools/pixelorama-bridge/pin.json
12882
+ var pin_default = {
12883
+ pixelorama: "v1.2.2",
12884
+ godot: "4.7.2",
12885
+ extensionsApi: 9,
12886
+ protocol: 3,
12887
+ release: "editor-pixelorama-v1.2.2-pk.14",
12888
+ files: {
12889
+ "index.apple-touch-icon.png": {
12890
+ sha256: "ee5434873e0fed91e9a888c3ea5a8b79beac25999308be7a6eb41bf4ce640d27",
12891
+ bytes: 1036
12892
+ },
12893
+ "index.audio.position.worklet.js": {
12894
+ sha256: "be33985bc7160d6bf9646f259cd86b259cd67b02ccb297ee5c44f8ac84327bc8",
12895
+ bytes: 2973
12896
+ },
12897
+ "index.audio.worklet.js": {
12898
+ sha256: "5b476a9c9ce642c0ee4256436d1bc31d9c38f868aca0f9a8e2a57c18d2dec2a3",
12899
+ bytes: 7298
12900
+ },
12901
+ "index.html": {
12902
+ sha256: "2974146c89f4df9f796e840947c334aaf3e4fa3dd18bcd7e08db2eabfd0f086e",
12903
+ bytes: 5442
12904
+ },
12905
+ "index.icon.png": {
12906
+ sha256: "42a327d23178ddc26cc97e3560ebca6bc7c575d2a1b12a4837f2462049ca5745",
12907
+ bytes: 1070
12908
+ },
12909
+ "index.js": {
12910
+ sha256: "33c94cb3175f3333b82e2a3be5e8e86f77986f0aa2042b1631f6367a4e5bb6ba",
12911
+ bytes: 279815
12912
+ },
12913
+ "index.pck": {
12914
+ sha256: "736bd383c82fdadf31cc894caf08490b99c5df572aa20db683972c35c7a0465b",
12915
+ bytes: 6429628
12916
+ },
12917
+ "index.png": {
12918
+ sha256: "6e3ca3eb4a99e4c07bdd89ebc873b440ec0ffd66ce1955160da9e7ed80d538d7",
12919
+ bytes: 13558
12920
+ },
12921
+ "index.wasm": {
12922
+ sha256: "fc74679e3b97f76878947fcd4fbe1268cbfa6188182a2e33bbc3f5dc9bfa57d0",
12923
+ bytes: 39514754
12924
+ }
12925
+ }
12926
+ };
12927
+
12928
+ // src/editor/pin.ts
12929
+ var EDITOR_PIN = pin_default;
12930
+ var EDITOR_RELEASE_BASE = "https://github.com/gfargo/pixelkiln/releases/download";
12931
+
12932
+ // src/editor/install.ts
12933
+ var import_node_crypto7 = require("crypto");
12934
+ var import_node_fs23 = require("fs");
12935
+ var import_promises21 = require("fs/promises");
12936
+ var import_node_os3 = __toESM(require("os"), 1);
12937
+ var import_node_path26 = __toESM(require("path"), 1);
12938
+ var EditorInstallError = class extends Error {
12939
+ constructor(message6, file) {
12940
+ super(message6);
12941
+ this.file = file;
12942
+ this.name = "EditorInstallError";
12943
+ }
12944
+ file;
12945
+ };
12946
+ function editorToolsRoot() {
12947
+ const override = process.env.PIXELKILN_TOOLS_DIR?.trim();
12948
+ if (override) return import_node_path26.default.resolve(override);
12949
+ if (process.platform === "darwin") return import_node_path26.default.join(import_node_os3.default.homedir(), "Library", "Caches", "pixelkiln", "tools");
12950
+ if (process.platform === "win32") {
12951
+ return import_node_path26.default.join(process.env.LOCALAPPDATA ?? import_node_path26.default.join(import_node_os3.default.homedir(), "AppData", "Local"), "pixelkiln", "tools");
12952
+ }
12953
+ return import_node_path26.default.join(process.env.XDG_CACHE_HOME ?? import_node_path26.default.join(import_node_os3.default.homedir(), ".cache"), "pixelkiln", "tools");
12954
+ }
12955
+ function editorDir(pin = EDITOR_PIN, root = editorToolsRoot()) {
12956
+ return import_node_path26.default.join(root, "pixelorama", pin.release ?? "unpublished");
12957
+ }
12958
+ function editorBaseUrl(pin = EDITOR_PIN) {
12959
+ const override = process.env.PIXELKILN_EDITOR_URL?.trim();
12960
+ if (override) return override.replace(/\/+$/, "");
12961
+ if (!pin.release) return null;
12962
+ return `${EDITOR_RELEASE_BASE}/${encodeURIComponent(pin.release)}`;
12963
+ }
12964
+ async function sha256Of(file) {
12965
+ try {
12966
+ return (0, import_node_crypto7.createHash)("sha256").update(await (0, import_promises21.readFile)(file)).digest("hex");
12967
+ } catch {
12968
+ return null;
12969
+ }
12970
+ }
12971
+ async function editorStatus(opts = {}) {
12972
+ const pin = opts.pin ?? EDITOR_PIN;
12973
+ const dir = opts.dir ?? editorDir(pin);
12974
+ const missing = [];
12975
+ let installedBytes = 0;
12976
+ let totalBytes = 0;
12977
+ for (const [name, expected] of Object.entries(pin.files)) {
12978
+ totalBytes += expected.bytes;
12979
+ const file = import_node_path26.default.join(dir, name);
12980
+ const info = (0, import_node_fs23.existsSync)(file) ? await (0, import_promises21.stat)(file).catch(() => null) : null;
12981
+ if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
12982
+ installedBytes += expected.bytes;
12983
+ } else {
12984
+ missing.push(name);
12985
+ }
12986
+ }
12987
+ const pinned = Object.keys(pin.files).length > 0 && pin.release !== null;
12988
+ return {
12989
+ release: pin.release,
12990
+ dir,
12991
+ installed: pinned && missing.length === 0,
12992
+ missing: pinned ? missing : Object.keys(pin.files),
12993
+ totalBytes,
12994
+ installedBytes
12995
+ };
12996
+ }
12997
+ async function installEditor(opts = {}) {
12998
+ const pin = opts.pin ?? EDITOR_PIN;
12999
+ const dir = opts.dir ?? editorDir(pin);
13000
+ const baseUrl = opts.baseUrl ?? editorBaseUrl(pin);
13001
+ const doFetch = opts.fetch ?? fetch;
13002
+ const before = await editorStatus({ pin, dir });
13003
+ if (!pin.release || !Object.keys(pin.files).length) {
13004
+ throw new EditorInstallError(
13005
+ "this PixelKiln version pins no published editor build; upgrade, or set PIXELKILN_EDITOR_URL to a build you trust"
13006
+ );
13007
+ }
13008
+ if (!baseUrl) throw new EditorInstallError("no download location for the editor build");
13009
+ await (0, import_promises21.mkdir)(dir, { recursive: true });
13010
+ const downloaded = [];
13011
+ const skipped = Object.keys(pin.files).filter((name) => !before.missing.includes(name));
13012
+ let fetchedBytes = before.installedBytes;
13013
+ const names = before.missing;
13014
+ for (const [index, name] of names.entries()) {
13015
+ const expected = pin.files[name];
13016
+ const report = (phase, received) => opts.onProgress?.({
13017
+ file: name,
13018
+ index,
13019
+ count: names.length,
13020
+ bytes: expected.bytes,
13021
+ totalBytes: before.totalBytes,
13022
+ fetchedBytes: fetchedBytes + received,
13023
+ phase
13024
+ });
13025
+ report("start", 0);
13026
+ const url = `${baseUrl}/${encodeURIComponent(name)}`;
13027
+ const response = await doFetch(url, { signal: opts.signal, redirect: "follow" });
13028
+ if (!response.ok) throw new EditorInstallError(`${name}: ${response.status} ${response.statusText} from ${url}`, name);
13029
+ const bytes = await readBody(response, expected.bytes, (received) => report("progress", received));
13030
+ if (bytes.length !== expected.bytes) {
13031
+ throw new EditorInstallError(`${name}: expected ${expected.bytes} bytes, got ${bytes.length}`, name);
13032
+ }
13033
+ const digest2 = (0, import_node_crypto7.createHash)("sha256").update(bytes).digest("hex");
13034
+ if (digest2 !== expected.sha256) {
13035
+ throw new EditorInstallError(`${name}: sha256 ${digest2.slice(0, 12)}\u2026 does not match the pinned ${expected.sha256.slice(0, 12)}\u2026`, name);
13036
+ }
13037
+ const final = import_node_path26.default.join(dir, name);
13038
+ const tmp = `${final}.${process.pid}.part`;
13039
+ try {
13040
+ await (0, import_promises21.writeFile)(tmp, bytes);
13041
+ await (0, import_promises21.rename)(tmp, final);
13042
+ } finally {
13043
+ await (0, import_promises21.rm)(tmp, { force: true });
13044
+ }
13045
+ fetchedBytes += bytes.length;
13046
+ downloaded.push(name);
13047
+ report("done", 0);
13048
+ }
13049
+ const after = await editorStatus({ pin, dir });
13050
+ if (!after.installed) {
13051
+ throw new EditorInstallError(`editor install is incomplete: ${after.missing.join(", ")}`);
13052
+ }
13053
+ return { ...after, downloaded, skipped };
13054
+ }
13055
+ async function readBody(response, expectedBytes, onBytes) {
13056
+ if (!response.body) return Buffer.from(await response.arrayBuffer());
13057
+ const chunks = [];
13058
+ let received = 0;
13059
+ const reader = response.body.getReader();
13060
+ for (; ; ) {
13061
+ const { done, value } = await reader.read();
13062
+ if (done) break;
13063
+ chunks.push(value);
13064
+ received += value.length;
13065
+ if (received > expectedBytes) {
13066
+ await reader.cancel().catch(() => {
13067
+ });
13068
+ break;
13069
+ }
13070
+ onBytes(received);
13071
+ }
13072
+ return Buffer.concat(chunks);
13073
+ }
13074
+ var EDITOR_CONTENT_TYPES = {
13075
+ ".html": "text/html; charset=utf-8",
13076
+ ".js": "text/javascript; charset=utf-8",
13077
+ ".wasm": "application/wasm",
13078
+ ".pck": "application/octet-stream",
13079
+ ".png": "image/png",
13080
+ ".json": "application/json; charset=utf-8"
13081
+ };
13082
+ function editorContentType(name) {
13083
+ return EDITOR_CONTENT_TYPES[import_node_path26.default.extname(name).toLowerCase()] ?? null;
13084
+ }
13085
+
13086
+ // src/gallery/editor.ts
13087
+ var import_promises22 = require("fs/promises");
13088
+ var import_node_path27 = __toESM(require("path"), 1);
13089
+ function editorRoute(release, name = "index.html") {
13090
+ return `/editor/${encodeURIComponent(release)}/${encodeURIComponent(name)}`;
13091
+ }
13092
+ function createGalleryEditorHandlers(opts = {}) {
13093
+ const pin = opts.pin ?? EDITOR_PIN;
13094
+ const dir = opts.dir ?? editorDir(pin);
13095
+ const log = opts.onProgress ?? (() => {
13096
+ });
13097
+ let verified = null;
13098
+ let checking = null;
13099
+ let installing = null;
13100
+ let running = null;
13101
+ let error = null;
13102
+ const check = () => {
13103
+ if (!checking) {
13104
+ checking = editorStatus({ pin, dir }).then((status) => {
13105
+ verified = status;
13106
+ return status;
13107
+ }).finally(() => {
13108
+ checking = null;
13109
+ });
13110
+ }
13111
+ return checking;
13112
+ };
13113
+ const describe = (status) => ({
13114
+ ...status,
13115
+ pixelorama: pin.pixelorama,
13116
+ protocol: pin.protocol,
13117
+ url: status.installed && pin.release ? editorRoute(pin.release) : null,
13118
+ installing,
13119
+ error
13120
+ });
13121
+ return {
13122
+ release: pin.release,
13123
+ status: async () => describe(verified ?? await check()),
13124
+ install: async () => {
13125
+ if (!running) {
13126
+ error = null;
13127
+ installing = { file: "", index: 0, count: 0, bytes: 0, totalBytes: 0, fetchedBytes: 0, phase: "start" };
13128
+ log(` installing the editor (Pixelorama ${pin.pixelorama}) into ${dir}`);
13129
+ running = installEditor({
13130
+ pin,
13131
+ dir,
13132
+ baseUrl: opts.baseUrl ?? editorBaseUrl(pin) ?? void 0,
13133
+ ...opts.fetch ? { fetch: opts.fetch } : {},
13134
+ onProgress: (progress) => {
13135
+ installing = progress;
13136
+ }
13137
+ }).then((result) => {
13138
+ verified = result;
13139
+ const mb = (result.totalBytes / 1e6).toFixed(1);
13140
+ log(` editor ready: ${result.downloaded.length} file(s) fetched, ${result.skipped.length} already present (${mb} MB)`);
13141
+ }).catch((err) => {
13142
+ error = err instanceof Error ? err.message : String(err);
13143
+ verified = null;
13144
+ log(` editor install failed: ${error}`);
13145
+ }).finally(() => {
13146
+ installing = null;
13147
+ running = null;
13148
+ });
13149
+ }
13150
+ return describe(verified ?? await check());
13151
+ },
13152
+ file: async (release, name) => {
13153
+ if (!pin.release || release !== pin.release) return null;
13154
+ const expected = pin.files[name];
13155
+ const contentType = editorContentType(name);
13156
+ if (!expected || !contentType) return null;
13157
+ const status = verified ?? await check();
13158
+ if (!status.installed) return null;
13159
+ let bytes;
13160
+ try {
13161
+ bytes = await (0, import_promises22.readFile)(import_node_path27.default.join(dir, name));
13162
+ } catch {
13163
+ verified = null;
13164
+ return null;
13165
+ }
13166
+ if (bytes.length !== expected.bytes) {
13167
+ verified = null;
13168
+ return null;
13169
+ }
13170
+ return { bytes, contentType };
13171
+ }
13172
+ };
13173
+ }
13174
+
12635
13175
  // src/gallery/server.ts
12636
13176
  var import_node_child_process4 = require("child_process");
12637
- var import_node_crypto7 = require("crypto");
13177
+ var import_node_crypto8 = require("crypto");
12638
13178
  var import_node_http2 = require("http");
12639
- var import_promises21 = require("fs/promises");
13179
+ var import_promises23 = require("fs/promises");
12640
13180
  var MAX_EDIT_BYTES = 48 * 1024 * 1024;
12641
13181
  var EDITOR_CSP = [
12642
13182
  "default-src 'self'",
@@ -12664,7 +13204,7 @@ async function readJsonBody(req) {
12664
13204
  async function serveGallery(opts) {
12665
13205
  const log = opts.onProgress ?? (() => {
12666
13206
  });
12667
- const session = opts.edit || opts.generate || opts.editor ? (0, import_node_crypto7.randomBytes)(16).toString("hex") : null;
13207
+ const session = opts.edit || opts.generate || opts.editor ? (0, import_node_crypto8.randomBytes)(16).toString("hex") : null;
12668
13208
  let media = /* @__PURE__ */ new Map();
12669
13209
  let loading = null;
12670
13210
  const reviewAssets = /* @__PURE__ */ new Map();
@@ -12795,7 +13335,7 @@ async function serveGallery(opts) {
12795
13335
  const asset = reviewAssets.get(jobId)?.get(url2.pathname);
12796
13336
  if (!asset) return fail(404, "no such review asset");
12797
13337
  try {
12798
- const bytes = await (0, import_promises21.readFile)(asset.path);
13338
+ const bytes = await (0, import_promises23.readFile)(asset.path);
12799
13339
  res.writeHead(200, {
12800
13340
  "Content-Type": asset.contentType,
12801
13341
  "Cache-Control": "no-store",
@@ -12871,7 +13411,7 @@ async function serveGallery(opts) {
12871
13411
  if (!asset) return fail(404, "no such gallery media");
12872
13412
  let bytes;
12873
13413
  try {
12874
- bytes = await (0, import_promises21.readFile)(asset.path);
13414
+ bytes = await (0, import_promises23.readFile)(asset.path);
12875
13415
  } catch {
12876
13416
  return fail(404, "gallery media is no longer on disk; refresh the page");
12877
13417
  }
@@ -12916,7 +13456,7 @@ async function serveGallery(opts) {
12916
13456
  }
12917
13457
 
12918
13458
  // src/gallery/generate.ts
12919
- var import_node_crypto8 = require("crypto");
13459
+ var import_node_crypto9 = require("crypto");
12920
13460
  var import_zod10 = require("zod");
12921
13461
  var GenerateRequestSchema = import_zod10.z.object({
12922
13462
  project: import_zod10.z.string().min(1).optional(),
@@ -13116,7 +13656,7 @@ function createGenerateHandlers(opts) {
13116
13656
  }
13117
13657
  }
13118
13658
  const job = {
13119
- id: (0, import_node_crypto8.randomBytes)(8).toString("hex"),
13659
+ id: (0, import_node_crypto9.randomBytes)(8).toString("hex"),
13120
13660
  project: request.project ?? null,
13121
13661
  keys: request.keys,
13122
13662
  mode: request.refresh ? "refresh" : request.resume ? "resume" : "generate",
@@ -13181,19 +13721,19 @@ function createGenerateHandlers(opts) {
13181
13721
  }
13182
13722
 
13183
13723
  // src/pipeline/salvage.ts
13184
- var import_promises22 = require("fs/promises");
13185
- var import_node_fs22 = require("fs");
13186
- var import_node_path26 = __toESM(require("path"), 1);
13724
+ var import_promises24 = require("fs/promises");
13725
+ var import_node_fs24 = require("fs");
13726
+ var import_node_path28 = __toESM(require("path"), 1);
13187
13727
  function providerClaimId(provider, objectId) {
13188
13728
  return `${encodeURIComponent(provider)}:${encodeURIComponent(objectId)}`;
13189
13729
  }
13190
13730
  async function loadClaims(lockPaths, opts = {}) {
13191
13731
  const claimed = /* @__PURE__ */ new Set();
13192
13732
  for (const p of lockPaths) {
13193
- if (!(0, import_node_fs22.existsSync)(p)) throw new Error(`Claim lockfile not found: ${p}`);
13733
+ if (!(0, import_node_fs24.existsSync)(p)) throw new Error(`Claim lockfile not found: ${p}`);
13194
13734
  let parsed;
13195
13735
  try {
13196
- parsed = parseLock(JSON.parse(await (0, import_promises22.readFile)(p, "utf8")));
13736
+ parsed = parseLock(JSON.parse(await (0, import_promises24.readFile)(p, "utf8")));
13197
13737
  } catch {
13198
13738
  throw new Error(`Claim lockfile is malformed: ${p}`);
13199
13739
  }
@@ -13246,20 +13786,20 @@ function matchOrphanStyle(prompt, manifest) {
13246
13786
  return matchStyleByPattern(prompt, manifest);
13247
13787
  }
13248
13788
  async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, claimPaths) {
13249
- const own = import_node_path26.default.resolve(ownManifestPath);
13789
+ const own = import_node_path28.default.resolve(ownManifestPath);
13250
13790
  const siblingManifestPaths = [
13251
13791
  .../* @__PURE__ */ new Set([
13252
13792
  ...workspaceManifestPaths,
13253
- ...claimPaths.map((c) => import_node_path26.default.join(import_node_path26.default.dirname(import_node_path26.default.resolve(c)), "pixelkiln.manifest.json"))
13793
+ ...claimPaths.map((c) => import_node_path28.default.join(import_node_path28.default.dirname(import_node_path28.default.resolve(c)), "pixelkiln.manifest.json"))
13254
13794
  ])
13255
13795
  ];
13256
13796
  const siblings = [];
13257
13797
  for (const siblingManifestPath of siblingManifestPaths) {
13258
- if (import_node_path26.default.resolve(siblingManifestPath) === own) continue;
13259
- if (!(0, import_node_fs22.existsSync)(siblingManifestPath)) continue;
13798
+ if (import_node_path28.default.resolve(siblingManifestPath) === own) continue;
13799
+ if (!(0, import_node_fs24.existsSync)(siblingManifestPath)) continue;
13260
13800
  try {
13261
13801
  const { manifest } = await loadManifest(siblingManifestPath);
13262
- siblings.push({ label: import_node_path26.default.basename(import_node_path26.default.dirname(siblingManifestPath)), manifest });
13802
+ siblings.push({ label: import_node_path28.default.basename(import_node_path28.default.dirname(siblingManifestPath)), manifest });
13263
13803
  } catch {
13264
13804
  }
13265
13805
  }
@@ -13375,8 +13915,8 @@ async function applyTags(provider, decisions, existing, opts = {}) {
13375
13915
  }
13376
13916
 
13377
13917
  // src/pick/salvage-server.ts
13378
- var import_promises23 = require("fs/promises");
13379
- var import_node_path27 = __toESM(require("path"), 1);
13918
+ var import_promises25 = require("fs/promises");
13919
+ var import_node_path29 = __toESM(require("path"), 1);
13380
13920
 
13381
13921
  // src/pick/salvage-sheet.ts
13382
13922
  var escapeHtml3 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
@@ -13541,7 +14081,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
13541
14081
  });
13542
14082
  const html = renderSalvageSheet(orphans, {
13543
14083
  styleId: ctx.styleId,
13544
- importDir: import_node_path27.default.relative(process.cwd(), ctx.importDir) || "."
14084
+ importDir: import_node_path29.default.relative(process.cwd(), ctx.importDir) || "."
13545
14085
  });
13546
14086
  const byId = new Map(orphans.map((o) => [o.id, o]));
13547
14087
  const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
@@ -13570,10 +14110,10 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
13570
14110
  if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
13571
14111
  decodePng(buf);
13572
14112
  const assetId = idFromPrompt(orphan.prompt, taken);
13573
- const rel = import_node_path27.default.join("_salvaged", `${assetId}.png`);
13574
- const outFile = import_node_path27.default.resolve(ctx.importDir, rel);
13575
- await (0, import_promises23.mkdir)(import_node_path27.default.dirname(outFile), { recursive: true });
13576
- await (0, import_promises23.writeFile)(outFile, buf);
14113
+ const rel = import_node_path29.default.join("_salvaged", `${assetId}.png`);
14114
+ const outFile = import_node_path29.default.resolve(ctx.importDir, rel);
14115
+ await (0, import_promises25.mkdir)(import_node_path29.default.dirname(outFile), { recursive: true });
14116
+ await (0, import_promises25.writeFile)(outFile, buf);
13577
14117
  ctx.manifest.assets[assetId] = {
13578
14118
  prompt: orphan.prompt,
13579
14119
  promptByStyle: {},
@@ -13605,7 +14145,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
13605
14145
  sourceUrl: durableSource,
13606
14146
  sourceUrls: durableSource ? [{ url: durableSource }] : [],
13607
14147
  outputs: [{
13608
- path: portableOutputPath(outFile, import_node_path27.default.dirname(ctx.manifestPath)),
14148
+ path: portableOutputPath(outFile, import_node_path29.default.dirname(ctx.manifestPath)),
13609
14149
  sha256: sha256(buf)
13610
14150
  }],
13611
14151
  submittedAt: orphan.createdAt,
@@ -13629,9 +14169,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
13629
14169
  }
13630
14170
  }
13631
14171
  await applyTags(provider, decisions, existingTags, { onProgress: log });
13632
- const raw = JSON.parse(await (0, import_promises23.readFile)(ctx.manifestPath, "utf8"));
14172
+ const raw = JSON.parse(await (0, import_promises25.readFile)(ctx.manifestPath, "utf8"));
13633
14173
  for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
13634
- await (0, import_promises23.writeFile)(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
14174
+ await (0, import_promises25.writeFile)(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
13635
14175
  await saveLock(ctx.lockPath, ctx.lock);
13636
14176
  return result;
13637
14177
  }
@@ -13746,17 +14286,23 @@ async function workspaceStatus(ws, dir) {
13746
14286
  ComfyUIProvider,
13747
14287
  DEFAULT_QUALITY_TOLERANCES,
13748
14288
  DEFAULT_RATE_LIMIT,
14289
+ EDITOR_PIN,
14290
+ EDITOR_RELEASE_BASE,
14291
+ EditorInstallError,
13749
14292
  FAKE_PNG,
13750
14293
  FakeProvider,
13751
14294
  GenerateRequestError,
13752
14295
  GenerateRequestSchema,
13753
14296
  GeneratorSchema,
13754
14297
  GridConfidenceSchema,
14298
+ HAND_EDIT_DIR,
14299
+ HandEditCompanionSchema,
13755
14300
  ImageQualityMetricsSchema,
13756
14301
  InheritedStyleSchema,
13757
14302
  LockEntrySchema,
13758
14303
  LockSchema,
13759
14304
  MAX_DOWNLOAD_BYTES,
14305
+ MAX_HAND_EDIT_BYTES,
13760
14306
  MAX_RETRIES,
13761
14307
  ManifestDriftError,
13762
14308
  ManifestEditError,
@@ -13803,12 +14349,19 @@ async function workspaceStatus(ws, dir) {
13803
14349
  countNumberedDescriptions,
13804
14350
  createArtifactBundleManifest,
13805
14351
  createGalleryEditHandler,
14352
+ createGalleryEditorHandlers,
13806
14353
  createGenerateHandlers,
13807
14354
  createProvider,
13808
14355
  currentEntryOutputPath,
13809
14356
  currentOutputPath,
14357
+ detachHandEdit,
13810
14358
  detectMediaType,
13811
14359
  doctor,
14360
+ editorBaseUrl,
14361
+ editorDir,
14362
+ editorRoute,
14363
+ editorStatus,
14364
+ editorToolsRoot,
13812
14365
  evaluateAudit,
13813
14366
  expectedOutputPath,
13814
14367
  exportTileset,
@@ -13820,13 +14373,22 @@ async function workspaceStatus(ws, dir) {
13820
14373
  galleryMediaRoute,
13821
14374
  generationCost,
13822
14375
  groupOrphansByStyle,
14376
+ handEditBase,
14377
+ handEditBases,
14378
+ handEditCompanionPath,
14379
+ handEditMembers,
14380
+ handEditPath,
14381
+ handEditProjectPath,
13823
14382
  hex,
13824
14383
  idFromPrompt,
13825
14384
  imageMetadata,
13826
14385
  inspectCaches,
13827
14386
  inspectQualityProfile,
13828
14387
  inspectRevisionReadiness,
14388
+ installEditor,
13829
14389
  installRecipe,
14390
+ isFrameSetEntry,
14391
+ isMemberSetEntry,
13830
14392
  isSensitiveSourceUrl,
13831
14393
  listBundledRecipes,
13832
14394
  loadClaims,
@@ -13841,12 +14403,14 @@ async function workspaceStatus(ws, dir) {
13841
14403
  measureImageQuality,
13842
14404
  mediaExtension,
13843
14405
  mediaTypeFromExtension,
14406
+ memberPath,
13844
14407
  mergePalettes,
13845
14408
  mountSprites,
13846
14409
  mountStyle,
13847
14410
  normalizeLockOutputPaths,
13848
14411
  normalizeRefinementPalette,
13849
14412
  normalizeTileRules,
14413
+ openInEditor,
13850
14414
  outliers,
13851
14415
  outputId,
13852
14416
  packSprites,
@@ -13866,6 +14430,7 @@ async function workspaceStatus(ws, dir) {
13866
14430
  qualityRecordPath,
13867
14431
  quantizeToPalette,
13868
14432
  readArtifactBundleManifest,
14433
+ readHandEditCompanion,
13869
14434
  readQualityBaseline,
13870
14435
  recipeDigest,
13871
14436
  refineAsset,
@@ -13898,6 +14463,7 @@ async function workspaceStatus(ws, dir) {
13898
14463
  retryAfterMs,
13899
14464
  runPicker,
13900
14465
  runSalvage,
14466
+ saveHandEdit,
13901
14467
  saveLock,
13902
14468
  saveWorkspace,
13903
14469
  scanAssets,
@@ -13909,8 +14475,11 @@ async function workspaceStatus(ws, dir) {
13909
14475
  shouldRetry,
13910
14476
  slugify,
13911
14477
  snapshotQualityBaseline,
14478
+ sourceIsStem,
14479
+ sourceOutputPath,
13912
14480
  specHash,
13913
14481
  spendByUnit,
14482
+ startHandEdit,
13914
14483
  styleImagesBase64,
13915
14484
  submit,
13916
14485
  summarize,