pixelkiln 0.69.0 → 0.70.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
@@ -2055,9 +2055,9 @@ var PixelLabClient = class {
2055
2055
  apiKey;
2056
2056
  http;
2057
2057
  /** Retries transport failures, 429, and 5xx; see http.ts for the policy. */
2058
- async request(path34, init) {
2058
+ async request(path35, init) {
2059
2059
  const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
2060
- const res = await this.http(`${BASE}${path34}`, {
2060
+ const res = await this.http(`${BASE}${path35}`, {
2061
2061
  ...init,
2062
2062
  headers: {
2063
2063
  Authorization: auth,
@@ -2067,13 +2067,13 @@ var PixelLabClient = class {
2067
2067
  });
2068
2068
  const text = await res.text();
2069
2069
  if (!res.ok) {
2070
- throw new PixelLabError(`${init?.method ?? "GET"} ${path34} \u2192 ${res.status}`, res.status, text);
2070
+ throw new PixelLabError(`${init?.method ?? "GET"} ${path35} \u2192 ${res.status}`, res.status, text);
2071
2071
  }
2072
2072
  if (!text) return {};
2073
2073
  try {
2074
2074
  return JSON.parse(text);
2075
2075
  } catch {
2076
- throw new Error(`${init?.method ?? "GET"} ${path34} returned invalid JSON`);
2076
+ throw new Error(`${init?.method ?? "GET"} ${path35} returned invalid JSON`);
2077
2077
  }
2078
2078
  }
2079
2079
  async balance() {
@@ -2465,10 +2465,10 @@ var PixelLabClient = class {
2465
2465
  const palette = args.paletteSwatchBase64 ? { color_image: { type: "base64", base64: args.paletteSwatchBase64, format: "png" }, force_colors: true } : {};
2466
2466
  const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
2467
2467
  const south = args.reference?.south;
2468
- let path34;
2468
+ let path35;
2469
2469
  let body;
2470
2470
  if (args.mode === "v3") {
2471
- path34 = "/create-character-v3";
2471
+ path35 = "/create-character-v3";
2472
2472
  body = {
2473
2473
  description: args.description,
2474
2474
  image_size: imageSize,
@@ -2482,7 +2482,7 @@ var PixelLabClient = class {
2482
2482
  ...args.enhancePrompt ? { enhance_prompt: true } : {}
2483
2483
  };
2484
2484
  } else if (args.mode === "pro-flash") {
2485
- path34 = "/create-character-pro-flash";
2485
+ path35 = "/create-character-pro-flash";
2486
2486
  const traits = args.styleTraits;
2487
2487
  body = {
2488
2488
  description: args.description,
@@ -2506,7 +2506,7 @@ var PixelLabClient = class {
2506
2506
  } : {}
2507
2507
  };
2508
2508
  } else if (args.mode === "pro") {
2509
- path34 = "/create-character-pro";
2509
+ path35 = "/create-character-pro";
2510
2510
  body = {
2511
2511
  description: args.description,
2512
2512
  image_size: imageSize,
@@ -2520,7 +2520,7 @@ var PixelLabClient = class {
2520
2520
  ...!south && args.styleCharacterId ? { style_character_id: args.styleCharacterId } : {}
2521
2521
  };
2522
2522
  } else {
2523
- path34 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
2523
+ path35 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
2524
2524
  body = {
2525
2525
  description: args.description,
2526
2526
  image_size: imageSize,
@@ -2537,8 +2537,8 @@ var PixelLabClient = class {
2537
2537
  ...palette
2538
2538
  };
2539
2539
  }
2540
- const raw = await this.request(path34, { method: "POST", body: JSON.stringify(body) });
2541
- return validateResponse(CharacterSubmitSchema, raw, path34.slice(1));
2540
+ const raw = await this.request(path35, { method: "POST", body: JSON.stringify(body) });
2541
+ return validateResponse(CharacterSubmitSchema, raw, path35.slice(1));
2542
2542
  }
2543
2543
  /** A text edit of an existing character, applied to every direction and stored as a sibling. */
2544
2544
  async createCharacterState(args) {
@@ -3716,10 +3716,10 @@ var import_node_fs = require("fs");
3716
3716
  function sha256(data) {
3717
3717
  return (0, import_node_crypto.createHash)("sha256").update(data).digest("hex");
3718
3718
  }
3719
- async function sha256File(path34) {
3719
+ async function sha256File(path35) {
3720
3720
  return new Promise((resolve, reject) => {
3721
3721
  const hash = (0, import_node_crypto.createHash)("sha256");
3722
- const stream = (0, import_node_fs.createReadStream)(path34);
3722
+ const stream = (0, import_node_fs.createReadStream)(path35);
3723
3723
  stream.on("data", (chunk2) => hash.update(chunk2));
3724
3724
  stream.on("error", reject);
3725
3725
  stream.on("end", () => resolve(hash.digest("hex")));
@@ -6082,9 +6082,9 @@ var RetroDiffusionClient = class {
6082
6082
  if (!response.ok) throw new ProviderError("retrodiffusion", `Retro Diffusion download failed (${response.status})`, { status: response.status });
6083
6083
  return Buffer.from(await response.arrayBuffer());
6084
6084
  }
6085
- async call(path34, init = {}) {
6085
+ async call(path35, init = {}) {
6086
6086
  if (!this.token) throw new Error("RD_API_KEY is not set");
6087
- const response = await this.request(`${this.baseUrl}${path34}`, {
6087
+ const response = await this.request(`${this.baseUrl}${path35}`, {
6088
6088
  ...init,
6089
6089
  headers: {
6090
6090
  "Content-Type": "application/json",
@@ -12373,10 +12373,10 @@ function isSha256Hash(value) {
12373
12373
  function parseCache(value) {
12374
12374
  return HashCacheSchema.parse(value);
12375
12375
  }
12376
- async function loadCache(path34) {
12377
- if (!(0, import_node_fs14.existsSync)(path34)) return { version: 1, hashes: {} };
12376
+ async function loadCache(path35) {
12377
+ if (!(0, import_node_fs14.existsSync)(path35)) return { version: 1, hashes: {} };
12378
12378
  try {
12379
- const parsed = HashCacheSchema.safeParse(JSON.parse(await (0, import_promises11.readFile)(path34, "utf8")));
12379
+ const parsed = HashCacheSchema.safeParse(JSON.parse(await (0, import_promises11.readFile)(path35, "utf8")));
12380
12380
  if (!parsed.success) return { version: 1, hashes: {} };
12381
12381
  return {
12382
12382
  version: 1,
@@ -12388,18 +12388,18 @@ async function loadCache(path34) {
12388
12388
  return { version: 1, hashes: {} };
12389
12389
  }
12390
12390
  }
12391
- async function saveCache(path34, cache) {
12391
+ async function saveCache(path35, cache) {
12392
12392
  const sorted = {};
12393
12393
  for (const key of Object.keys(cache.hashes).sort()) {
12394
12394
  const hash = cache.hashes[key];
12395
12395
  if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
12396
12396
  sorted[key] = hash;
12397
12397
  }
12398
- await (0, import_promises11.mkdir)(import_node_path18.default.dirname(import_node_path18.default.resolve(path34)), { recursive: true });
12399
- const tmp = `${path34}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
12398
+ await (0, import_promises11.mkdir)(import_node_path18.default.dirname(import_node_path18.default.resolve(path35)), { recursive: true });
12399
+ const tmp = `${path35}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
12400
12400
  try {
12401
12401
  await (0, import_promises11.writeFile)(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
12402
- await (0, import_promises11.rename)(tmp, path34);
12402
+ await (0, import_promises11.rename)(tmp, path35);
12403
12403
  } finally {
12404
12404
  await (0, import_promises11.rm)(tmp, { force: true });
12405
12405
  }
@@ -15204,9 +15204,9 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
15204
15204
 
15205
15205
  // src/gallery/snapshot.ts
15206
15206
  var import_node_crypto6 = require("crypto");
15207
- var import_node_fs25 = require("fs");
15208
- var import_promises22 = require("fs/promises");
15209
- var import_node_path30 = __toESM(require("path"), 1);
15207
+ var import_node_fs26 = require("fs");
15208
+ var import_promises23 = require("fs/promises");
15209
+ var import_node_path31 = __toESM(require("path"), 1);
15210
15210
 
15211
15211
  // src/workspace.ts
15212
15212
  var import_promises19 = require("fs/promises");
@@ -15342,8 +15342,8 @@ function validateWorkspace(ws, dir) {
15342
15342
  }
15343
15343
 
15344
15344
  // src/gallery/edit.ts
15345
- var import_node_path29 = __toESM(require("path"), 1);
15346
- var import_zod10 = require("zod");
15345
+ var import_node_path30 = __toESM(require("path"), 1);
15346
+ var import_zod11 = require("zod");
15347
15347
 
15348
15348
  // src/manifest-edit.ts
15349
15349
  var import_promises20 = require("fs/promises");
@@ -15391,15 +15391,27 @@ var NewAssetSchema = import_zod8.z.object({
15391
15391
  /**
15392
15392
  * A controlled regeneration of another asset in the same manifest,
15393
15393
  * instead of a fresh generation. `inpaint` needs a mask upload the
15394
- * gallery does not offer yet, so only `image-to-image` can be created
15395
- * here; hand-edit the manifest for the other modes.
15394
+ * gallery does not offer yet, so only `image-to-image` and
15395
+ * `animate-skeleton` (whose keypoints file the gallery writes first) can
15396
+ * be created here; hand-edit the manifest for the other modes.
15396
15397
  */
15397
- revision: import_zod8.z.object({
15398
- mode: import_zod8.z.literal("image-to-image"),
15399
- /** Asset id of the parent this revises; must already exist. */
15400
- from: import_zod8.z.string().min(1),
15401
- strength: import_zod8.z.number().min(0).max(1).optional()
15402
- }).strict().optional(),
15398
+ revision: import_zod8.z.discriminatedUnion("mode", [
15399
+ import_zod8.z.object({
15400
+ mode: import_zod8.z.literal("image-to-image"),
15401
+ /** Asset id of the parent this revises; must already exist. */
15402
+ from: import_zod8.z.string().min(1),
15403
+ strength: import_zod8.z.number().min(0).max(1).optional()
15404
+ }).strict(),
15405
+ import_zod8.z.object({
15406
+ mode: import_zod8.z.literal("animate-skeleton"),
15407
+ from: import_zod8.z.string().min(1),
15408
+ /** Manifest-relative; checked when the manifest is loaded, like every revision input. */
15409
+ keypointsFile: import_zod8.z.string().min(1),
15410
+ direction: CharacterDirectionSchema,
15411
+ description: import_zod8.z.string().min(1).optional(),
15412
+ skeletonTemplate: import_zod8.z.string().min(1).optional()
15413
+ }).strict()
15414
+ ]).optional(),
15403
15415
  /** A pose or outfit of an existing `character`/`objectPro` base or state. */
15404
15416
  state: NewStateSchema.optional(),
15405
15417
  /** A loop of an existing `character`/`objectPro` base or state. */
@@ -15903,41 +15915,118 @@ function openInEditor(file, editor = process.env.PIXELKILN_EDITOR) {
15903
15915
  return openExternal(file, command);
15904
15916
  }
15905
15917
 
15918
+ // src/gallery/skeleton.ts
15919
+ var import_node_fs25 = require("fs");
15920
+ var import_promises22 = require("fs/promises");
15921
+ var import_node_path29 = __toESM(require("path"), 1);
15922
+ var import_zod10 = require("zod");
15923
+ var SkeletonEstimateRequestSchema = import_zod10.z.object({
15924
+ project: import_zod10.z.string().min(1).optional(),
15925
+ styleId: import_zod10.z.string().min(1),
15926
+ assetId: import_zod10.z.string().min(1),
15927
+ frames: import_zod10.z.number().int().min(3).max(15).optional()
15928
+ }).strict();
15929
+ var SkeletonAnimationRequestSchema = import_zod10.z.object({
15930
+ action: import_zod10.z.literal("create-skeleton-animation"),
15931
+ project: import_zod10.z.string().min(1).optional(),
15932
+ expectedSha256: import_zod10.z.string().regex(/^[0-9a-f]{64}$/),
15933
+ styleId: import_zod10.z.string().min(1),
15934
+ /** The sprite the animation poses. */
15935
+ from: import_zod10.z.string().min(1),
15936
+ assetId: import_zod10.z.string().min(1).regex(/^[^/\\]+$/, "asset ids cannot contain slashes"),
15937
+ /** The motion, sent as PixelLab's `action`. */
15938
+ prompt: import_zod10.z.string().min(1),
15939
+ direction: CharacterDirectionSchema,
15940
+ description: import_zod10.z.string().min(1).optional(),
15941
+ skeletonTemplate: import_zod10.z.string().min(1).optional(),
15942
+ /** Manifest-relative JSON path inside the project. */
15943
+ keypointsFile: import_zod10.z.string().min(1),
15944
+ /** Poses to write to `keypointsFile`; omitted to use a file that already exists. */
15945
+ set: SkeletonSetSchema.optional(),
15946
+ /** Replace an existing `keypointsFile` with `set`. */
15947
+ overwrite: import_zod10.z.boolean().optional()
15948
+ }).strict();
15949
+ async function createSkeletonAnimation(manifestPath, request) {
15950
+ const root = import_node_path29.default.dirname(import_node_path29.default.resolve(manifestPath));
15951
+ const file = import_node_path29.default.resolve(root, request.keypointsFile);
15952
+ const relative = import_node_path29.default.relative(root, file);
15953
+ if (!request.keypointsFile.endsWith(".json") || relative.startsWith("..") || import_node_path29.default.isAbsolute(relative)) {
15954
+ throw new ManifestEditError("the keypoints file must be a .json path inside the project");
15955
+ }
15956
+ const existed = (0, import_node_fs25.existsSync)(file);
15957
+ let previous = null;
15958
+ if (request.set) {
15959
+ if (existed && !request.overwrite) {
15960
+ throw new ManifestEditError(`${request.keypointsFile} already exists; choose another path, or replace it`);
15961
+ }
15962
+ if (existed) previous = await (0, import_promises22.readFile)(file);
15963
+ await (0, import_promises22.mkdir)(import_node_path29.default.dirname(file), { recursive: true });
15964
+ await (0, import_promises22.writeFile)(file, JSON.stringify(request.set, null, 2) + "\n");
15965
+ } else if (!existed) {
15966
+ throw new ManifestEditError(`${request.keypointsFile} does not exist; paste poses or estimate them from the sprite`);
15967
+ }
15968
+ try {
15969
+ await applyManifestEdit(manifestPath, {
15970
+ action: "add-asset",
15971
+ assetId: request.assetId,
15972
+ expectedSha256: request.expectedSha256,
15973
+ asset: {
15974
+ prompt: request.prompt,
15975
+ styles: [request.styleId],
15976
+ revision: {
15977
+ mode: "animate-skeleton",
15978
+ from: request.from,
15979
+ keypointsFile: relative.split(import_node_path29.default.sep).join("/"),
15980
+ direction: request.direction,
15981
+ ...request.description ? { description: request.description } : {},
15982
+ ...request.skeletonTemplate ? { skeletonTemplate: request.skeletonTemplate } : {}
15983
+ }
15984
+ }
15985
+ });
15986
+ } catch (error) {
15987
+ if (request.set) {
15988
+ if (previous) await (0, import_promises22.writeFile)(file, previous);
15989
+ else await (0, import_promises22.rm)(file, { force: true });
15990
+ }
15991
+ throw error;
15992
+ }
15993
+ }
15994
+
15906
15995
  // src/gallery/edit.ts
15907
- var Base64 = import_zod10.z.string().regex(/^[A-Za-z0-9+/]*={0,2}$/).max(Math.ceil(MAX_HAND_EDIT_BYTES / 3) * 4);
15908
- var HandEditRequestSchema = import_zod10.z.discriminatedUnion("action", [
15909
- import_zod10.z.object({
15910
- action: import_zod10.z.literal("start-edit"),
15911
- project: import_zod10.z.string().min(1).optional(),
15912
- assetId: import_zod10.z.string().min(1),
15913
- styleId: import_zod10.z.string().min(1),
15914
- expectedSha256: import_zod10.z.string().regex(/^[0-9a-f]{64}$/),
15996
+ var Base64 = import_zod11.z.string().regex(/^[A-Za-z0-9+/]*={0,2}$/).max(Math.ceil(MAX_HAND_EDIT_BYTES / 3) * 4);
15997
+ var HandEditRequestSchema = import_zod11.z.discriminatedUnion("action", [
15998
+ import_zod11.z.object({
15999
+ action: import_zod11.z.literal("start-edit"),
16000
+ project: import_zod11.z.string().min(1).optional(),
16001
+ assetId: import_zod11.z.string().min(1),
16002
+ styleId: import_zod11.z.string().min(1),
16003
+ expectedSha256: import_zod11.z.string().regex(/^[0-9a-f]{64}$/),
15915
16004
  /** Also launch the author's editor on the file. */
15916
- open: import_zod10.z.boolean().optional()
16005
+ open: import_zod11.z.boolean().optional()
15917
16006
  }).strict(),
15918
- import_zod10.z.object({
15919
- action: import_zod10.z.literal("detach-edit"),
15920
- project: import_zod10.z.string().min(1).optional(),
15921
- assetId: import_zod10.z.string().min(1),
15922
- styleId: import_zod10.z.string().min(1),
15923
- expectedSha256: import_zod10.z.string().regex(/^[0-9a-f]{64}$/)
16007
+ import_zod11.z.object({
16008
+ action: import_zod11.z.literal("detach-edit"),
16009
+ project: import_zod11.z.string().min(1).optional(),
16010
+ assetId: import_zod11.z.string().min(1),
16011
+ styleId: import_zod11.z.string().min(1),
16012
+ expectedSha256: import_zod11.z.string().regex(/^[0-9a-f]{64}$/)
15924
16013
  }).strict(),
15925
16014
  /** From the in-browser editor: the page holds the bytes, the server validates and writes. */
15926
- import_zod10.z.object({
15927
- action: import_zod10.z.literal("save-edit"),
15928
- project: import_zod10.z.string().min(1).optional(),
15929
- assetId: import_zod10.z.string().min(1),
15930
- styleId: import_zod10.z.string().min(1),
15931
- expectedSha256: import_zod10.z.string().regex(/^[0-9a-f]{64}$/),
16015
+ import_zod11.z.object({
16016
+ action: import_zod11.z.literal("save-edit"),
16017
+ project: import_zod11.z.string().min(1).optional(),
16018
+ assetId: import_zod11.z.string().min(1),
16019
+ styleId: import_zod11.z.string().min(1),
16020
+ expectedSha256: import_zod11.z.string().regex(/^[0-9a-f]{64}$/),
15932
16021
  /** The flattened image; for a frame set, `frames` instead. */
15933
16022
  png: Base64.min(1).optional(),
15934
16023
  /** Every frame of a set, tagged with the role it was opened under (null for one added in the editor). */
15935
- frames: import_zod10.z.array(import_zod10.z.object({ role: import_zod10.z.string().min(1).nullable(), png: Base64.min(1) }).strict()).min(1).max(256).optional(),
16024
+ frames: import_zod11.z.array(import_zod11.z.object({ role: import_zod11.z.string().min(1).nullable(), png: Base64.min(1) }).strict()).min(1).max(256).optional(),
15936
16025
  /** The editor's layered project file, kept beside the edit. */
15937
16026
  pxo: Base64.optional(),
15938
16027
  /** Editor identity from the bridge's ready message. */
15939
- editor: import_zod10.z.string().min(1).max(200),
15940
- protocol: import_zod10.z.number().int().positive()
16028
+ editor: import_zod11.z.string().min(1).max(200),
16029
+ protocol: import_zod11.z.number().int().positive()
15941
16030
  }).strict()
15942
16031
  ]);
15943
16032
  function createGalleryEditHandler(opts) {
@@ -15978,6 +16067,23 @@ function createGalleryEditHandler(opts) {
15978
16067
  }
15979
16068
  return opts.reload();
15980
16069
  }
16070
+ const skeleton = SkeletonAnimationRequestSchema.safeParse(body);
16071
+ if (skeleton.success) {
16072
+ let manifestPath2;
16073
+ try {
16074
+ manifestPath2 = await opts.manifestFor(skeleton.data.project);
16075
+ } catch (error) {
16076
+ throw new ManifestEditError(error instanceof Error ? error.message : String(error));
16077
+ }
16078
+ await createSkeletonAnimation(manifestPath2, skeleton.data);
16079
+ log(` manifest edited: added skeleton animation ${skeleton.data.assetId} (${skeleton.data.keypointsFile})`);
16080
+ return opts.reload();
16081
+ }
16082
+ if (body?.action === "create-skeleton-animation") {
16083
+ throw new ManifestEditError(
16084
+ "invalid skeleton animation: " + skeleton.error.issues.slice(0, 3).map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")
16085
+ );
16086
+ }
15981
16087
  const parsed = ManifestEditSchema.safeParse(body);
15982
16088
  if (!parsed.success) {
15983
16089
  throw new ManifestEditError(
@@ -15992,7 +16098,7 @@ function createGalleryEditHandler(opts) {
15992
16098
  }
15993
16099
  const result = await applyManifestEdit(manifestPath, parsed.data);
15994
16100
  if (result.changed) {
15995
- const relative = import_node_path29.default.relative(process.cwd(), result.manifestPath);
16101
+ const relative = import_node_path30.default.relative(process.cwd(), result.manifestPath);
15996
16102
  const shown = !relative || relative.startsWith("..") ? result.manifestPath : relative;
15997
16103
  const subject = parsed.data.action === "patch-style" ? `style ${parsed.data.styleId}` : parsed.data.action === "set-source" || parsed.data.action === "clear-source" ? `${parsed.data.styleId}/${parsed.data.assetId} source` : parsed.data.assetId;
15998
16104
  log(` manifest edited: ${parsed.data.action === "add-asset" ? "added" : "changed"} ${subject} in ${shown}`);
@@ -16004,7 +16110,7 @@ function createGalleryEditHandler(opts) {
16004
16110
  // src/gallery/snapshot.ts
16005
16111
  var PROJECT_FILE_TYPE = "application/octet-stream";
16006
16112
  function galleryMediaId(absolutePath) {
16007
- return (0, import_node_crypto6.createHash)("sha256").update(import_node_path30.default.resolve(absolutePath)).digest("hex").slice(0, 24);
16113
+ return (0, import_node_crypto6.createHash)("sha256").update(import_node_path31.default.resolve(absolutePath)).digest("hex").slice(0, 24);
16008
16114
  }
16009
16115
  function galleryMediaRoute(id) {
16010
16116
  return `/media/${id}`;
@@ -16023,9 +16129,9 @@ function describeHistory(media, entry, cacheDir) {
16023
16129
  return (entry.history ?? []).map((generation, i) => {
16024
16130
  const outputs = generation.outputs.map((output) => {
16025
16131
  const mediaType = output.mediaType ?? MediaType.PNG;
16026
- const file = import_node_path30.default.join(cacheDir, cacheFileName(output.sha256, mediaType));
16132
+ const file = import_node_path31.default.join(cacheDir, cacheFileName(output.sha256, mediaType));
16027
16133
  let url = null;
16028
- if ((0, import_node_fs25.existsSync)(file)) {
16134
+ if ((0, import_node_fs26.existsSync)(file)) {
16029
16135
  const id = galleryMediaId(file);
16030
16136
  media.set(id, { path: file, contentType: mediaType });
16031
16137
  url = `${galleryMediaRoute(id)}?v=${output.sha256.slice(0, 24)}`;
@@ -16057,7 +16163,7 @@ function describeHistory(media, entry, cacheDir) {
16057
16163
  var MAX_COMPARED_BYTES = 8 * 1024 * 1024;
16058
16164
  async function samePixels(a, b) {
16059
16165
  try {
16060
- const [left, right] = await Promise.all([(0, import_promises22.readFile)(a), (0, import_promises22.readFile)(b)]);
16166
+ const [left, right] = await Promise.all([(0, import_promises23.readFile)(a), (0, import_promises23.readFile)(b)]);
16061
16167
  if (left.length > MAX_COMPARED_BYTES || right.length > MAX_COMPARED_BYTES) return false;
16062
16168
  const x = decodePng(left);
16063
16169
  const y = decodePng(right);
@@ -16123,7 +16229,7 @@ function describeCharacter(spec, entry) {
16123
16229
  }
16124
16230
  async function fileInfo(absolutePath) {
16125
16231
  try {
16126
- const info = await (0, import_promises22.stat)(absolutePath);
16232
+ const info = await (0, import_promises23.stat)(absolutePath);
16127
16233
  return info.isFile() ? { bytes: info.size, modifiedAt: info.mtime.toISOString() } : null;
16128
16234
  } catch {
16129
16235
  return null;
@@ -16155,10 +16261,10 @@ async function describeOutput(media, root, absolutePath, recorded = {}) {
16155
16261
  async function describeQuality(media, root, inspection) {
16156
16262
  const outputPaths = inspection.outputs ?? [inspection.output];
16157
16263
  const outputs = await Promise.all(
16158
- outputPaths.map((file) => describeOutput(media, root, import_node_path30.default.resolve(root, file)))
16264
+ outputPaths.map((file) => describeOutput(media, root, import_node_path31.default.resolve(root, file)))
16159
16265
  );
16160
- const recordPath = import_node_path30.default.resolve(root, inspection.record);
16161
- const recordExists = (0, import_node_fs25.existsSync)(recordPath);
16266
+ const recordPath = import_node_path31.default.resolve(root, inspection.record);
16267
+ const recordExists = (0, import_node_fs26.existsSync)(recordPath);
16162
16268
  let options = null;
16163
16269
  let check = null;
16164
16270
  if (recordExists) {
@@ -16177,7 +16283,7 @@ async function describeQuality(media, root, inspection) {
16177
16283
  return {
16178
16284
  state: inspection.state,
16179
16285
  reason: inspection.reason,
16180
- output: portableOutputPath(import_node_path30.default.resolve(root, inspection.output), root),
16286
+ output: portableOutputPath(import_node_path31.default.resolve(root, inspection.output), root),
16181
16287
  record: portableOutputPath(recordPath, root),
16182
16288
  recordExists,
16183
16289
  outputs,
@@ -16215,9 +16321,9 @@ async function buildGallerySnapshot(opts) {
16215
16321
  const media = /* @__PURE__ */ new Map();
16216
16322
  const plan = await buildPlan(specs, lock);
16217
16323
  const items = [];
16218
- const manifestText = await (0, import_promises22.readFile)(loaded.path, "utf8");
16324
+ const manifestText = await (0, import_promises23.readFile)(loaded.path, "utf8");
16219
16325
  const manifestSha256 = sha256(manifestText);
16220
- const cacheDir = import_node_path30.default.resolve(import_node_path30.default.dirname(opts.lockPath), ".pixelkiln", "cache");
16326
+ const cacheDir = import_node_path31.default.resolve(import_node_path31.default.dirname(opts.lockPath), ".pixelkiln", "cache");
16221
16327
  let rawStyles = {};
16222
16328
  try {
16223
16329
  const raw = JSON.parse(manifestText);
@@ -16241,12 +16347,12 @@ async function buildGallerySnapshot(opts) {
16241
16347
  )
16242
16348
  );
16243
16349
  if (spec.source) {
16244
- const editPath = import_node_path30.default.resolve(root, spec.source);
16350
+ const editPath = import_node_path31.default.resolve(root, spec.source);
16245
16351
  const memberCount = sourceIsStem(spec.source, entry, root) ? entry.outputs.length : 1;
16246
16352
  const editShas = await Promise.all(
16247
16353
  Array.from({ length: memberCount }, async (_, index) => {
16248
16354
  const file = sourceOutputPath(spec.source, entry, index, root);
16249
- return { file, sha256: (0, import_node_fs25.existsSync)(file) ? await sha256File(file) : null };
16355
+ return { file, sha256: (0, import_node_fs26.existsSync)(file) ? await sha256File(file) : null };
16250
16356
  })
16251
16357
  );
16252
16358
  edits = await Promise.all(editShas.map(({ file, sha256: editSha }) => describeOutput(media, root, file, { sha256: editSha })));
@@ -16254,7 +16360,7 @@ async function buildGallerySnapshot(opts) {
16254
16360
  const companion = await readHandEditCompanion(editPath);
16255
16361
  if (companion) {
16256
16362
  const projectPath = handEditProjectPath(editPath);
16257
- const project = companion.project && (0, import_node_fs25.existsSync)(projectPath) ? await fileInfo(projectPath) : null;
16363
+ const project = companion.project && (0, import_node_fs26.existsSync)(projectPath) ? await fileInfo(projectPath) : null;
16258
16364
  let projectUrl = null;
16259
16365
  if (project) {
16260
16366
  const id = galleryMediaId(projectPath);
@@ -16290,8 +16396,8 @@ async function buildGallerySnapshot(opts) {
16290
16396
  editStatus = edits.some((member) => !member.exists) ? "missing" : editChanged.every((changed) => !changed) ? "same" : regenerated ? "regenerated-since" : "edited";
16291
16397
  }
16292
16398
  } else if (spec.source) {
16293
- outputs = [await describeOutput(media, root, import_node_path30.default.resolve(root, spec.source))];
16294
- } else if (!entry && (0, import_node_fs25.existsSync)(spec.outFile)) {
16399
+ outputs = [await describeOutput(media, root, import_node_path31.default.resolve(root, spec.source))];
16400
+ } else if (!entry && (0, import_node_fs26.existsSync)(spec.outFile)) {
16295
16401
  outputs = [await describeOutput(media, root, spec.outFile)];
16296
16402
  } else {
16297
16403
  outputs = [];
@@ -16332,6 +16438,10 @@ async function buildGallerySnapshot(opts) {
16332
16438
  revision: entry?.revision ?? null,
16333
16439
  revisionParentKey: spec.revision ? lockKey(spec.styleId, spec.revision.sourceAssetId) : entry?.revision ? lockKey(spec.styleId, entry.revision.sourceAssetId) : null,
16334
16440
  mirrorOfKey: spec.mirror ? lockKey(spec.styleId, spec.mirror.sourceAssetId) : entry?.mirror ? lockKey(spec.styleId, entry.mirror.sourceAssetId) : null,
16441
+ skeleton: spec.revision?.mode === "animate-skeleton" && spec.revision.keypointsFile ? {
16442
+ keypointsFile: portableOutputPath(spec.revision.keypointsFile, root),
16443
+ set: spec.revision.skeleton ?? null
16444
+ } : null,
16335
16445
  character: describeCharacter(spec, entry),
16336
16446
  outputs,
16337
16447
  quality,
@@ -16396,6 +16506,7 @@ async function buildGallerySnapshot(opts) {
16396
16506
  revision: entry.revision,
16397
16507
  revisionParentKey: entry.revision ? lockKey(entry.styleId, entry.revision.sourceAssetId) : null,
16398
16508
  mirrorOfKey: entry.mirror ? lockKey(entry.styleId, entry.mirror.sourceAssetId) : null,
16509
+ skeleton: null,
16399
16510
  character: describeCharacter(void 0, entry),
16400
16511
  outputs,
16401
16512
  quality: null,
@@ -16486,7 +16597,7 @@ async function buildGallerySnapshot(opts) {
16486
16597
  id: loaded.manifest.name,
16487
16598
  name: loaded.manifest.name,
16488
16599
  manifest: loaded.path,
16489
- lock: import_node_path30.default.resolve(opts.lockPath),
16600
+ lock: import_node_path31.default.resolve(opts.lockPath),
16490
16601
  root,
16491
16602
  provider: loaded.manifest.provider,
16492
16603
  account: null,
@@ -16512,7 +16623,7 @@ async function buildGallerySnapshot(opts) {
16512
16623
  return { snapshot, media };
16513
16624
  }
16514
16625
  async function buildWorkspaceGallerySnapshot(opts) {
16515
- const dir = import_node_path30.default.dirname(import_node_path30.default.resolve(opts.workspacePath));
16626
+ const dir = import_node_path31.default.dirname(import_node_path31.default.resolve(opts.workspacePath));
16516
16627
  const media = /* @__PURE__ */ new Map();
16517
16628
  const projects = [];
16518
16629
  const items = [];
@@ -16555,11 +16666,11 @@ async function buildWorkspaceGallerySnapshot(opts) {
16555
16666
  id: project.id,
16556
16667
  name: loaded.manifest.name,
16557
16668
  manifest: manifestPath,
16558
- lock: import_node_path30.default.resolve(lockPath),
16669
+ lock: import_node_path31.default.resolve(lockPath),
16559
16670
  root: loaded.root,
16560
16671
  provider: loaded.manifest.provider,
16561
16672
  account: project.account ?? null,
16562
- manifestSha256: manifestSha256 ?? sha256(await (0, import_promises22.readFile)(manifestPath, "utf8")),
16673
+ manifestSha256: manifestSha256 ?? sha256(await (0, import_promises23.readFile)(manifestPath, "utf8")),
16563
16674
  historyLimit: historyLimit(loaded.manifest),
16564
16675
  entries: Object.keys(lock.entries).length,
16565
16676
  items: projectItems.length,
@@ -16571,8 +16682,8 @@ async function buildWorkspaceGallerySnapshot(opts) {
16571
16682
  id: project.id,
16572
16683
  name: project.id,
16573
16684
  manifest: manifestPath,
16574
- lock: import_node_path30.default.resolve(lockPath),
16575
- root: import_node_path30.default.dirname(manifestPath),
16685
+ lock: import_node_path31.default.resolve(lockPath),
16686
+ root: import_node_path31.default.dirname(manifestPath),
16576
16687
  provider: project.provider,
16577
16688
  account: project.account ?? null,
16578
16689
  manifestSha256: null,
@@ -16599,7 +16710,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
16599
16710
  version: 1,
16600
16711
  generatedAt: (opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
16601
16712
  project: null,
16602
- workspace: { path: import_node_path30.default.resolve(opts.workspacePath), dir, projects },
16713
+ workspace: { path: import_node_path31.default.resolve(opts.workspacePath), dir, projects },
16603
16714
  filter,
16604
16715
  totals: {
16605
16716
  entries: projects.reduce((sum, project) => sum + project.entries, 0),
@@ -16616,13 +16727,13 @@ async function buildWorkspaceGallerySnapshot(opts) {
16616
16727
  }
16617
16728
 
16618
16729
  // src/gallery/page.ts
16619
- var import_node_fs26 = require("fs");
16730
+ var import_node_fs27 = require("fs");
16620
16731
  var CLIENT_DIR = new URL("./client/", importMetaUrl);
16621
16732
  var client;
16622
16733
  function clientAssets() {
16623
16734
  client ??= {
16624
- css: (0, import_node_fs26.readFileSync)(new URL("gallery.css", CLIENT_DIR), "utf8"),
16625
- js: (0, import_node_fs26.readFileSync)(new URL("gallery.js", CLIENT_DIR), "utf8")
16735
+ css: (0, import_node_fs27.readFileSync)(new URL("gallery.css", CLIENT_DIR), "utf8"),
16736
+ js: (0, import_node_fs27.readFileSync)(new URL("gallery.js", CLIENT_DIR), "utf8")
16626
16737
  };
16627
16738
  return client;
16628
16739
  }
@@ -16748,10 +16859,10 @@ var EDITOR_RELEASE_BASE = "https://github.com/gfargo/pixelkiln/releases/download
16748
16859
 
16749
16860
  // src/editor/install.ts
16750
16861
  var import_node_crypto7 = require("crypto");
16751
- var import_node_fs27 = require("fs");
16752
- var import_promises23 = require("fs/promises");
16862
+ var import_node_fs28 = require("fs");
16863
+ var import_promises24 = require("fs/promises");
16753
16864
  var import_node_os3 = __toESM(require("os"), 1);
16754
- var import_node_path31 = __toESM(require("path"), 1);
16865
+ var import_node_path32 = __toESM(require("path"), 1);
16755
16866
  var EditorInstallError = class extends Error {
16756
16867
  constructor(message6, file) {
16757
16868
  super(message6);
@@ -16762,15 +16873,15 @@ var EditorInstallError = class extends Error {
16762
16873
  };
16763
16874
  function editorToolsRoot() {
16764
16875
  const override = process.env.PIXELKILN_TOOLS_DIR?.trim();
16765
- if (override) return import_node_path31.default.resolve(override);
16766
- if (process.platform === "darwin") return import_node_path31.default.join(import_node_os3.default.homedir(), "Library", "Caches", "pixelkiln", "tools");
16876
+ if (override) return import_node_path32.default.resolve(override);
16877
+ if (process.platform === "darwin") return import_node_path32.default.join(import_node_os3.default.homedir(), "Library", "Caches", "pixelkiln", "tools");
16767
16878
  if (process.platform === "win32") {
16768
- return import_node_path31.default.join(process.env.LOCALAPPDATA ?? import_node_path31.default.join(import_node_os3.default.homedir(), "AppData", "Local"), "pixelkiln", "tools");
16879
+ return import_node_path32.default.join(process.env.LOCALAPPDATA ?? import_node_path32.default.join(import_node_os3.default.homedir(), "AppData", "Local"), "pixelkiln", "tools");
16769
16880
  }
16770
- return import_node_path31.default.join(process.env.XDG_CACHE_HOME ?? import_node_path31.default.join(import_node_os3.default.homedir(), ".cache"), "pixelkiln", "tools");
16881
+ return import_node_path32.default.join(process.env.XDG_CACHE_HOME ?? import_node_path32.default.join(import_node_os3.default.homedir(), ".cache"), "pixelkiln", "tools");
16771
16882
  }
16772
16883
  function editorDir(pin = EDITOR_PIN, root = editorToolsRoot()) {
16773
- return import_node_path31.default.join(root, "pixelorama", pin.release ?? "unpublished");
16884
+ return import_node_path32.default.join(root, "pixelorama", pin.release ?? "unpublished");
16774
16885
  }
16775
16886
  function editorBaseUrl(pin = EDITOR_PIN) {
16776
16887
  const override = process.env.PIXELKILN_EDITOR_URL?.trim();
@@ -16780,7 +16891,7 @@ function editorBaseUrl(pin = EDITOR_PIN) {
16780
16891
  }
16781
16892
  async function sha256Of(file) {
16782
16893
  try {
16783
- return (0, import_node_crypto7.createHash)("sha256").update(await (0, import_promises23.readFile)(file)).digest("hex");
16894
+ return (0, import_node_crypto7.createHash)("sha256").update(await (0, import_promises24.readFile)(file)).digest("hex");
16784
16895
  } catch {
16785
16896
  return null;
16786
16897
  }
@@ -16793,8 +16904,8 @@ async function editorStatus(opts = {}) {
16793
16904
  let totalBytes = 0;
16794
16905
  for (const [name, expected] of Object.entries(pin.files)) {
16795
16906
  totalBytes += expected.bytes;
16796
- const file = import_node_path31.default.join(dir, name);
16797
- const info = (0, import_node_fs27.existsSync)(file) ? await (0, import_promises23.stat)(file).catch(() => null) : null;
16907
+ const file = import_node_path32.default.join(dir, name);
16908
+ const info = (0, import_node_fs28.existsSync)(file) ? await (0, import_promises24.stat)(file).catch(() => null) : null;
16798
16909
  if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
16799
16910
  installedBytes += expected.bytes;
16800
16911
  } else {
@@ -16823,7 +16934,7 @@ async function installEditor(opts = {}) {
16823
16934
  );
16824
16935
  }
16825
16936
  if (!baseUrl) throw new EditorInstallError("no download location for the editor build");
16826
- await (0, import_promises23.mkdir)(dir, { recursive: true });
16937
+ await (0, import_promises24.mkdir)(dir, { recursive: true });
16827
16938
  const downloaded = [];
16828
16939
  const skipped = Object.keys(pin.files).filter((name) => !before.missing.includes(name));
16829
16940
  let fetchedBytes = before.installedBytes;
@@ -16851,13 +16962,13 @@ async function installEditor(opts = {}) {
16851
16962
  if (digest2 !== expected.sha256) {
16852
16963
  throw new EditorInstallError(`${name}: sha256 ${digest2.slice(0, 12)}\u2026 does not match the pinned ${expected.sha256.slice(0, 12)}\u2026`, name);
16853
16964
  }
16854
- const final = import_node_path31.default.join(dir, name);
16965
+ const final = import_node_path32.default.join(dir, name);
16855
16966
  const tmp = `${final}.${process.pid}.part`;
16856
16967
  try {
16857
- await (0, import_promises23.writeFile)(tmp, bytes);
16858
- await (0, import_promises23.rename)(tmp, final);
16968
+ await (0, import_promises24.writeFile)(tmp, bytes);
16969
+ await (0, import_promises24.rename)(tmp, final);
16859
16970
  } finally {
16860
- await (0, import_promises23.rm)(tmp, { force: true });
16971
+ await (0, import_promises24.rm)(tmp, { force: true });
16861
16972
  }
16862
16973
  fetchedBytes += bytes.length;
16863
16974
  downloaded.push(name);
@@ -16897,12 +17008,12 @@ var EDITOR_CONTENT_TYPES = {
16897
17008
  ".json": "application/json; charset=utf-8"
16898
17009
  };
16899
17010
  function editorContentType(name) {
16900
- return EDITOR_CONTENT_TYPES[import_node_path31.default.extname(name).toLowerCase()] ?? null;
17011
+ return EDITOR_CONTENT_TYPES[import_node_path32.default.extname(name).toLowerCase()] ?? null;
16901
17012
  }
16902
17013
 
16903
17014
  // src/gallery/editor.ts
16904
- var import_promises24 = require("fs/promises");
16905
- var import_node_path32 = __toESM(require("path"), 1);
17015
+ var import_promises25 = require("fs/promises");
17016
+ var import_node_path33 = __toESM(require("path"), 1);
16906
17017
  function editorRoute(release, name = "index.html") {
16907
17018
  return `/editor/${encodeURIComponent(release)}/${encodeURIComponent(name)}`;
16908
17019
  }
@@ -16975,7 +17086,7 @@ function createGalleryEditorHandlers(opts = {}) {
16975
17086
  if (!status.installed) return null;
16976
17087
  let bytes;
16977
17088
  try {
16978
- bytes = await (0, import_promises24.readFile)(import_node_path32.default.join(dir, name));
17089
+ bytes = await (0, import_promises25.readFile)(import_node_path33.default.join(dir, name));
16979
17090
  } catch {
16980
17091
  verified = null;
16981
17092
  return null;
@@ -16992,7 +17103,7 @@ function createGalleryEditorHandlers(opts = {}) {
16992
17103
  // src/gallery/server.ts
16993
17104
  var import_node_crypto8 = require("crypto");
16994
17105
  var import_node_http2 = require("http");
16995
- var import_promises25 = require("fs/promises");
17106
+ var import_promises26 = require("fs/promises");
16996
17107
  var MAX_EDIT_BYTES = 48 * 1024 * 1024;
16997
17108
  var EDITOR_CSP = [
16998
17109
  "default-src 'self'",
@@ -17020,7 +17131,7 @@ async function readJsonBody(req) {
17020
17131
  async function serveGallery(opts) {
17021
17132
  const log = opts.onProgress ?? (() => {
17022
17133
  });
17023
- const session = opts.edit || opts.generate || opts.editor ? (0, import_node_crypto8.randomBytes)(16).toString("hex") : null;
17134
+ const session = opts.edit || opts.generate || opts.editor || opts.skeleton ? (0, import_node_crypto8.randomBytes)(16).toString("hex") : null;
17024
17135
  let media = /* @__PURE__ */ new Map();
17025
17136
  let loading = null;
17026
17137
  const reviewAssets = /* @__PURE__ */ new Map();
@@ -17095,6 +17206,17 @@ async function serveGallery(opts) {
17095
17206
  }
17096
17207
  return;
17097
17208
  }
17209
+ if (req.method === "POST" && url2.pathname === "/api/skeleton/estimate") {
17210
+ if (!opts.skeleton || !session) return fail(405, "skeleton estimates need the gallery started with --edit");
17211
+ try {
17212
+ const body = await guardedBody("skeleton estimates");
17213
+ if (body === void 0) return;
17214
+ json(200, await opts.skeleton.estimate(body));
17215
+ } catch (err) {
17216
+ reportError("skeleton estimate", err);
17217
+ }
17218
+ return;
17219
+ }
17098
17220
  if (req.method === "POST" && url2.pathname === "/api/editor/install") {
17099
17221
  if (!opts.editor || !session) return fail(405, "the in-browser editor is off for this gallery");
17100
17222
  try {
@@ -17151,7 +17273,7 @@ async function serveGallery(opts) {
17151
17273
  const asset = reviewAssets.get(jobId)?.get(url2.pathname);
17152
17274
  if (!asset) return fail(404, "no such review asset");
17153
17275
  try {
17154
- const bytes = await (0, import_promises25.readFile)(asset.path);
17276
+ const bytes = await (0, import_promises26.readFile)(asset.path);
17155
17277
  res.writeHead(200, {
17156
17278
  "Content-Type": asset.contentType,
17157
17279
  "Cache-Control": "no-store",
@@ -17227,7 +17349,7 @@ async function serveGallery(opts) {
17227
17349
  if (!asset) return fail(404, "no such gallery media");
17228
17350
  let bytes;
17229
17351
  try {
17230
- bytes = await (0, import_promises25.readFile)(asset.path);
17352
+ bytes = await (0, import_promises26.readFile)(asset.path);
17231
17353
  } catch {
17232
17354
  return fail(404, "gallery media is no longer on disk; refresh the page");
17233
17355
  }
@@ -17271,27 +17393,27 @@ async function serveGallery(opts) {
17271
17393
 
17272
17394
  // src/gallery/generate.ts
17273
17395
  var import_node_crypto9 = require("crypto");
17274
- var import_zod11 = require("zod");
17275
- var GenerateRequestSchema = import_zod11.z.object({
17276
- project: import_zod11.z.string().min(1).optional(),
17277
- keys: import_zod11.z.array(import_zod11.z.string().min(1)).min(1).max(500),
17396
+ var import_zod12 = require("zod");
17397
+ var GenerateRequestSchema = import_zod12.z.object({
17398
+ project: import_zod12.z.string().min(1).optional(),
17399
+ keys: import_zod12.z.array(import_zod12.z.string().min(1)).min(1).max(500),
17278
17400
  /**
17279
17401
  * Regenerate work that is up to date, as `gen --force` does, and let the
17280
17402
  * fetch replace a locally modified or untracked file. With `restore`, put
17281
17403
  * the recorded bytes back over a modified file.
17282
17404
  */
17283
- force: import_zod11.z.boolean().optional(),
17405
+ force: import_zod12.z.boolean().optional(),
17284
17406
  /** Advance in-flight, review, or selected work without submitting anything. */
17285
- resume: import_zod11.z.boolean().optional(),
17407
+ resume: import_zod12.z.boolean().optional(),
17286
17408
  /** Re-download downloaded outputs and replace files whose object changed upstream. */
17287
- refresh: import_zod11.z.boolean().optional(),
17409
+ refresh: import_zod12.z.boolean().optional(),
17288
17410
  /** Put recorded outputs back on disk from the cache or the provider, without generating. */
17289
- restore: import_zod11.z.boolean().optional(),
17411
+ restore: import_zod12.z.boolean().optional(),
17290
17412
  /**
17291
17413
  * Bring a replaced generation back: 1-based position in the record's
17292
17414
  * history (newest first) or an output hash prefix. One key at a time.
17293
17415
  */
17294
- revert: import_zod11.z.string().min(1).max(64).optional()
17416
+ revert: import_zod12.z.string().min(1).max(64).optional()
17295
17417
  }).strict().refine((request) => [request.resume, request.refresh, request.restore, request.revert !== void 0].filter(Boolean).length <= 1, {
17296
17418
  message: "resume, refresh, restore, and revert are exclusive"
17297
17419
  }).refine((request) => request.revert === void 0 || request.keys.length === 1, { message: "revert takes one key" });
@@ -17601,19 +17723,19 @@ function createGenerateHandlers(opts) {
17601
17723
  }
17602
17724
 
17603
17725
  // src/pipeline/salvage.ts
17604
- var import_promises26 = require("fs/promises");
17605
- var import_node_fs28 = require("fs");
17606
- var import_node_path33 = __toESM(require("path"), 1);
17726
+ var import_promises27 = require("fs/promises");
17727
+ var import_node_fs29 = require("fs");
17728
+ var import_node_path34 = __toESM(require("path"), 1);
17607
17729
  function providerClaimId(provider, objectId) {
17608
17730
  return `${encodeURIComponent(provider)}:${encodeURIComponent(objectId)}`;
17609
17731
  }
17610
17732
  async function loadClaims(lockPaths, opts = {}) {
17611
17733
  const claimed = /* @__PURE__ */ new Set();
17612
17734
  for (const p of lockPaths) {
17613
- if (!(0, import_node_fs28.existsSync)(p)) throw new Error(`Claim lockfile not found: ${p}`);
17735
+ if (!(0, import_node_fs29.existsSync)(p)) throw new Error(`Claim lockfile not found: ${p}`);
17614
17736
  let parsed;
17615
17737
  try {
17616
- parsed = parseLock(JSON.parse(await (0, import_promises26.readFile)(p, "utf8")));
17738
+ parsed = parseLock(JSON.parse(await (0, import_promises27.readFile)(p, "utf8")));
17617
17739
  } catch {
17618
17740
  throw new Error(`Claim lockfile is malformed: ${p}`);
17619
17741
  }
@@ -17717,20 +17839,20 @@ function matchOrphanStyle(prompt, manifest) {
17717
17839
  return matchStyleByPattern(prompt, manifest);
17718
17840
  }
17719
17841
  async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, claimPaths) {
17720
- const own = import_node_path33.default.resolve(ownManifestPath);
17842
+ const own = import_node_path34.default.resolve(ownManifestPath);
17721
17843
  const siblingManifestPaths = [
17722
17844
  .../* @__PURE__ */ new Set([
17723
17845
  ...workspaceManifestPaths,
17724
- ...claimPaths.map((c) => import_node_path33.default.join(import_node_path33.default.dirname(import_node_path33.default.resolve(c)), "pixelkiln.manifest.json"))
17846
+ ...claimPaths.map((c) => import_node_path34.default.join(import_node_path34.default.dirname(import_node_path34.default.resolve(c)), "pixelkiln.manifest.json"))
17725
17847
  ])
17726
17848
  ];
17727
17849
  const siblings = [];
17728
17850
  for (const siblingManifestPath of siblingManifestPaths) {
17729
- if (import_node_path33.default.resolve(siblingManifestPath) === own) continue;
17730
- if (!(0, import_node_fs28.existsSync)(siblingManifestPath)) continue;
17851
+ if (import_node_path34.default.resolve(siblingManifestPath) === own) continue;
17852
+ if (!(0, import_node_fs29.existsSync)(siblingManifestPath)) continue;
17731
17853
  try {
17732
17854
  const { manifest } = await loadManifest(siblingManifestPath);
17733
- siblings.push({ label: import_node_path33.default.basename(import_node_path33.default.dirname(siblingManifestPath)), manifest });
17855
+ siblings.push({ label: import_node_path34.default.basename(import_node_path34.default.dirname(siblingManifestPath)), manifest });
17734
17856
  } catch {
17735
17857
  }
17736
17858
  }
@@ -17850,8 +17972,8 @@ async function applyTags(provider, decisions, existing, opts = {}) {
17850
17972
  }
17851
17973
 
17852
17974
  // src/pick/salvage-server.ts
17853
- var import_promises27 = require("fs/promises");
17854
- var import_node_path34 = __toESM(require("path"), 1);
17975
+ var import_promises28 = require("fs/promises");
17976
+ var import_node_path35 = __toESM(require("path"), 1);
17855
17977
 
17856
17978
  // src/pick/salvage-sheet.ts
17857
17979
  var escapeHtml3 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
@@ -18017,7 +18139,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
18017
18139
  });
18018
18140
  const html = renderSalvageSheet(orphans, {
18019
18141
  styleId: ctx.styleId,
18020
- importDir: import_node_path34.default.relative(process.cwd(), ctx.importDir) || "."
18142
+ importDir: import_node_path35.default.relative(process.cwd(), ctx.importDir) || "."
18021
18143
  });
18022
18144
  const byId = new Map(orphans.map((o) => [o.id, o]));
18023
18145
  const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
@@ -18073,10 +18195,10 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
18073
18195
  if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
18074
18196
  decodePng(buf);
18075
18197
  const assetId = idFromPrompt(orphan.prompt, taken);
18076
- const rel = import_node_path34.default.join("_salvaged", `${assetId}.png`);
18077
- const outFile = import_node_path34.default.resolve(ctx.importDir, rel);
18078
- await (0, import_promises27.mkdir)(import_node_path34.default.dirname(outFile), { recursive: true });
18079
- await (0, import_promises27.writeFile)(outFile, buf);
18198
+ const rel = import_node_path35.default.join("_salvaged", `${assetId}.png`);
18199
+ const outFile = import_node_path35.default.resolve(ctx.importDir, rel);
18200
+ await (0, import_promises28.mkdir)(import_node_path35.default.dirname(outFile), { recursive: true });
18201
+ await (0, import_promises28.writeFile)(outFile, buf);
18080
18202
  ctx.manifest.assets[assetId] = {
18081
18203
  prompt: orphan.prompt,
18082
18204
  promptByStyle: {},
@@ -18108,7 +18230,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
18108
18230
  sourceUrl: durableSource,
18109
18231
  sourceUrls: durableSource ? [{ url: durableSource }] : [],
18110
18232
  outputs: [{
18111
- path: portableOutputPath(outFile, import_node_path34.default.dirname(ctx.manifestPath)),
18233
+ path: portableOutputPath(outFile, import_node_path35.default.dirname(ctx.manifestPath)),
18112
18234
  sha256: sha256(buf)
18113
18235
  }],
18114
18236
  submittedAt: orphan.createdAt,
@@ -18132,9 +18254,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
18132
18254
  }
18133
18255
  }
18134
18256
  await applyTags(provider, decisions, existingTags, { onProgress: log, characters: characterMembers });
18135
- const raw = JSON.parse(await (0, import_promises27.readFile)(ctx.manifestPath, "utf8"));
18257
+ const raw = JSON.parse(await (0, import_promises28.readFile)(ctx.manifestPath, "utf8"));
18136
18258
  for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
18137
- await (0, import_promises27.writeFile)(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
18259
+ await (0, import_promises28.writeFile)(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
18138
18260
  await saveLock(ctx.lockPath, ctx.lock);
18139
18261
  if (characterAssetIds.length) result.characterAssetIds = characterAssetIds;
18140
18262
  return result;