pixelkiln 0.69.0 → 0.70.1

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
@@ -164,6 +164,7 @@ __export(index_exports, {
164
164
  fontOutputPaths: () => fontOutputPaths,
165
165
  formatCost: () => formatCost,
166
166
  frameSetFps: () => frameSetFps,
167
+ galleryContentSecurityPolicy: () => galleryContentSecurityPolicy,
167
168
  galleryMediaId: () => galleryMediaId,
168
169
  galleryMediaRoute: () => galleryMediaRoute,
169
170
  generateFont: () => generateFont,
@@ -2055,9 +2056,9 @@ var PixelLabClient = class {
2055
2056
  apiKey;
2056
2057
  http;
2057
2058
  /** Retries transport failures, 429, and 5xx; see http.ts for the policy. */
2058
- async request(path34, init) {
2059
+ async request(path35, init) {
2059
2060
  const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
2060
- const res = await this.http(`${BASE}${path34}`, {
2061
+ const res = await this.http(`${BASE}${path35}`, {
2061
2062
  ...init,
2062
2063
  headers: {
2063
2064
  Authorization: auth,
@@ -2067,13 +2068,13 @@ var PixelLabClient = class {
2067
2068
  });
2068
2069
  const text = await res.text();
2069
2070
  if (!res.ok) {
2070
- throw new PixelLabError(`${init?.method ?? "GET"} ${path34} \u2192 ${res.status}`, res.status, text);
2071
+ throw new PixelLabError(`${init?.method ?? "GET"} ${path35} \u2192 ${res.status}`, res.status, text);
2071
2072
  }
2072
2073
  if (!text) return {};
2073
2074
  try {
2074
2075
  return JSON.parse(text);
2075
2076
  } catch {
2076
- throw new Error(`${init?.method ?? "GET"} ${path34} returned invalid JSON`);
2077
+ throw new Error(`${init?.method ?? "GET"} ${path35} returned invalid JSON`);
2077
2078
  }
2078
2079
  }
2079
2080
  async balance() {
@@ -2465,10 +2466,10 @@ var PixelLabClient = class {
2465
2466
  const palette = args.paletteSwatchBase64 ? { color_image: { type: "base64", base64: args.paletteSwatchBase64, format: "png" }, force_colors: true } : {};
2466
2467
  const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
2467
2468
  const south = args.reference?.south;
2468
- let path34;
2469
+ let path35;
2469
2470
  let body;
2470
2471
  if (args.mode === "v3") {
2471
- path34 = "/create-character-v3";
2472
+ path35 = "/create-character-v3";
2472
2473
  body = {
2473
2474
  description: args.description,
2474
2475
  image_size: imageSize,
@@ -2482,7 +2483,7 @@ var PixelLabClient = class {
2482
2483
  ...args.enhancePrompt ? { enhance_prompt: true } : {}
2483
2484
  };
2484
2485
  } else if (args.mode === "pro-flash") {
2485
- path34 = "/create-character-pro-flash";
2486
+ path35 = "/create-character-pro-flash";
2486
2487
  const traits = args.styleTraits;
2487
2488
  body = {
2488
2489
  description: args.description,
@@ -2506,7 +2507,7 @@ var PixelLabClient = class {
2506
2507
  } : {}
2507
2508
  };
2508
2509
  } else if (args.mode === "pro") {
2509
- path34 = "/create-character-pro";
2510
+ path35 = "/create-character-pro";
2510
2511
  body = {
2511
2512
  description: args.description,
2512
2513
  image_size: imageSize,
@@ -2520,7 +2521,7 @@ var PixelLabClient = class {
2520
2521
  ...!south && args.styleCharacterId ? { style_character_id: args.styleCharacterId } : {}
2521
2522
  };
2522
2523
  } else {
2523
- path34 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
2524
+ path35 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
2524
2525
  body = {
2525
2526
  description: args.description,
2526
2527
  image_size: imageSize,
@@ -2537,8 +2538,8 @@ var PixelLabClient = class {
2537
2538
  ...palette
2538
2539
  };
2539
2540
  }
2540
- const raw = await this.request(path34, { method: "POST", body: JSON.stringify(body) });
2541
- return validateResponse(CharacterSubmitSchema, raw, path34.slice(1));
2541
+ const raw = await this.request(path35, { method: "POST", body: JSON.stringify(body) });
2542
+ return validateResponse(CharacterSubmitSchema, raw, path35.slice(1));
2542
2543
  }
2543
2544
  /** A text edit of an existing character, applied to every direction and stored as a sibling. */
2544
2545
  async createCharacterState(args) {
@@ -3716,10 +3717,10 @@ var import_node_fs = require("fs");
3716
3717
  function sha256(data) {
3717
3718
  return (0, import_node_crypto.createHash)("sha256").update(data).digest("hex");
3718
3719
  }
3719
- async function sha256File(path34) {
3720
+ async function sha256File(path35) {
3720
3721
  return new Promise((resolve, reject) => {
3721
3722
  const hash = (0, import_node_crypto.createHash)("sha256");
3722
- const stream = (0, import_node_fs.createReadStream)(path34);
3723
+ const stream = (0, import_node_fs.createReadStream)(path35);
3723
3724
  stream.on("data", (chunk2) => hash.update(chunk2));
3724
3725
  stream.on("error", reject);
3725
3726
  stream.on("end", () => resolve(hash.digest("hex")));
@@ -6082,9 +6083,9 @@ var RetroDiffusionClient = class {
6082
6083
  if (!response.ok) throw new ProviderError("retrodiffusion", `Retro Diffusion download failed (${response.status})`, { status: response.status });
6083
6084
  return Buffer.from(await response.arrayBuffer());
6084
6085
  }
6085
- async call(path34, init = {}) {
6086
+ async call(path35, init = {}) {
6086
6087
  if (!this.token) throw new Error("RD_API_KEY is not set");
6087
- const response = await this.request(`${this.baseUrl}${path34}`, {
6088
+ const response = await this.request(`${this.baseUrl}${path35}`, {
6088
6089
  ...init,
6089
6090
  headers: {
6090
6091
  "Content-Type": "application/json",
@@ -12373,10 +12374,10 @@ function isSha256Hash(value) {
12373
12374
  function parseCache(value) {
12374
12375
  return HashCacheSchema.parse(value);
12375
12376
  }
12376
- async function loadCache(path34) {
12377
- if (!(0, import_node_fs14.existsSync)(path34)) return { version: 1, hashes: {} };
12377
+ async function loadCache(path35) {
12378
+ if (!(0, import_node_fs14.existsSync)(path35)) return { version: 1, hashes: {} };
12378
12379
  try {
12379
- const parsed = HashCacheSchema.safeParse(JSON.parse(await (0, import_promises11.readFile)(path34, "utf8")));
12380
+ const parsed = HashCacheSchema.safeParse(JSON.parse(await (0, import_promises11.readFile)(path35, "utf8")));
12380
12381
  if (!parsed.success) return { version: 1, hashes: {} };
12381
12382
  return {
12382
12383
  version: 1,
@@ -12388,18 +12389,18 @@ async function loadCache(path34) {
12388
12389
  return { version: 1, hashes: {} };
12389
12390
  }
12390
12391
  }
12391
- async function saveCache(path34, cache) {
12392
+ async function saveCache(path35, cache) {
12392
12393
  const sorted = {};
12393
12394
  for (const key of Object.keys(cache.hashes).sort()) {
12394
12395
  const hash = cache.hashes[key];
12395
12396
  if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
12396
12397
  sorted[key] = hash;
12397
12398
  }
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`;
12399
+ await (0, import_promises11.mkdir)(import_node_path18.default.dirname(import_node_path18.default.resolve(path35)), { recursive: true });
12400
+ const tmp = `${path35}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
12400
12401
  try {
12401
12402
  await (0, import_promises11.writeFile)(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
12402
- await (0, import_promises11.rename)(tmp, path34);
12403
+ await (0, import_promises11.rename)(tmp, path35);
12403
12404
  } finally {
12404
12405
  await (0, import_promises11.rm)(tmp, { force: true });
12405
12406
  }
@@ -15204,9 +15205,9 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
15204
15205
 
15205
15206
  // src/gallery/snapshot.ts
15206
15207
  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);
15208
+ var import_node_fs26 = require("fs");
15209
+ var import_promises23 = require("fs/promises");
15210
+ var import_node_path31 = __toESM(require("path"), 1);
15210
15211
 
15211
15212
  // src/workspace.ts
15212
15213
  var import_promises19 = require("fs/promises");
@@ -15342,8 +15343,8 @@ function validateWorkspace(ws, dir) {
15342
15343
  }
15343
15344
 
15344
15345
  // src/gallery/edit.ts
15345
- var import_node_path29 = __toESM(require("path"), 1);
15346
- var import_zod10 = require("zod");
15346
+ var import_node_path30 = __toESM(require("path"), 1);
15347
+ var import_zod11 = require("zod");
15347
15348
 
15348
15349
  // src/manifest-edit.ts
15349
15350
  var import_promises20 = require("fs/promises");
@@ -15391,15 +15392,27 @@ var NewAssetSchema = import_zod8.z.object({
15391
15392
  /**
15392
15393
  * A controlled regeneration of another asset in the same manifest,
15393
15394
  * 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.
15395
+ * gallery does not offer yet, so only `image-to-image` and
15396
+ * `animate-skeleton` (whose keypoints file the gallery writes first) can
15397
+ * be created here; hand-edit the manifest for the other modes.
15396
15398
  */
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(),
15399
+ revision: import_zod8.z.discriminatedUnion("mode", [
15400
+ import_zod8.z.object({
15401
+ mode: import_zod8.z.literal("image-to-image"),
15402
+ /** Asset id of the parent this revises; must already exist. */
15403
+ from: import_zod8.z.string().min(1),
15404
+ strength: import_zod8.z.number().min(0).max(1).optional()
15405
+ }).strict(),
15406
+ import_zod8.z.object({
15407
+ mode: import_zod8.z.literal("animate-skeleton"),
15408
+ from: import_zod8.z.string().min(1),
15409
+ /** Manifest-relative; checked when the manifest is loaded, like every revision input. */
15410
+ keypointsFile: import_zod8.z.string().min(1),
15411
+ direction: CharacterDirectionSchema,
15412
+ description: import_zod8.z.string().min(1).optional(),
15413
+ skeletonTemplate: import_zod8.z.string().min(1).optional()
15414
+ }).strict()
15415
+ ]).optional(),
15403
15416
  /** A pose or outfit of an existing `character`/`objectPro` base or state. */
15404
15417
  state: NewStateSchema.optional(),
15405
15418
  /** A loop of an existing `character`/`objectPro` base or state. */
@@ -15903,41 +15916,118 @@ function openInEditor(file, editor = process.env.PIXELKILN_EDITOR) {
15903
15916
  return openExternal(file, command);
15904
15917
  }
15905
15918
 
15919
+ // src/gallery/skeleton.ts
15920
+ var import_node_fs25 = require("fs");
15921
+ var import_promises22 = require("fs/promises");
15922
+ var import_node_path29 = __toESM(require("path"), 1);
15923
+ var import_zod10 = require("zod");
15924
+ var SkeletonEstimateRequestSchema = import_zod10.z.object({
15925
+ project: import_zod10.z.string().min(1).optional(),
15926
+ styleId: import_zod10.z.string().min(1),
15927
+ assetId: import_zod10.z.string().min(1),
15928
+ frames: import_zod10.z.number().int().min(3).max(15).optional()
15929
+ }).strict();
15930
+ var SkeletonAnimationRequestSchema = import_zod10.z.object({
15931
+ action: import_zod10.z.literal("create-skeleton-animation"),
15932
+ project: import_zod10.z.string().min(1).optional(),
15933
+ expectedSha256: import_zod10.z.string().regex(/^[0-9a-f]{64}$/),
15934
+ styleId: import_zod10.z.string().min(1),
15935
+ /** The sprite the animation poses. */
15936
+ from: import_zod10.z.string().min(1),
15937
+ assetId: import_zod10.z.string().min(1).regex(/^[^/\\]+$/, "asset ids cannot contain slashes"),
15938
+ /** The motion, sent as PixelLab's `action`. */
15939
+ prompt: import_zod10.z.string().min(1),
15940
+ direction: CharacterDirectionSchema,
15941
+ description: import_zod10.z.string().min(1).optional(),
15942
+ skeletonTemplate: import_zod10.z.string().min(1).optional(),
15943
+ /** Manifest-relative JSON path inside the project. */
15944
+ keypointsFile: import_zod10.z.string().min(1),
15945
+ /** Poses to write to `keypointsFile`; omitted to use a file that already exists. */
15946
+ set: SkeletonSetSchema.optional(),
15947
+ /** Replace an existing `keypointsFile` with `set`. */
15948
+ overwrite: import_zod10.z.boolean().optional()
15949
+ }).strict();
15950
+ async function createSkeletonAnimation(manifestPath, request) {
15951
+ const root = import_node_path29.default.dirname(import_node_path29.default.resolve(manifestPath));
15952
+ const file = import_node_path29.default.resolve(root, request.keypointsFile);
15953
+ const relative = import_node_path29.default.relative(root, file);
15954
+ if (!request.keypointsFile.endsWith(".json") || relative.startsWith("..") || import_node_path29.default.isAbsolute(relative)) {
15955
+ throw new ManifestEditError("the keypoints file must be a .json path inside the project");
15956
+ }
15957
+ const existed = (0, import_node_fs25.existsSync)(file);
15958
+ let previous = null;
15959
+ if (request.set) {
15960
+ if (existed && !request.overwrite) {
15961
+ throw new ManifestEditError(`${request.keypointsFile} already exists; choose another path, or replace it`);
15962
+ }
15963
+ if (existed) previous = await (0, import_promises22.readFile)(file);
15964
+ await (0, import_promises22.mkdir)(import_node_path29.default.dirname(file), { recursive: true });
15965
+ await (0, import_promises22.writeFile)(file, JSON.stringify(request.set, null, 2) + "\n");
15966
+ } else if (!existed) {
15967
+ throw new ManifestEditError(`${request.keypointsFile} does not exist; paste poses or estimate them from the sprite`);
15968
+ }
15969
+ try {
15970
+ await applyManifestEdit(manifestPath, {
15971
+ action: "add-asset",
15972
+ assetId: request.assetId,
15973
+ expectedSha256: request.expectedSha256,
15974
+ asset: {
15975
+ prompt: request.prompt,
15976
+ styles: [request.styleId],
15977
+ revision: {
15978
+ mode: "animate-skeleton",
15979
+ from: request.from,
15980
+ keypointsFile: relative.split(import_node_path29.default.sep).join("/"),
15981
+ direction: request.direction,
15982
+ ...request.description ? { description: request.description } : {},
15983
+ ...request.skeletonTemplate ? { skeletonTemplate: request.skeletonTemplate } : {}
15984
+ }
15985
+ }
15986
+ });
15987
+ } catch (error) {
15988
+ if (request.set) {
15989
+ if (previous) await (0, import_promises22.writeFile)(file, previous);
15990
+ else await (0, import_promises22.rm)(file, { force: true });
15991
+ }
15992
+ throw error;
15993
+ }
15994
+ }
15995
+
15906
15996
  // 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}$/),
15997
+ var Base64 = import_zod11.z.string().regex(/^[A-Za-z0-9+/]*={0,2}$/).max(Math.ceil(MAX_HAND_EDIT_BYTES / 3) * 4);
15998
+ var HandEditRequestSchema = import_zod11.z.discriminatedUnion("action", [
15999
+ import_zod11.z.object({
16000
+ action: import_zod11.z.literal("start-edit"),
16001
+ project: import_zod11.z.string().min(1).optional(),
16002
+ assetId: import_zod11.z.string().min(1),
16003
+ styleId: import_zod11.z.string().min(1),
16004
+ expectedSha256: import_zod11.z.string().regex(/^[0-9a-f]{64}$/),
15915
16005
  /** Also launch the author's editor on the file. */
15916
- open: import_zod10.z.boolean().optional()
16006
+ open: import_zod11.z.boolean().optional()
15917
16007
  }).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}$/)
16008
+ import_zod11.z.object({
16009
+ action: import_zod11.z.literal("detach-edit"),
16010
+ project: import_zod11.z.string().min(1).optional(),
16011
+ assetId: import_zod11.z.string().min(1),
16012
+ styleId: import_zod11.z.string().min(1),
16013
+ expectedSha256: import_zod11.z.string().regex(/^[0-9a-f]{64}$/)
15924
16014
  }).strict(),
15925
16015
  /** 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}$/),
16016
+ import_zod11.z.object({
16017
+ action: import_zod11.z.literal("save-edit"),
16018
+ project: import_zod11.z.string().min(1).optional(),
16019
+ assetId: import_zod11.z.string().min(1),
16020
+ styleId: import_zod11.z.string().min(1),
16021
+ expectedSha256: import_zod11.z.string().regex(/^[0-9a-f]{64}$/),
15932
16022
  /** The flattened image; for a frame set, `frames` instead. */
15933
16023
  png: Base64.min(1).optional(),
15934
16024
  /** 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(),
16025
+ 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
16026
  /** The editor's layered project file, kept beside the edit. */
15937
16027
  pxo: Base64.optional(),
15938
16028
  /** 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()
16029
+ editor: import_zod11.z.string().min(1).max(200),
16030
+ protocol: import_zod11.z.number().int().positive()
15941
16031
  }).strict()
15942
16032
  ]);
15943
16033
  function createGalleryEditHandler(opts) {
@@ -15978,6 +16068,23 @@ function createGalleryEditHandler(opts) {
15978
16068
  }
15979
16069
  return opts.reload();
15980
16070
  }
16071
+ const skeleton = SkeletonAnimationRequestSchema.safeParse(body);
16072
+ if (skeleton.success) {
16073
+ let manifestPath2;
16074
+ try {
16075
+ manifestPath2 = await opts.manifestFor(skeleton.data.project);
16076
+ } catch (error) {
16077
+ throw new ManifestEditError(error instanceof Error ? error.message : String(error));
16078
+ }
16079
+ await createSkeletonAnimation(manifestPath2, skeleton.data);
16080
+ log(` manifest edited: added skeleton animation ${skeleton.data.assetId} (${skeleton.data.keypointsFile})`);
16081
+ return opts.reload();
16082
+ }
16083
+ if (body?.action === "create-skeleton-animation") {
16084
+ throw new ManifestEditError(
16085
+ "invalid skeleton animation: " + skeleton.error.issues.slice(0, 3).map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")
16086
+ );
16087
+ }
15981
16088
  const parsed = ManifestEditSchema.safeParse(body);
15982
16089
  if (!parsed.success) {
15983
16090
  throw new ManifestEditError(
@@ -15992,7 +16099,7 @@ function createGalleryEditHandler(opts) {
15992
16099
  }
15993
16100
  const result = await applyManifestEdit(manifestPath, parsed.data);
15994
16101
  if (result.changed) {
15995
- const relative = import_node_path29.default.relative(process.cwd(), result.manifestPath);
16102
+ const relative = import_node_path30.default.relative(process.cwd(), result.manifestPath);
15996
16103
  const shown = !relative || relative.startsWith("..") ? result.manifestPath : relative;
15997
16104
  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
16105
  log(` manifest edited: ${parsed.data.action === "add-asset" ? "added" : "changed"} ${subject} in ${shown}`);
@@ -16004,7 +16111,7 @@ function createGalleryEditHandler(opts) {
16004
16111
  // src/gallery/snapshot.ts
16005
16112
  var PROJECT_FILE_TYPE = "application/octet-stream";
16006
16113
  function galleryMediaId(absolutePath) {
16007
- return (0, import_node_crypto6.createHash)("sha256").update(import_node_path30.default.resolve(absolutePath)).digest("hex").slice(0, 24);
16114
+ return (0, import_node_crypto6.createHash)("sha256").update(import_node_path31.default.resolve(absolutePath)).digest("hex").slice(0, 24);
16008
16115
  }
16009
16116
  function galleryMediaRoute(id) {
16010
16117
  return `/media/${id}`;
@@ -16023,9 +16130,9 @@ function describeHistory(media, entry, cacheDir) {
16023
16130
  return (entry.history ?? []).map((generation, i) => {
16024
16131
  const outputs = generation.outputs.map((output) => {
16025
16132
  const mediaType = output.mediaType ?? MediaType.PNG;
16026
- const file = import_node_path30.default.join(cacheDir, cacheFileName(output.sha256, mediaType));
16133
+ const file = import_node_path31.default.join(cacheDir, cacheFileName(output.sha256, mediaType));
16027
16134
  let url = null;
16028
- if ((0, import_node_fs25.existsSync)(file)) {
16135
+ if ((0, import_node_fs26.existsSync)(file)) {
16029
16136
  const id = galleryMediaId(file);
16030
16137
  media.set(id, { path: file, contentType: mediaType });
16031
16138
  url = `${galleryMediaRoute(id)}?v=${output.sha256.slice(0, 24)}`;
@@ -16057,7 +16164,7 @@ function describeHistory(media, entry, cacheDir) {
16057
16164
  var MAX_COMPARED_BYTES = 8 * 1024 * 1024;
16058
16165
  async function samePixels(a, b) {
16059
16166
  try {
16060
- const [left, right] = await Promise.all([(0, import_promises22.readFile)(a), (0, import_promises22.readFile)(b)]);
16167
+ const [left, right] = await Promise.all([(0, import_promises23.readFile)(a), (0, import_promises23.readFile)(b)]);
16061
16168
  if (left.length > MAX_COMPARED_BYTES || right.length > MAX_COMPARED_BYTES) return false;
16062
16169
  const x = decodePng(left);
16063
16170
  const y = decodePng(right);
@@ -16123,7 +16230,7 @@ function describeCharacter(spec, entry) {
16123
16230
  }
16124
16231
  async function fileInfo(absolutePath) {
16125
16232
  try {
16126
- const info = await (0, import_promises22.stat)(absolutePath);
16233
+ const info = await (0, import_promises23.stat)(absolutePath);
16127
16234
  return info.isFile() ? { bytes: info.size, modifiedAt: info.mtime.toISOString() } : null;
16128
16235
  } catch {
16129
16236
  return null;
@@ -16155,10 +16262,10 @@ async function describeOutput(media, root, absolutePath, recorded = {}) {
16155
16262
  async function describeQuality(media, root, inspection) {
16156
16263
  const outputPaths = inspection.outputs ?? [inspection.output];
16157
16264
  const outputs = await Promise.all(
16158
- outputPaths.map((file) => describeOutput(media, root, import_node_path30.default.resolve(root, file)))
16265
+ outputPaths.map((file) => describeOutput(media, root, import_node_path31.default.resolve(root, file)))
16159
16266
  );
16160
- const recordPath = import_node_path30.default.resolve(root, inspection.record);
16161
- const recordExists = (0, import_node_fs25.existsSync)(recordPath);
16267
+ const recordPath = import_node_path31.default.resolve(root, inspection.record);
16268
+ const recordExists = (0, import_node_fs26.existsSync)(recordPath);
16162
16269
  let options = null;
16163
16270
  let check = null;
16164
16271
  if (recordExists) {
@@ -16177,7 +16284,7 @@ async function describeQuality(media, root, inspection) {
16177
16284
  return {
16178
16285
  state: inspection.state,
16179
16286
  reason: inspection.reason,
16180
- output: portableOutputPath(import_node_path30.default.resolve(root, inspection.output), root),
16287
+ output: portableOutputPath(import_node_path31.default.resolve(root, inspection.output), root),
16181
16288
  record: portableOutputPath(recordPath, root),
16182
16289
  recordExists,
16183
16290
  outputs,
@@ -16215,9 +16322,9 @@ async function buildGallerySnapshot(opts) {
16215
16322
  const media = /* @__PURE__ */ new Map();
16216
16323
  const plan = await buildPlan(specs, lock);
16217
16324
  const items = [];
16218
- const manifestText = await (0, import_promises22.readFile)(loaded.path, "utf8");
16325
+ const manifestText = await (0, import_promises23.readFile)(loaded.path, "utf8");
16219
16326
  const manifestSha256 = sha256(manifestText);
16220
- const cacheDir = import_node_path30.default.resolve(import_node_path30.default.dirname(opts.lockPath), ".pixelkiln", "cache");
16327
+ const cacheDir = import_node_path31.default.resolve(import_node_path31.default.dirname(opts.lockPath), ".pixelkiln", "cache");
16221
16328
  let rawStyles = {};
16222
16329
  try {
16223
16330
  const raw = JSON.parse(manifestText);
@@ -16241,12 +16348,12 @@ async function buildGallerySnapshot(opts) {
16241
16348
  )
16242
16349
  );
16243
16350
  if (spec.source) {
16244
- const editPath = import_node_path30.default.resolve(root, spec.source);
16351
+ const editPath = import_node_path31.default.resolve(root, spec.source);
16245
16352
  const memberCount = sourceIsStem(spec.source, entry, root) ? entry.outputs.length : 1;
16246
16353
  const editShas = await Promise.all(
16247
16354
  Array.from({ length: memberCount }, async (_, index) => {
16248
16355
  const file = sourceOutputPath(spec.source, entry, index, root);
16249
- return { file, sha256: (0, import_node_fs25.existsSync)(file) ? await sha256File(file) : null };
16356
+ return { file, sha256: (0, import_node_fs26.existsSync)(file) ? await sha256File(file) : null };
16250
16357
  })
16251
16358
  );
16252
16359
  edits = await Promise.all(editShas.map(({ file, sha256: editSha }) => describeOutput(media, root, file, { sha256: editSha })));
@@ -16254,7 +16361,7 @@ async function buildGallerySnapshot(opts) {
16254
16361
  const companion = await readHandEditCompanion(editPath);
16255
16362
  if (companion) {
16256
16363
  const projectPath = handEditProjectPath(editPath);
16257
- const project = companion.project && (0, import_node_fs25.existsSync)(projectPath) ? await fileInfo(projectPath) : null;
16364
+ const project = companion.project && (0, import_node_fs26.existsSync)(projectPath) ? await fileInfo(projectPath) : null;
16258
16365
  let projectUrl = null;
16259
16366
  if (project) {
16260
16367
  const id = galleryMediaId(projectPath);
@@ -16290,8 +16397,8 @@ async function buildGallerySnapshot(opts) {
16290
16397
  editStatus = edits.some((member) => !member.exists) ? "missing" : editChanged.every((changed) => !changed) ? "same" : regenerated ? "regenerated-since" : "edited";
16291
16398
  }
16292
16399
  } 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)) {
16400
+ outputs = [await describeOutput(media, root, import_node_path31.default.resolve(root, spec.source))];
16401
+ } else if (!entry && (0, import_node_fs26.existsSync)(spec.outFile)) {
16295
16402
  outputs = [await describeOutput(media, root, spec.outFile)];
16296
16403
  } else {
16297
16404
  outputs = [];
@@ -16332,6 +16439,10 @@ async function buildGallerySnapshot(opts) {
16332
16439
  revision: entry?.revision ?? null,
16333
16440
  revisionParentKey: spec.revision ? lockKey(spec.styleId, spec.revision.sourceAssetId) : entry?.revision ? lockKey(spec.styleId, entry.revision.sourceAssetId) : null,
16334
16441
  mirrorOfKey: spec.mirror ? lockKey(spec.styleId, spec.mirror.sourceAssetId) : entry?.mirror ? lockKey(spec.styleId, entry.mirror.sourceAssetId) : null,
16442
+ skeleton: spec.revision?.mode === "animate-skeleton" && spec.revision.keypointsFile ? {
16443
+ keypointsFile: portableOutputPath(spec.revision.keypointsFile, root),
16444
+ set: spec.revision.skeleton ?? null
16445
+ } : null,
16335
16446
  character: describeCharacter(spec, entry),
16336
16447
  outputs,
16337
16448
  quality,
@@ -16396,6 +16507,7 @@ async function buildGallerySnapshot(opts) {
16396
16507
  revision: entry.revision,
16397
16508
  revisionParentKey: entry.revision ? lockKey(entry.styleId, entry.revision.sourceAssetId) : null,
16398
16509
  mirrorOfKey: entry.mirror ? lockKey(entry.styleId, entry.mirror.sourceAssetId) : null,
16510
+ skeleton: null,
16399
16511
  character: describeCharacter(void 0, entry),
16400
16512
  outputs,
16401
16513
  quality: null,
@@ -16486,7 +16598,7 @@ async function buildGallerySnapshot(opts) {
16486
16598
  id: loaded.manifest.name,
16487
16599
  name: loaded.manifest.name,
16488
16600
  manifest: loaded.path,
16489
- lock: import_node_path30.default.resolve(opts.lockPath),
16601
+ lock: import_node_path31.default.resolve(opts.lockPath),
16490
16602
  root,
16491
16603
  provider: loaded.manifest.provider,
16492
16604
  account: null,
@@ -16512,7 +16624,7 @@ async function buildGallerySnapshot(opts) {
16512
16624
  return { snapshot, media };
16513
16625
  }
16514
16626
  async function buildWorkspaceGallerySnapshot(opts) {
16515
- const dir = import_node_path30.default.dirname(import_node_path30.default.resolve(opts.workspacePath));
16627
+ const dir = import_node_path31.default.dirname(import_node_path31.default.resolve(opts.workspacePath));
16516
16628
  const media = /* @__PURE__ */ new Map();
16517
16629
  const projects = [];
16518
16630
  const items = [];
@@ -16555,11 +16667,11 @@ async function buildWorkspaceGallerySnapshot(opts) {
16555
16667
  id: project.id,
16556
16668
  name: loaded.manifest.name,
16557
16669
  manifest: manifestPath,
16558
- lock: import_node_path30.default.resolve(lockPath),
16670
+ lock: import_node_path31.default.resolve(lockPath),
16559
16671
  root: loaded.root,
16560
16672
  provider: loaded.manifest.provider,
16561
16673
  account: project.account ?? null,
16562
- manifestSha256: manifestSha256 ?? sha256(await (0, import_promises22.readFile)(manifestPath, "utf8")),
16674
+ manifestSha256: manifestSha256 ?? sha256(await (0, import_promises23.readFile)(manifestPath, "utf8")),
16563
16675
  historyLimit: historyLimit(loaded.manifest),
16564
16676
  entries: Object.keys(lock.entries).length,
16565
16677
  items: projectItems.length,
@@ -16571,8 +16683,8 @@ async function buildWorkspaceGallerySnapshot(opts) {
16571
16683
  id: project.id,
16572
16684
  name: project.id,
16573
16685
  manifest: manifestPath,
16574
- lock: import_node_path30.default.resolve(lockPath),
16575
- root: import_node_path30.default.dirname(manifestPath),
16686
+ lock: import_node_path31.default.resolve(lockPath),
16687
+ root: import_node_path31.default.dirname(manifestPath),
16576
16688
  provider: project.provider,
16577
16689
  account: project.account ?? null,
16578
16690
  manifestSha256: null,
@@ -16599,7 +16711,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
16599
16711
  version: 1,
16600
16712
  generatedAt: (opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
16601
16713
  project: null,
16602
- workspace: { path: import_node_path30.default.resolve(opts.workspacePath), dir, projects },
16714
+ workspace: { path: import_node_path31.default.resolve(opts.workspacePath), dir, projects },
16603
16715
  filter,
16604
16716
  totals: {
16605
16717
  entries: projects.reduce((sum, project) => sum + project.entries, 0),
@@ -16616,13 +16728,48 @@ async function buildWorkspaceGallerySnapshot(opts) {
16616
16728
  }
16617
16729
 
16618
16730
  // src/gallery/page.ts
16619
- var import_node_fs26 = require("fs");
16731
+ var import_node_fs27 = require("fs");
16732
+
16733
+ // src/gallery/client-bundle.ts
16734
+ var import_node_module = require("module");
16735
+ var import_node_url2 = require("url");
16736
+ function bundleGalleryClient(entry = (0, import_node_url2.fileURLToPath)(new URL("./client/src/main.ts", importMetaUrl))) {
16737
+ const esbuild = (0, import_node_module.createRequire)(importMetaUrl)("esbuild");
16738
+ const result = esbuild.buildSync({
16739
+ entryPoints: [entry],
16740
+ bundle: true,
16741
+ format: "iife",
16742
+ target: "es2020",
16743
+ platform: "browser",
16744
+ write: false,
16745
+ legalComments: "none",
16746
+ charset: "utf8"
16747
+ });
16748
+ return result.outputFiles[0].text;
16749
+ }
16750
+
16751
+ // src/gallery/page.ts
16752
+ function galleryContentSecurityPolicy(nonce) {
16753
+ return [
16754
+ "default-src 'self'",
16755
+ `script-src 'nonce-${nonce}'`,
16756
+ `style-src 'nonce-${nonce}'`,
16757
+ "img-src 'self' data: blob:",
16758
+ "frame-src 'self'",
16759
+ "connect-src 'self'",
16760
+ "object-src 'none'",
16761
+ "base-uri 'none'",
16762
+ "form-action 'none'",
16763
+ "frame-ancestors 'none'"
16764
+ ].join("; ");
16765
+ }
16620
16766
  var CLIENT_DIR = new URL("./client/", importMetaUrl);
16621
16767
  var client;
16622
16768
  function clientAssets() {
16769
+ const prebuilt = new URL("gallery.js", CLIENT_DIR);
16623
16770
  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")
16771
+ css: (0, import_node_fs27.readFileSync)(new URL("gallery.css", CLIENT_DIR), "utf8"),
16772
+ js: (0, import_node_fs27.existsSync)(prebuilt) ? (0, import_node_fs27.readFileSync)(prebuilt, "utf8") : bundleGalleryClient()
16626
16773
  };
16627
16774
  return client;
16628
16775
  }
@@ -16634,6 +16781,7 @@ function renderGallery(snapshot, opts = {}) {
16634
16781
  const editor = JSON.stringify(Boolean(opts.editor && opts.session));
16635
16782
  const title = `${snapshot.project?.name ?? "workspace"} | pixelkiln`;
16636
16783
  const { css, js } = clientAssets();
16784
+ const nonce = opts.nonce ? ` nonce="${escapeHtml2(opts.nonce)}"` : "";
16637
16785
  return `<!doctype html>
16638
16786
  <html lang="en">
16639
16787
  <head>
@@ -16641,7 +16789,7 @@ function renderGallery(snapshot, opts = {}) {
16641
16789
  <meta name="viewport" content="width=device-width, initial-scale=1">
16642
16790
  <title>${escapeHtml2(title)}</title>
16643
16791
  <link rel="icon" href="data:,">
16644
- <style>
16792
+ <style${nonce}>
16645
16793
  ${css}</style>
16646
16794
  </head>
16647
16795
  <body>
@@ -16683,7 +16831,7 @@ ${css}</style>
16683
16831
  <div id="drawer-host"></div>
16684
16832
  <div id="dialog-host"></div>
16685
16833
  <datalist id="view-options"><option value="low top-down"><option value="high top-down"><option value="side"><option value="sidescroller"></datalist>
16686
- <script>
16834
+ <script${nonce}>
16687
16835
  const INITIAL = ${data};
16688
16836
  const SESSION = ${session};
16689
16837
  const EDITABLE = ${editable};
@@ -16748,10 +16896,10 @@ var EDITOR_RELEASE_BASE = "https://github.com/gfargo/pixelkiln/releases/download
16748
16896
 
16749
16897
  // src/editor/install.ts
16750
16898
  var import_node_crypto7 = require("crypto");
16751
- var import_node_fs27 = require("fs");
16752
- var import_promises23 = require("fs/promises");
16899
+ var import_node_fs28 = require("fs");
16900
+ var import_promises24 = require("fs/promises");
16753
16901
  var import_node_os3 = __toESM(require("os"), 1);
16754
- var import_node_path31 = __toESM(require("path"), 1);
16902
+ var import_node_path32 = __toESM(require("path"), 1);
16755
16903
  var EditorInstallError = class extends Error {
16756
16904
  constructor(message6, file) {
16757
16905
  super(message6);
@@ -16762,15 +16910,15 @@ var EditorInstallError = class extends Error {
16762
16910
  };
16763
16911
  function editorToolsRoot() {
16764
16912
  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");
16913
+ if (override) return import_node_path32.default.resolve(override);
16914
+ if (process.platform === "darwin") return import_node_path32.default.join(import_node_os3.default.homedir(), "Library", "Caches", "pixelkiln", "tools");
16767
16915
  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");
16916
+ return import_node_path32.default.join(process.env.LOCALAPPDATA ?? import_node_path32.default.join(import_node_os3.default.homedir(), "AppData", "Local"), "pixelkiln", "tools");
16769
16917
  }
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");
16918
+ 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
16919
  }
16772
16920
  function editorDir(pin = EDITOR_PIN, root = editorToolsRoot()) {
16773
- return import_node_path31.default.join(root, "pixelorama", pin.release ?? "unpublished");
16921
+ return import_node_path32.default.join(root, "pixelorama", pin.release ?? "unpublished");
16774
16922
  }
16775
16923
  function editorBaseUrl(pin = EDITOR_PIN) {
16776
16924
  const override = process.env.PIXELKILN_EDITOR_URL?.trim();
@@ -16780,7 +16928,7 @@ function editorBaseUrl(pin = EDITOR_PIN) {
16780
16928
  }
16781
16929
  async function sha256Of(file) {
16782
16930
  try {
16783
- return (0, import_node_crypto7.createHash)("sha256").update(await (0, import_promises23.readFile)(file)).digest("hex");
16931
+ return (0, import_node_crypto7.createHash)("sha256").update(await (0, import_promises24.readFile)(file)).digest("hex");
16784
16932
  } catch {
16785
16933
  return null;
16786
16934
  }
@@ -16793,8 +16941,8 @@ async function editorStatus(opts = {}) {
16793
16941
  let totalBytes = 0;
16794
16942
  for (const [name, expected] of Object.entries(pin.files)) {
16795
16943
  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;
16944
+ const file = import_node_path32.default.join(dir, name);
16945
+ const info = (0, import_node_fs28.existsSync)(file) ? await (0, import_promises24.stat)(file).catch(() => null) : null;
16798
16946
  if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
16799
16947
  installedBytes += expected.bytes;
16800
16948
  } else {
@@ -16823,7 +16971,7 @@ async function installEditor(opts = {}) {
16823
16971
  );
16824
16972
  }
16825
16973
  if (!baseUrl) throw new EditorInstallError("no download location for the editor build");
16826
- await (0, import_promises23.mkdir)(dir, { recursive: true });
16974
+ await (0, import_promises24.mkdir)(dir, { recursive: true });
16827
16975
  const downloaded = [];
16828
16976
  const skipped = Object.keys(pin.files).filter((name) => !before.missing.includes(name));
16829
16977
  let fetchedBytes = before.installedBytes;
@@ -16851,13 +16999,13 @@ async function installEditor(opts = {}) {
16851
16999
  if (digest2 !== expected.sha256) {
16852
17000
  throw new EditorInstallError(`${name}: sha256 ${digest2.slice(0, 12)}\u2026 does not match the pinned ${expected.sha256.slice(0, 12)}\u2026`, name);
16853
17001
  }
16854
- const final = import_node_path31.default.join(dir, name);
17002
+ const final = import_node_path32.default.join(dir, name);
16855
17003
  const tmp = `${final}.${process.pid}.part`;
16856
17004
  try {
16857
- await (0, import_promises23.writeFile)(tmp, bytes);
16858
- await (0, import_promises23.rename)(tmp, final);
17005
+ await (0, import_promises24.writeFile)(tmp, bytes);
17006
+ await (0, import_promises24.rename)(tmp, final);
16859
17007
  } finally {
16860
- await (0, import_promises23.rm)(tmp, { force: true });
17008
+ await (0, import_promises24.rm)(tmp, { force: true });
16861
17009
  }
16862
17010
  fetchedBytes += bytes.length;
16863
17011
  downloaded.push(name);
@@ -16897,12 +17045,12 @@ var EDITOR_CONTENT_TYPES = {
16897
17045
  ".json": "application/json; charset=utf-8"
16898
17046
  };
16899
17047
  function editorContentType(name) {
16900
- return EDITOR_CONTENT_TYPES[import_node_path31.default.extname(name).toLowerCase()] ?? null;
17048
+ return EDITOR_CONTENT_TYPES[import_node_path32.default.extname(name).toLowerCase()] ?? null;
16901
17049
  }
16902
17050
 
16903
17051
  // src/gallery/editor.ts
16904
- var import_promises24 = require("fs/promises");
16905
- var import_node_path32 = __toESM(require("path"), 1);
17052
+ var import_promises25 = require("fs/promises");
17053
+ var import_node_path33 = __toESM(require("path"), 1);
16906
17054
  function editorRoute(release, name = "index.html") {
16907
17055
  return `/editor/${encodeURIComponent(release)}/${encodeURIComponent(name)}`;
16908
17056
  }
@@ -16975,7 +17123,7 @@ function createGalleryEditorHandlers(opts = {}) {
16975
17123
  if (!status.installed) return null;
16976
17124
  let bytes;
16977
17125
  try {
16978
- bytes = await (0, import_promises24.readFile)(import_node_path32.default.join(dir, name));
17126
+ bytes = await (0, import_promises25.readFile)(import_node_path33.default.join(dir, name));
16979
17127
  } catch {
16980
17128
  verified = null;
16981
17129
  return null;
@@ -16992,7 +17140,7 @@ function createGalleryEditorHandlers(opts = {}) {
16992
17140
  // src/gallery/server.ts
16993
17141
  var import_node_crypto8 = require("crypto");
16994
17142
  var import_node_http2 = require("http");
16995
- var import_promises25 = require("fs/promises");
17143
+ var import_promises26 = require("fs/promises");
16996
17144
  var MAX_EDIT_BYTES = 48 * 1024 * 1024;
16997
17145
  var EDITOR_CSP = [
16998
17146
  "default-src 'self'",
@@ -17020,7 +17168,7 @@ async function readJsonBody(req) {
17020
17168
  async function serveGallery(opts) {
17021
17169
  const log = opts.onProgress ?? (() => {
17022
17170
  });
17023
- const session = opts.edit || opts.generate || opts.editor ? (0, import_node_crypto8.randomBytes)(16).toString("hex") : null;
17171
+ const session = opts.edit || opts.generate || opts.editor || opts.skeleton ? (0, import_node_crypto8.randomBytes)(16).toString("hex") : null;
17024
17172
  let media = /* @__PURE__ */ new Map();
17025
17173
  let loading = null;
17026
17174
  const reviewAssets = /* @__PURE__ */ new Map();
@@ -17095,6 +17243,17 @@ async function serveGallery(opts) {
17095
17243
  }
17096
17244
  return;
17097
17245
  }
17246
+ if (req.method === "POST" && url2.pathname === "/api/skeleton/estimate") {
17247
+ if (!opts.skeleton || !session) return fail(405, "skeleton estimates need the gallery started with --edit");
17248
+ try {
17249
+ const body = await guardedBody("skeleton estimates");
17250
+ if (body === void 0) return;
17251
+ json(200, await opts.skeleton.estimate(body));
17252
+ } catch (err) {
17253
+ reportError("skeleton estimate", err);
17254
+ }
17255
+ return;
17256
+ }
17098
17257
  if (req.method === "POST" && url2.pathname === "/api/editor/install") {
17099
17258
  if (!opts.editor || !session) return fail(405, "the in-browser editor is off for this gallery");
17100
17259
  try {
@@ -17151,7 +17310,7 @@ async function serveGallery(opts) {
17151
17310
  const asset = reviewAssets.get(jobId)?.get(url2.pathname);
17152
17311
  if (!asset) return fail(404, "no such review asset");
17153
17312
  try {
17154
- const bytes = await (0, import_promises25.readFile)(asset.path);
17313
+ const bytes = await (0, import_promises26.readFile)(asset.path);
17155
17314
  res.writeHead(200, {
17156
17315
  "Content-Type": asset.contentType,
17157
17316
  "Cache-Control": "no-store",
@@ -17165,6 +17324,10 @@ async function serveGallery(opts) {
17165
17324
  }
17166
17325
  }
17167
17326
  if (req.method !== "GET" && req.method !== "HEAD") return fail(405, "the gallery is read-only");
17327
+ if (url2.pathname === "/api/skeleton") {
17328
+ if (!opts.skeleton) return fail(404, "skeleton estimates need the gallery started with --edit");
17329
+ return json(200, opts.skeleton.status());
17330
+ }
17168
17331
  if (url2.pathname === "/api/jobs") {
17169
17332
  if (!opts.generate) return fail(404, "generation is off");
17170
17333
  return json(200, opts.generate.status());
@@ -17198,12 +17361,15 @@ async function serveGallery(opts) {
17198
17361
  try {
17199
17362
  if (url2.pathname === "/") {
17200
17363
  const { snapshot } = await load();
17364
+ const nonce = (0, import_node_crypto8.randomBytes)(16).toString("base64");
17201
17365
  res.writeHead(200, {
17202
17366
  "Content-Type": "text/html; charset=utf-8",
17203
17367
  "Cache-Control": "no-store",
17204
- "X-Content-Type-Options": "nosniff"
17368
+ "X-Content-Type-Options": "nosniff",
17369
+ "Content-Security-Policy": galleryContentSecurityPolicy(nonce)
17205
17370
  });
17206
17371
  res.end(renderGallery(snapshot, {
17372
+ nonce,
17207
17373
  ...session ? { session } : {},
17208
17374
  editable: Boolean(opts.edit),
17209
17375
  generation: Boolean(opts.generate),
@@ -17227,7 +17393,7 @@ async function serveGallery(opts) {
17227
17393
  if (!asset) return fail(404, "no such gallery media");
17228
17394
  let bytes;
17229
17395
  try {
17230
- bytes = await (0, import_promises25.readFile)(asset.path);
17396
+ bytes = await (0, import_promises26.readFile)(asset.path);
17231
17397
  } catch {
17232
17398
  return fail(404, "gallery media is no longer on disk; refresh the page");
17233
17399
  }
@@ -17271,27 +17437,27 @@ async function serveGallery(opts) {
17271
17437
 
17272
17438
  // src/gallery/generate.ts
17273
17439
  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),
17440
+ var import_zod12 = require("zod");
17441
+ var GenerateRequestSchema = import_zod12.z.object({
17442
+ project: import_zod12.z.string().min(1).optional(),
17443
+ keys: import_zod12.z.array(import_zod12.z.string().min(1)).min(1).max(500),
17278
17444
  /**
17279
17445
  * Regenerate work that is up to date, as `gen --force` does, and let the
17280
17446
  * fetch replace a locally modified or untracked file. With `restore`, put
17281
17447
  * the recorded bytes back over a modified file.
17282
17448
  */
17283
- force: import_zod11.z.boolean().optional(),
17449
+ force: import_zod12.z.boolean().optional(),
17284
17450
  /** Advance in-flight, review, or selected work without submitting anything. */
17285
- resume: import_zod11.z.boolean().optional(),
17451
+ resume: import_zod12.z.boolean().optional(),
17286
17452
  /** Re-download downloaded outputs and replace files whose object changed upstream. */
17287
- refresh: import_zod11.z.boolean().optional(),
17453
+ refresh: import_zod12.z.boolean().optional(),
17288
17454
  /** Put recorded outputs back on disk from the cache or the provider, without generating. */
17289
- restore: import_zod11.z.boolean().optional(),
17455
+ restore: import_zod12.z.boolean().optional(),
17290
17456
  /**
17291
17457
  * Bring a replaced generation back: 1-based position in the record's
17292
17458
  * history (newest first) or an output hash prefix. One key at a time.
17293
17459
  */
17294
- revert: import_zod11.z.string().min(1).max(64).optional()
17460
+ revert: import_zod12.z.string().min(1).max(64).optional()
17295
17461
  }).strict().refine((request) => [request.resume, request.refresh, request.restore, request.revert !== void 0].filter(Boolean).length <= 1, {
17296
17462
  message: "resume, refresh, restore, and revert are exclusive"
17297
17463
  }).refine((request) => request.revert === void 0 || request.keys.length === 1, { message: "revert takes one key" });
@@ -17601,19 +17767,19 @@ function createGenerateHandlers(opts) {
17601
17767
  }
17602
17768
 
17603
17769
  // 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);
17770
+ var import_promises27 = require("fs/promises");
17771
+ var import_node_fs29 = require("fs");
17772
+ var import_node_path34 = __toESM(require("path"), 1);
17607
17773
  function providerClaimId(provider, objectId) {
17608
17774
  return `${encodeURIComponent(provider)}:${encodeURIComponent(objectId)}`;
17609
17775
  }
17610
17776
  async function loadClaims(lockPaths, opts = {}) {
17611
17777
  const claimed = /* @__PURE__ */ new Set();
17612
17778
  for (const p of lockPaths) {
17613
- if (!(0, import_node_fs28.existsSync)(p)) throw new Error(`Claim lockfile not found: ${p}`);
17779
+ if (!(0, import_node_fs29.existsSync)(p)) throw new Error(`Claim lockfile not found: ${p}`);
17614
17780
  let parsed;
17615
17781
  try {
17616
- parsed = parseLock(JSON.parse(await (0, import_promises26.readFile)(p, "utf8")));
17782
+ parsed = parseLock(JSON.parse(await (0, import_promises27.readFile)(p, "utf8")));
17617
17783
  } catch {
17618
17784
  throw new Error(`Claim lockfile is malformed: ${p}`);
17619
17785
  }
@@ -17717,20 +17883,20 @@ function matchOrphanStyle(prompt, manifest) {
17717
17883
  return matchStyleByPattern(prompt, manifest);
17718
17884
  }
17719
17885
  async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, claimPaths) {
17720
- const own = import_node_path33.default.resolve(ownManifestPath);
17886
+ const own = import_node_path34.default.resolve(ownManifestPath);
17721
17887
  const siblingManifestPaths = [
17722
17888
  .../* @__PURE__ */ new Set([
17723
17889
  ...workspaceManifestPaths,
17724
- ...claimPaths.map((c) => import_node_path33.default.join(import_node_path33.default.dirname(import_node_path33.default.resolve(c)), "pixelkiln.manifest.json"))
17890
+ ...claimPaths.map((c) => import_node_path34.default.join(import_node_path34.default.dirname(import_node_path34.default.resolve(c)), "pixelkiln.manifest.json"))
17725
17891
  ])
17726
17892
  ];
17727
17893
  const siblings = [];
17728
17894
  for (const siblingManifestPath of siblingManifestPaths) {
17729
- if (import_node_path33.default.resolve(siblingManifestPath) === own) continue;
17730
- if (!(0, import_node_fs28.existsSync)(siblingManifestPath)) continue;
17895
+ if (import_node_path34.default.resolve(siblingManifestPath) === own) continue;
17896
+ if (!(0, import_node_fs29.existsSync)(siblingManifestPath)) continue;
17731
17897
  try {
17732
17898
  const { manifest } = await loadManifest(siblingManifestPath);
17733
- siblings.push({ label: import_node_path33.default.basename(import_node_path33.default.dirname(siblingManifestPath)), manifest });
17899
+ siblings.push({ label: import_node_path34.default.basename(import_node_path34.default.dirname(siblingManifestPath)), manifest });
17734
17900
  } catch {
17735
17901
  }
17736
17902
  }
@@ -17850,8 +18016,8 @@ async function applyTags(provider, decisions, existing, opts = {}) {
17850
18016
  }
17851
18017
 
17852
18018
  // src/pick/salvage-server.ts
17853
- var import_promises27 = require("fs/promises");
17854
- var import_node_path34 = __toESM(require("path"), 1);
18019
+ var import_promises28 = require("fs/promises");
18020
+ var import_node_path35 = __toESM(require("path"), 1);
17855
18021
 
17856
18022
  // src/pick/salvage-sheet.ts
17857
18023
  var escapeHtml3 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
@@ -18017,7 +18183,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
18017
18183
  });
18018
18184
  const html = renderSalvageSheet(orphans, {
18019
18185
  styleId: ctx.styleId,
18020
- importDir: import_node_path34.default.relative(process.cwd(), ctx.importDir) || "."
18186
+ importDir: import_node_path35.default.relative(process.cwd(), ctx.importDir) || "."
18021
18187
  });
18022
18188
  const byId = new Map(orphans.map((o) => [o.id, o]));
18023
18189
  const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
@@ -18073,10 +18239,10 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
18073
18239
  if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
18074
18240
  decodePng(buf);
18075
18241
  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);
18242
+ const rel = import_node_path35.default.join("_salvaged", `${assetId}.png`);
18243
+ const outFile = import_node_path35.default.resolve(ctx.importDir, rel);
18244
+ await (0, import_promises28.mkdir)(import_node_path35.default.dirname(outFile), { recursive: true });
18245
+ await (0, import_promises28.writeFile)(outFile, buf);
18080
18246
  ctx.manifest.assets[assetId] = {
18081
18247
  prompt: orphan.prompt,
18082
18248
  promptByStyle: {},
@@ -18108,7 +18274,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
18108
18274
  sourceUrl: durableSource,
18109
18275
  sourceUrls: durableSource ? [{ url: durableSource }] : [],
18110
18276
  outputs: [{
18111
- path: portableOutputPath(outFile, import_node_path34.default.dirname(ctx.manifestPath)),
18277
+ path: portableOutputPath(outFile, import_node_path35.default.dirname(ctx.manifestPath)),
18112
18278
  sha256: sha256(buf)
18113
18279
  }],
18114
18280
  submittedAt: orphan.createdAt,
@@ -18132,9 +18298,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
18132
18298
  }
18133
18299
  }
18134
18300
  await applyTags(provider, decisions, existingTags, { onProgress: log, characters: characterMembers });
18135
- const raw = JSON.parse(await (0, import_promises27.readFile)(ctx.manifestPath, "utf8"));
18301
+ const raw = JSON.parse(await (0, import_promises28.readFile)(ctx.manifestPath, "utf8"));
18136
18302
  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");
18303
+ await (0, import_promises28.writeFile)(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
18138
18304
  await saveLock(ctx.lockPath, ctx.lock);
18139
18305
  if (characterAssetIds.length) result.characterAssetIds = characterAssetIds;
18140
18306
  return result;
@@ -18377,6 +18543,7 @@ async function workspaceStatus(ws, dir) {
18377
18543
  fontOutputPaths,
18378
18544
  formatCost,
18379
18545
  frameSetFps,
18546
+ galleryContentSecurityPolicy,
18380
18547
  galleryMediaId,
18381
18548
  galleryMediaRoute,
18382
18549
  generateFont,