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/cli.js CHANGED
@@ -67,8 +67,8 @@ function exitCodeFor(err) {
67
67
  }
68
68
 
69
69
  // src/cli/main.ts
70
- import { existsSync as existsSync34 } from "fs";
71
- import { readFile as readFile31 } from "fs/promises";
70
+ import { existsSync as existsSync36 } from "fs";
71
+ import { readFile as readFile32 } from "fs/promises";
72
72
 
73
73
  // src/cli/args.ts
74
74
  import path from "path";
@@ -108,7 +108,8 @@ var VALUE_FLAGS = [
108
108
  "--description",
109
109
  "--weight",
110
110
  "--glyph-px",
111
- "--frames"
111
+ "--frames",
112
+ "--estimate-limit"
112
113
  ];
113
114
  var BOOL_FLAGS = [
114
115
  "--force",
@@ -430,7 +431,8 @@ function parseArgs(argv) {
430
431
  description: get("--description"),
431
432
  weight: get("--weight"),
432
433
  glyphPx: numberOption("--glyph-px", { min: 8, max: 64, integer: true }),
433
- frames: numberOption("--frames", { min: 3, max: 15, integer: true })
434
+ frames: numberOption("--frames", { min: 3, max: 15, integer: true }),
435
+ estimateLimit: numberOption("--estimate-limit", { min: 0, max: 1e3, integer: true })
434
436
  };
435
437
  }
436
438
 
@@ -543,6 +545,7 @@ Options
543
545
  --no-open Do not auto-open the browser (pick, salvage, gallery) or editor (edit)
544
546
  --edit gallery: allow manifest edits from the page (never spends)
545
547
  --no-editor gallery: do not offer or serve the in-browser editor
548
+ --estimate-limit <n> gallery: PixelLab skeleton estimates the page may make (default 10)
546
549
  --tag Also push tags upstream after fetch
547
550
  --refresh fetch: re-download and replace files whose object changed
548
551
  upstream (e.g. edited in PixelLab's editor); no generation
@@ -662,10 +665,10 @@ import { createReadStream } from "fs";
662
665
  function sha256(data) {
663
666
  return createHash("sha256").update(data).digest("hex");
664
667
  }
665
- async function sha256File(path48) {
668
+ async function sha256File(path49) {
666
669
  return new Promise((resolve, reject) => {
667
670
  const hash = createHash("sha256");
668
- const stream = createReadStream(path48);
671
+ const stream = createReadStream(path49);
669
672
  stream.on("data", (chunk2) => hash.update(chunk2));
670
673
  stream.on("error", reject);
671
674
  stream.on("end", () => resolve(hash.digest("hex")));
@@ -5837,6 +5840,10 @@ function parseSkeletonSet(json, file) {
5837
5840
  }
5838
5841
  return result.data;
5839
5842
  }
5843
+ var ESTIMATE_SKELETON_SIZES = [16, 32, 64, 128, 256];
5844
+ function estimateSkeletonSizeProblem(width, height) {
5845
+ return width === height && ESTIMATE_SKELETON_SIZES.includes(width) ? null : `${width}x${height}; estimate-skeleton takes a square image of ${ESTIMATE_SKELETON_SIZES.join(", ")} pixels`;
5846
+ }
5840
5847
  var DEFAULT_SCAFFOLD_FRAMES = 4;
5841
5848
  function scaffoldSkeletonSet(keypoints, frames = DEFAULT_SCAFFOLD_FRAMES) {
5842
5849
  const copy = () => keypoints.map((joint) => ({ ...joint }));
@@ -6156,9 +6163,9 @@ var PixelLabClient = class {
6156
6163
  apiKey;
6157
6164
  http;
6158
6165
  /** Retries transport failures, 429, and 5xx; see http.ts for the policy. */
6159
- async request(path48, init) {
6166
+ async request(path49, init) {
6160
6167
  const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
6161
- const res = await this.http(`${BASE}${path48}`, {
6168
+ const res = await this.http(`${BASE}${path49}`, {
6162
6169
  ...init,
6163
6170
  headers: {
6164
6171
  Authorization: auth,
@@ -6168,13 +6175,13 @@ var PixelLabClient = class {
6168
6175
  });
6169
6176
  const text = await res.text();
6170
6177
  if (!res.ok) {
6171
- throw new PixelLabError(`${init?.method ?? "GET"} ${path48} \u2192 ${res.status}`, res.status, text);
6178
+ throw new PixelLabError(`${init?.method ?? "GET"} ${path49} \u2192 ${res.status}`, res.status, text);
6172
6179
  }
6173
6180
  if (!text) return {};
6174
6181
  try {
6175
6182
  return JSON.parse(text);
6176
6183
  } catch {
6177
- throw new Error(`${init?.method ?? "GET"} ${path48} returned invalid JSON`);
6184
+ throw new Error(`${init?.method ?? "GET"} ${path49} returned invalid JSON`);
6178
6185
  }
6179
6186
  }
6180
6187
  async balance() {
@@ -6566,10 +6573,10 @@ var PixelLabClient = class {
6566
6573
  const palette = args.paletteSwatchBase64 ? { color_image: { type: "base64", base64: args.paletteSwatchBase64, format: "png" }, force_colors: true } : {};
6567
6574
  const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
6568
6575
  const south = args.reference?.south;
6569
- let path48;
6576
+ let path49;
6570
6577
  let body;
6571
6578
  if (args.mode === "v3") {
6572
- path48 = "/create-character-v3";
6579
+ path49 = "/create-character-v3";
6573
6580
  body = {
6574
6581
  description: args.description,
6575
6582
  image_size: imageSize,
@@ -6583,7 +6590,7 @@ var PixelLabClient = class {
6583
6590
  ...args.enhancePrompt ? { enhance_prompt: true } : {}
6584
6591
  };
6585
6592
  } else if (args.mode === "pro-flash") {
6586
- path48 = "/create-character-pro-flash";
6593
+ path49 = "/create-character-pro-flash";
6587
6594
  const traits = args.styleTraits;
6588
6595
  body = {
6589
6596
  description: args.description,
@@ -6607,7 +6614,7 @@ var PixelLabClient = class {
6607
6614
  } : {}
6608
6615
  };
6609
6616
  } else if (args.mode === "pro") {
6610
- path48 = "/create-character-pro";
6617
+ path49 = "/create-character-pro";
6611
6618
  body = {
6612
6619
  description: args.description,
6613
6620
  image_size: imageSize,
@@ -6621,7 +6628,7 @@ var PixelLabClient = class {
6621
6628
  ...!south && args.styleCharacterId ? { style_character_id: args.styleCharacterId } : {}
6622
6629
  };
6623
6630
  } else {
6624
- path48 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
6631
+ path49 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
6625
6632
  body = {
6626
6633
  description: args.description,
6627
6634
  image_size: imageSize,
@@ -6638,8 +6645,8 @@ var PixelLabClient = class {
6638
6645
  ...palette
6639
6646
  };
6640
6647
  }
6641
- const raw = await this.request(path48, { method: "POST", body: JSON.stringify(body) });
6642
- return validateResponse(CharacterSubmitSchema, raw, path48.slice(1));
6648
+ const raw = await this.request(path49, { method: "POST", body: JSON.stringify(body) });
6649
+ return validateResponse(CharacterSubmitSchema, raw, path49.slice(1));
6643
6650
  }
6644
6651
  /** A text edit of an existing character, applied to every direction and stored as a sibling. */
6645
6652
  async createCharacterState(args) {
@@ -9486,9 +9493,9 @@ var RetroDiffusionClient = class {
9486
9493
  if (!response.ok) throw new ProviderError("retrodiffusion", `Retro Diffusion download failed (${response.status})`, { status: response.status });
9487
9494
  return Buffer.from(await response.arrayBuffer());
9488
9495
  }
9489
- async call(path48, init = {}) {
9496
+ async call(path49, init = {}) {
9490
9497
  if (!this.token) throw new Error("RD_API_KEY is not set");
9491
- const response = await this.request(`${this.baseUrl}${path48}`, {
9498
+ const response = await this.request(`${this.baseUrl}${path49}`, {
9492
9499
  ...init,
9493
9500
  headers: {
9494
9501
  "Content-Type": "application/json",
@@ -11793,10 +11800,10 @@ function isSha256Hash(value) {
11793
11800
  function parseCache(value) {
11794
11801
  return HashCacheSchema.parse(value);
11795
11802
  }
11796
- async function loadCache(path48) {
11797
- if (!existsSync10(path48)) return { version: 1, hashes: {} };
11803
+ async function loadCache(path49) {
11804
+ if (!existsSync10(path49)) return { version: 1, hashes: {} };
11798
11805
  try {
11799
- const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile8(path48, "utf8")));
11806
+ const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile8(path49, "utf8")));
11800
11807
  if (!parsed.success) return { version: 1, hashes: {} };
11801
11808
  return {
11802
11809
  version: 1,
@@ -11808,18 +11815,18 @@ async function loadCache(path48) {
11808
11815
  return { version: 1, hashes: {} };
11809
11816
  }
11810
11817
  }
11811
- async function saveCache(path48, cache) {
11818
+ async function saveCache(path49, cache) {
11812
11819
  const sorted = {};
11813
11820
  for (const key of Object.keys(cache.hashes).sort()) {
11814
11821
  const hash = cache.hashes[key];
11815
11822
  if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
11816
11823
  sorted[key] = hash;
11817
11824
  }
11818
- await mkdir5(pathModule.dirname(pathModule.resolve(path48)), { recursive: true });
11819
- const tmp = `${path48}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
11825
+ await mkdir5(pathModule.dirname(pathModule.resolve(path49)), { recursive: true });
11826
+ const tmp = `${path49}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
11820
11827
  try {
11821
11828
  await writeFile5(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
11822
- await rename5(tmp, path48);
11829
+ await rename5(tmp, path49);
11823
11830
  } finally {
11824
11831
  await rm5(tmp, { force: true });
11825
11832
  }
@@ -13823,15 +13830,27 @@ var NewAssetSchema = z6.object({
13823
13830
  /**
13824
13831
  * A controlled regeneration of another asset in the same manifest,
13825
13832
  * instead of a fresh generation. `inpaint` needs a mask upload the
13826
- * gallery does not offer yet, so only `image-to-image` can be created
13827
- * here; hand-edit the manifest for the other modes.
13833
+ * gallery does not offer yet, so only `image-to-image` and
13834
+ * `animate-skeleton` (whose keypoints file the gallery writes first) can
13835
+ * be created here; hand-edit the manifest for the other modes.
13828
13836
  */
13829
- revision: z6.object({
13830
- mode: z6.literal("image-to-image"),
13831
- /** Asset id of the parent this revises; must already exist. */
13832
- from: z6.string().min(1),
13833
- strength: z6.number().min(0).max(1).optional()
13834
- }).strict().optional(),
13837
+ revision: z6.discriminatedUnion("mode", [
13838
+ z6.object({
13839
+ mode: z6.literal("image-to-image"),
13840
+ /** Asset id of the parent this revises; must already exist. */
13841
+ from: z6.string().min(1),
13842
+ strength: z6.number().min(0).max(1).optional()
13843
+ }).strict(),
13844
+ z6.object({
13845
+ mode: z6.literal("animate-skeleton"),
13846
+ from: z6.string().min(1),
13847
+ /** Manifest-relative; checked when the manifest is loaded, like every revision input. */
13848
+ keypointsFile: z6.string().min(1),
13849
+ direction: CharacterDirectionSchema,
13850
+ description: z6.string().min(1).optional(),
13851
+ skeletonTemplate: z6.string().min(1).optional()
13852
+ }).strict()
13853
+ ]).optional(),
13835
13854
  /** A pose or outfit of an existing `character`/`objectPro` base or state. */
13836
13855
  state: NewStateSchema.optional(),
13837
13856
  /** A loop of an existing `character`/`objectPro` base or state. */
@@ -14356,52 +14375,188 @@ async function runEdit(args) {
14356
14375
  }
14357
14376
 
14358
14377
  // src/cli/commands/gallery.ts
14359
- import path32 from "path";
14360
- import { existsSync as existsSync23 } from "fs";
14378
+ import path33 from "path";
14379
+ import { existsSync as existsSync25 } from "fs";
14361
14380
 
14362
14381
  // src/gallery/snapshot.ts
14363
14382
  import { createHash as createHash3 } from "crypto";
14364
- import { existsSync as existsSync19 } from "fs";
14365
- import { readFile as readFile17, stat as stat2 } from "fs/promises";
14366
- import path28 from "path";
14383
+ import { existsSync as existsSync20 } from "fs";
14384
+ import { readFile as readFile18, stat as stat2 } from "fs/promises";
14385
+ import path29 from "path";
14367
14386
 
14368
14387
  // src/gallery/edit.ts
14388
+ import path27 from "path";
14389
+ import { z as z9 } from "zod";
14390
+
14391
+ // src/gallery/skeleton.ts
14392
+ import { existsSync as existsSync18 } from "fs";
14393
+ import { mkdir as mkdir10, readFile as readFile17, rm as rm9, writeFile as writeFile12 } from "fs/promises";
14369
14394
  import path26 from "path";
14370
14395
  import { z as z8 } from "zod";
14371
- var Base64 = z8.string().regex(/^[A-Za-z0-9+/]*={0,2}$/).max(Math.ceil(MAX_HAND_EDIT_BYTES / 3) * 4);
14372
- var HandEditRequestSchema = z8.discriminatedUnion("action", [
14373
- z8.object({
14374
- action: z8.literal("start-edit"),
14375
- project: z8.string().min(1).optional(),
14376
- assetId: z8.string().min(1),
14377
- styleId: z8.string().min(1),
14378
- expectedSha256: z8.string().regex(/^[0-9a-f]{64}$/),
14396
+ var SkeletonEstimateRequestSchema = z8.object({
14397
+ project: z8.string().min(1).optional(),
14398
+ styleId: z8.string().min(1),
14399
+ assetId: z8.string().min(1),
14400
+ frames: z8.number().int().min(3).max(15).optional()
14401
+ }).strict();
14402
+ var SkeletonAnimationRequestSchema = z8.object({
14403
+ action: z8.literal("create-skeleton-animation"),
14404
+ project: z8.string().min(1).optional(),
14405
+ expectedSha256: z8.string().regex(/^[0-9a-f]{64}$/),
14406
+ styleId: z8.string().min(1),
14407
+ /** The sprite the animation poses. */
14408
+ from: z8.string().min(1),
14409
+ assetId: z8.string().min(1).regex(/^[^/\\]+$/, "asset ids cannot contain slashes"),
14410
+ /** The motion, sent as PixelLab's `action`. */
14411
+ prompt: z8.string().min(1),
14412
+ direction: CharacterDirectionSchema,
14413
+ description: z8.string().min(1).optional(),
14414
+ skeletonTemplate: z8.string().min(1).optional(),
14415
+ /** Manifest-relative JSON path inside the project. */
14416
+ keypointsFile: z8.string().min(1),
14417
+ /** Poses to write to `keypointsFile`; omitted to use a file that already exists. */
14418
+ set: SkeletonSetSchema.optional(),
14419
+ /** Replace an existing `keypointsFile` with `set`. */
14420
+ overwrite: z8.boolean().optional()
14421
+ }).strict();
14422
+ var DEFAULT_ESTIMATE_LIMIT = 10;
14423
+ function skeletonSourceFile(spec) {
14424
+ if (spec.source) return path26.resolve(spec.root, spec.source);
14425
+ if (spec.quality && existsSync18(spec.quality.outFile)) return spec.quality.outFile;
14426
+ return spec.outFile;
14427
+ }
14428
+ function createGallerySkeletonHandlers(opts) {
14429
+ const log2 = opts.onProgress ?? (() => {
14430
+ });
14431
+ const limit = opts.limit ?? DEFAULT_ESTIMATE_LIMIT;
14432
+ let used = 0;
14433
+ const spent = {};
14434
+ const status = () => ({ used, limit, spent: { ...spent } });
14435
+ return {
14436
+ status,
14437
+ async estimate(body) {
14438
+ const parsed = SkeletonEstimateRequestSchema.safeParse(body);
14439
+ if (!parsed.success) throw new ManifestEditError(`invalid estimate request: ${parsed.error.issues[0]?.message}`);
14440
+ const request = parsed.data;
14441
+ const ctx = await opts.loadProject(request.project);
14442
+ const key = lockKey(request.styleId, request.assetId);
14443
+ const spec = ctx.specs.find((candidate) => lockKey(candidate.styleId, candidate.assetId) === key);
14444
+ if (!spec) throw new ManifestEditError(`"${key}" is not declared by the manifest`);
14445
+ const file = skeletonSourceFile(spec);
14446
+ if (!existsSync18(file)) throw new ManifestEditError(`${key} has no image on disk to estimate from`);
14447
+ const bytes = await readFile17(file);
14448
+ const metadata = imageMetadata(bytes);
14449
+ if (!metadata) throw new ManifestEditError(`${path26.basename(file)} is not a readable PNG or JPEG`);
14450
+ const problem = estimateSkeletonSizeProblem(metadata.width, metadata.height);
14451
+ if (problem) throw new ManifestEditError(`${key} is ${problem}`);
14452
+ if (used >= limit) {
14453
+ throw Object.assign(
14454
+ new Error(`this gallery has made its ${limit} skeleton estimate${limit === 1 ? "" : "s"}; restart it with --estimate-limit to allow more`),
14455
+ { status: 429 }
14456
+ );
14457
+ }
14458
+ const client2 = (opts.client ?? clientFromEnv)();
14459
+ used++;
14460
+ const res = await client2.estimateSkeleton({ image: { base64: bytes.toString("base64"), format: metadata.format } });
14461
+ const billed = billedAmount(res.usage);
14462
+ if (billed) spent[billed.unit] = (spent[billed.unit] ?? 0) + billed.amount;
14463
+ log2(` estimated a skeleton for ${key} (PixelLab estimate-skeleton, ${used}/${limit} this session${billed ? `, billed ${billed.amount} ${billed.unit}` : ""})`);
14464
+ return {
14465
+ set: scaffoldSkeletonSet(res.keypoints, request.frames ?? DEFAULT_SCAFFOLD_FRAMES),
14466
+ source: path26.relative(ctx.loaded.root, file).split(path26.sep).join("/"),
14467
+ session: status()
14468
+ };
14469
+ }
14470
+ };
14471
+ }
14472
+ function billedAmount(usage) {
14473
+ if (!usage || typeof usage !== "object") return null;
14474
+ const { type } = usage;
14475
+ if (type !== "usd" && type !== "generations") return null;
14476
+ const amount = usage[type];
14477
+ return typeof amount === "number" && Number.isFinite(amount) ? { unit: type, amount } : null;
14478
+ }
14479
+ async function createSkeletonAnimation(manifestPath, request) {
14480
+ const root = path26.dirname(path26.resolve(manifestPath));
14481
+ const file = path26.resolve(root, request.keypointsFile);
14482
+ const relative = path26.relative(root, file);
14483
+ if (!request.keypointsFile.endsWith(".json") || relative.startsWith("..") || path26.isAbsolute(relative)) {
14484
+ throw new ManifestEditError("the keypoints file must be a .json path inside the project");
14485
+ }
14486
+ const existed = existsSync18(file);
14487
+ let previous = null;
14488
+ if (request.set) {
14489
+ if (existed && !request.overwrite) {
14490
+ throw new ManifestEditError(`${request.keypointsFile} already exists; choose another path, or replace it`);
14491
+ }
14492
+ if (existed) previous = await readFile17(file);
14493
+ await mkdir10(path26.dirname(file), { recursive: true });
14494
+ await writeFile12(file, JSON.stringify(request.set, null, 2) + "\n");
14495
+ } else if (!existed) {
14496
+ throw new ManifestEditError(`${request.keypointsFile} does not exist; paste poses or estimate them from the sprite`);
14497
+ }
14498
+ try {
14499
+ await applyManifestEdit(manifestPath, {
14500
+ action: "add-asset",
14501
+ assetId: request.assetId,
14502
+ expectedSha256: request.expectedSha256,
14503
+ asset: {
14504
+ prompt: request.prompt,
14505
+ styles: [request.styleId],
14506
+ revision: {
14507
+ mode: "animate-skeleton",
14508
+ from: request.from,
14509
+ keypointsFile: relative.split(path26.sep).join("/"),
14510
+ direction: request.direction,
14511
+ ...request.description ? { description: request.description } : {},
14512
+ ...request.skeletonTemplate ? { skeletonTemplate: request.skeletonTemplate } : {}
14513
+ }
14514
+ }
14515
+ });
14516
+ } catch (error) {
14517
+ if (request.set) {
14518
+ if (previous) await writeFile12(file, previous);
14519
+ else await rm9(file, { force: true });
14520
+ }
14521
+ throw error;
14522
+ }
14523
+ }
14524
+
14525
+ // src/gallery/edit.ts
14526
+ var Base64 = z9.string().regex(/^[A-Za-z0-9+/]*={0,2}$/).max(Math.ceil(MAX_HAND_EDIT_BYTES / 3) * 4);
14527
+ var HandEditRequestSchema = z9.discriminatedUnion("action", [
14528
+ z9.object({
14529
+ action: z9.literal("start-edit"),
14530
+ project: z9.string().min(1).optional(),
14531
+ assetId: z9.string().min(1),
14532
+ styleId: z9.string().min(1),
14533
+ expectedSha256: z9.string().regex(/^[0-9a-f]{64}$/),
14379
14534
  /** Also launch the author's editor on the file. */
14380
- open: z8.boolean().optional()
14535
+ open: z9.boolean().optional()
14381
14536
  }).strict(),
14382
- z8.object({
14383
- action: z8.literal("detach-edit"),
14384
- project: z8.string().min(1).optional(),
14385
- assetId: z8.string().min(1),
14386
- styleId: z8.string().min(1),
14387
- expectedSha256: z8.string().regex(/^[0-9a-f]{64}$/)
14537
+ z9.object({
14538
+ action: z9.literal("detach-edit"),
14539
+ project: z9.string().min(1).optional(),
14540
+ assetId: z9.string().min(1),
14541
+ styleId: z9.string().min(1),
14542
+ expectedSha256: z9.string().regex(/^[0-9a-f]{64}$/)
14388
14543
  }).strict(),
14389
14544
  /** From the in-browser editor: the page holds the bytes, the server validates and writes. */
14390
- z8.object({
14391
- action: z8.literal("save-edit"),
14392
- project: z8.string().min(1).optional(),
14393
- assetId: z8.string().min(1),
14394
- styleId: z8.string().min(1),
14395
- expectedSha256: z8.string().regex(/^[0-9a-f]{64}$/),
14545
+ z9.object({
14546
+ action: z9.literal("save-edit"),
14547
+ project: z9.string().min(1).optional(),
14548
+ assetId: z9.string().min(1),
14549
+ styleId: z9.string().min(1),
14550
+ expectedSha256: z9.string().regex(/^[0-9a-f]{64}$/),
14396
14551
  /** The flattened image; for a frame set, `frames` instead. */
14397
14552
  png: Base64.min(1).optional(),
14398
14553
  /** Every frame of a set, tagged with the role it was opened under (null for one added in the editor). */
14399
- frames: z8.array(z8.object({ role: z8.string().min(1).nullable(), png: Base64.min(1) }).strict()).min(1).max(256).optional(),
14554
+ frames: z9.array(z9.object({ role: z9.string().min(1).nullable(), png: Base64.min(1) }).strict()).min(1).max(256).optional(),
14400
14555
  /** The editor's layered project file, kept beside the edit. */
14401
14556
  pxo: Base64.optional(),
14402
14557
  /** Editor identity from the bridge's ready message. */
14403
- editor: z8.string().min(1).max(200),
14404
- protocol: z8.number().int().positive()
14558
+ editor: z9.string().min(1).max(200),
14559
+ protocol: z9.number().int().positive()
14405
14560
  }).strict()
14406
14561
  ]);
14407
14562
  function createGalleryEditHandler(opts) {
@@ -14442,6 +14597,23 @@ function createGalleryEditHandler(opts) {
14442
14597
  }
14443
14598
  return opts.reload();
14444
14599
  }
14600
+ const skeleton = SkeletonAnimationRequestSchema.safeParse(body);
14601
+ if (skeleton.success) {
14602
+ let manifestPath2;
14603
+ try {
14604
+ manifestPath2 = await opts.manifestFor(skeleton.data.project);
14605
+ } catch (error) {
14606
+ throw new ManifestEditError(error instanceof Error ? error.message : String(error));
14607
+ }
14608
+ await createSkeletonAnimation(manifestPath2, skeleton.data);
14609
+ log2(` manifest edited: added skeleton animation ${skeleton.data.assetId} (${skeleton.data.keypointsFile})`);
14610
+ return opts.reload();
14611
+ }
14612
+ if (body?.action === "create-skeleton-animation") {
14613
+ throw new ManifestEditError(
14614
+ "invalid skeleton animation: " + skeleton.error.issues.slice(0, 3).map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")
14615
+ );
14616
+ }
14445
14617
  const parsed = ManifestEditSchema.safeParse(body);
14446
14618
  if (!parsed.success) {
14447
14619
  throw new ManifestEditError(
@@ -14456,7 +14628,7 @@ function createGalleryEditHandler(opts) {
14456
14628
  }
14457
14629
  const result = await applyManifestEdit(manifestPath, parsed.data);
14458
14630
  if (result.changed) {
14459
- const relative = path26.relative(process.cwd(), result.manifestPath);
14631
+ const relative = path27.relative(process.cwd(), result.manifestPath);
14460
14632
  const shown = !relative || relative.startsWith("..") ? result.manifestPath : relative;
14461
14633
  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;
14462
14634
  log2(` manifest edited: ${parsed.data.action === "add-asset" ? "added" : "changed"} ${subject} in ${shown}`);
@@ -14466,8 +14638,8 @@ function createGalleryEditHandler(opts) {
14466
14638
  }
14467
14639
 
14468
14640
  // src/pipeline/history.ts
14469
- import { existsSync as existsSync18 } from "fs";
14470
- import path27 from "path";
14641
+ import { existsSync as existsSync19 } from "fs";
14642
+ import path28 from "path";
14471
14643
  var HISTORY_ENV = "PIXELKILN_HISTORY";
14472
14644
  function historyLimit(manifest, env = process.env) {
14473
14645
  if (manifest?.history !== void 0) return manifest.history;
@@ -14554,10 +14726,10 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
14554
14726
  }
14555
14727
  for (let i = 0; i < entry.outputs.length; i++) {
14556
14728
  const file = currentEntryOutputPath(entry, spec, i);
14557
- if (!existsSync18(file)) continue;
14729
+ if (!existsSync19(file)) continue;
14558
14730
  if (await sha256File(file) !== entry.outputs[i].sha256 && !opts.force) {
14559
14731
  throw new OverwriteRefusedError(
14560
- `${path27.relative(process.cwd(), file)} was changed after download; keep it with pixelkiln edit, or pass --force to replace it`,
14732
+ `${path28.relative(process.cwd(), file)} was changed after download; keep it with pixelkiln edit, or pass --force to replace it`,
14561
14733
  [file]
14562
14734
  );
14563
14735
  }
@@ -14610,7 +14782,7 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
14610
14782
  // src/gallery/snapshot.ts
14611
14783
  var PROJECT_FILE_TYPE = "application/octet-stream";
14612
14784
  function galleryMediaId(absolutePath) {
14613
- return createHash3("sha256").update(path28.resolve(absolutePath)).digest("hex").slice(0, 24);
14785
+ return createHash3("sha256").update(path29.resolve(absolutePath)).digest("hex").slice(0, 24);
14614
14786
  }
14615
14787
  function galleryMediaRoute(id) {
14616
14788
  return `/media/${id}`;
@@ -14629,9 +14801,9 @@ function describeHistory(media, entry, cacheDir) {
14629
14801
  return (entry.history ?? []).map((generation, i) => {
14630
14802
  const outputs = generation.outputs.map((output) => {
14631
14803
  const mediaType = output.mediaType ?? MediaType.PNG;
14632
- const file = path28.join(cacheDir, cacheFileName(output.sha256, mediaType));
14804
+ const file = path29.join(cacheDir, cacheFileName(output.sha256, mediaType));
14633
14805
  let url = null;
14634
- if (existsSync19(file)) {
14806
+ if (existsSync20(file)) {
14635
14807
  const id = galleryMediaId(file);
14636
14808
  media.set(id, { path: file, contentType: mediaType });
14637
14809
  url = `${galleryMediaRoute(id)}?v=${output.sha256.slice(0, 24)}`;
@@ -14663,7 +14835,7 @@ function describeHistory(media, entry, cacheDir) {
14663
14835
  var MAX_COMPARED_BYTES = 8 * 1024 * 1024;
14664
14836
  async function samePixels(a, b) {
14665
14837
  try {
14666
- const [left, right] = await Promise.all([readFile17(a), readFile17(b)]);
14838
+ const [left, right] = await Promise.all([readFile18(a), readFile18(b)]);
14667
14839
  if (left.length > MAX_COMPARED_BYTES || right.length > MAX_COMPARED_BYTES) return false;
14668
14840
  const x = decodePng(left);
14669
14841
  const y = decodePng(right);
@@ -14761,10 +14933,10 @@ async function describeOutput(media, root, absolutePath, recorded = {}) {
14761
14933
  async function describeQuality(media, root, inspection) {
14762
14934
  const outputPaths = inspection.outputs ?? [inspection.output];
14763
14935
  const outputs = await Promise.all(
14764
- outputPaths.map((file) => describeOutput(media, root, path28.resolve(root, file)))
14936
+ outputPaths.map((file) => describeOutput(media, root, path29.resolve(root, file)))
14765
14937
  );
14766
- const recordPath = path28.resolve(root, inspection.record);
14767
- const recordExists = existsSync19(recordPath);
14938
+ const recordPath = path29.resolve(root, inspection.record);
14939
+ const recordExists = existsSync20(recordPath);
14768
14940
  let options = null;
14769
14941
  let check = null;
14770
14942
  if (recordExists) {
@@ -14783,7 +14955,7 @@ async function describeQuality(media, root, inspection) {
14783
14955
  return {
14784
14956
  state: inspection.state,
14785
14957
  reason: inspection.reason,
14786
- output: portableOutputPath(path28.resolve(root, inspection.output), root),
14958
+ output: portableOutputPath(path29.resolve(root, inspection.output), root),
14787
14959
  record: portableOutputPath(recordPath, root),
14788
14960
  recordExists,
14789
14961
  outputs,
@@ -14821,9 +14993,9 @@ async function buildGallerySnapshot(opts) {
14821
14993
  const media = /* @__PURE__ */ new Map();
14822
14994
  const plan = await buildPlan(specs, lock);
14823
14995
  const items = [];
14824
- const manifestText = await readFile17(loaded.path, "utf8");
14996
+ const manifestText = await readFile18(loaded.path, "utf8");
14825
14997
  const manifestSha256 = sha256(manifestText);
14826
- const cacheDir = path28.resolve(path28.dirname(opts.lockPath), ".pixelkiln", "cache");
14998
+ const cacheDir = path29.resolve(path29.dirname(opts.lockPath), ".pixelkiln", "cache");
14827
14999
  let rawStyles = {};
14828
15000
  try {
14829
15001
  const raw = JSON.parse(manifestText);
@@ -14847,12 +15019,12 @@ async function buildGallerySnapshot(opts) {
14847
15019
  )
14848
15020
  );
14849
15021
  if (spec.source) {
14850
- const editPath = path28.resolve(root, spec.source);
15022
+ const editPath = path29.resolve(root, spec.source);
14851
15023
  const memberCount = sourceIsStem(spec.source, entry, root) ? entry.outputs.length : 1;
14852
15024
  const editShas = await Promise.all(
14853
15025
  Array.from({ length: memberCount }, async (_, index) => {
14854
15026
  const file = sourceOutputPath(spec.source, entry, index, root);
14855
- return { file, sha256: existsSync19(file) ? await sha256File(file) : null };
15027
+ return { file, sha256: existsSync20(file) ? await sha256File(file) : null };
14856
15028
  })
14857
15029
  );
14858
15030
  edits = await Promise.all(editShas.map(({ file, sha256: editSha }) => describeOutput(media, root, file, { sha256: editSha })));
@@ -14860,7 +15032,7 @@ async function buildGallerySnapshot(opts) {
14860
15032
  const companion = await readHandEditCompanion(editPath);
14861
15033
  if (companion) {
14862
15034
  const projectPath = handEditProjectPath(editPath);
14863
- const project = companion.project && existsSync19(projectPath) ? await fileInfo(projectPath) : null;
15035
+ const project = companion.project && existsSync20(projectPath) ? await fileInfo(projectPath) : null;
14864
15036
  let projectUrl = null;
14865
15037
  if (project) {
14866
15038
  const id = galleryMediaId(projectPath);
@@ -14896,8 +15068,8 @@ async function buildGallerySnapshot(opts) {
14896
15068
  editStatus = edits.some((member) => !member.exists) ? "missing" : editChanged.every((changed) => !changed) ? "same" : regenerated ? "regenerated-since" : "edited";
14897
15069
  }
14898
15070
  } else if (spec.source) {
14899
- outputs = [await describeOutput(media, root, path28.resolve(root, spec.source))];
14900
- } else if (!entry && existsSync19(spec.outFile)) {
15071
+ outputs = [await describeOutput(media, root, path29.resolve(root, spec.source))];
15072
+ } else if (!entry && existsSync20(spec.outFile)) {
14901
15073
  outputs = [await describeOutput(media, root, spec.outFile)];
14902
15074
  } else {
14903
15075
  outputs = [];
@@ -14938,6 +15110,10 @@ async function buildGallerySnapshot(opts) {
14938
15110
  revision: entry?.revision ?? null,
14939
15111
  revisionParentKey: spec.revision ? lockKey(spec.styleId, spec.revision.sourceAssetId) : entry?.revision ? lockKey(spec.styleId, entry.revision.sourceAssetId) : null,
14940
15112
  mirrorOfKey: spec.mirror ? lockKey(spec.styleId, spec.mirror.sourceAssetId) : entry?.mirror ? lockKey(spec.styleId, entry.mirror.sourceAssetId) : null,
15113
+ skeleton: spec.revision?.mode === "animate-skeleton" && spec.revision.keypointsFile ? {
15114
+ keypointsFile: portableOutputPath(spec.revision.keypointsFile, root),
15115
+ set: spec.revision.skeleton ?? null
15116
+ } : null,
14941
15117
  character: describeCharacter(spec, entry),
14942
15118
  outputs,
14943
15119
  quality,
@@ -15002,6 +15178,7 @@ async function buildGallerySnapshot(opts) {
15002
15178
  revision: entry.revision,
15003
15179
  revisionParentKey: entry.revision ? lockKey(entry.styleId, entry.revision.sourceAssetId) : null,
15004
15180
  mirrorOfKey: entry.mirror ? lockKey(entry.styleId, entry.mirror.sourceAssetId) : null,
15181
+ skeleton: null,
15005
15182
  character: describeCharacter(void 0, entry),
15006
15183
  outputs,
15007
15184
  quality: null,
@@ -15092,7 +15269,7 @@ async function buildGallerySnapshot(opts) {
15092
15269
  id: loaded.manifest.name,
15093
15270
  name: loaded.manifest.name,
15094
15271
  manifest: loaded.path,
15095
- lock: path28.resolve(opts.lockPath),
15272
+ lock: path29.resolve(opts.lockPath),
15096
15273
  root,
15097
15274
  provider: loaded.manifest.provider,
15098
15275
  account: null,
@@ -15118,7 +15295,7 @@ async function buildGallerySnapshot(opts) {
15118
15295
  return { snapshot, media };
15119
15296
  }
15120
15297
  async function buildWorkspaceGallerySnapshot(opts) {
15121
- const dir = path28.dirname(path28.resolve(opts.workspacePath));
15298
+ const dir = path29.dirname(path29.resolve(opts.workspacePath));
15122
15299
  const media = /* @__PURE__ */ new Map();
15123
15300
  const projects = [];
15124
15301
  const items = [];
@@ -15161,11 +15338,11 @@ async function buildWorkspaceGallerySnapshot(opts) {
15161
15338
  id: project.id,
15162
15339
  name: loaded.manifest.name,
15163
15340
  manifest: manifestPath,
15164
- lock: path28.resolve(lockPath),
15341
+ lock: path29.resolve(lockPath),
15165
15342
  root: loaded.root,
15166
15343
  provider: loaded.manifest.provider,
15167
15344
  account: project.account ?? null,
15168
- manifestSha256: manifestSha256 ?? sha256(await readFile17(manifestPath, "utf8")),
15345
+ manifestSha256: manifestSha256 ?? sha256(await readFile18(manifestPath, "utf8")),
15169
15346
  historyLimit: historyLimit(loaded.manifest),
15170
15347
  entries: Object.keys(lock.entries).length,
15171
15348
  items: projectItems.length,
@@ -15177,8 +15354,8 @@ async function buildWorkspaceGallerySnapshot(opts) {
15177
15354
  id: project.id,
15178
15355
  name: project.id,
15179
15356
  manifest: manifestPath,
15180
- lock: path28.resolve(lockPath),
15181
- root: path28.dirname(manifestPath),
15357
+ lock: path29.resolve(lockPath),
15358
+ root: path29.dirname(manifestPath),
15182
15359
  provider: project.provider,
15183
15360
  account: project.account ?? null,
15184
15361
  manifestSha256: null,
@@ -15205,7 +15382,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
15205
15382
  version: 1,
15206
15383
  generatedAt: (opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
15207
15384
  project: null,
15208
- workspace: { path: path28.resolve(opts.workspacePath), dir, projects },
15385
+ workspace: { path: path29.resolve(opts.workspacePath), dir, projects },
15209
15386
  filter,
15210
15387
  totals: {
15211
15388
  entries: projects.reduce((sum, project) => sum + project.entries, 0),
@@ -15224,16 +15401,51 @@ async function buildWorkspaceGallerySnapshot(opts) {
15224
15401
  // src/gallery/server.ts
15225
15402
  import { randomBytes } from "crypto";
15226
15403
  import { createServer as createServer2 } from "http";
15227
- import { readFile as readFile18 } from "fs/promises";
15404
+ import { readFile as readFile19 } from "fs/promises";
15405
+
15406
+ // src/gallery/page.ts
15407
+ import { existsSync as existsSync21, readFileSync as readFileSync4 } from "fs";
15408
+
15409
+ // src/gallery/client-bundle.ts
15410
+ import { createRequire } from "module";
15411
+ import { fileURLToPath } from "url";
15412
+ function bundleGalleryClient(entry = fileURLToPath(new URL("./client/src/main.ts", import.meta.url))) {
15413
+ const esbuild = createRequire(import.meta.url)("esbuild");
15414
+ const result = esbuild.buildSync({
15415
+ entryPoints: [entry],
15416
+ bundle: true,
15417
+ format: "iife",
15418
+ target: "es2020",
15419
+ platform: "browser",
15420
+ write: false,
15421
+ legalComments: "none",
15422
+ charset: "utf8"
15423
+ });
15424
+ return result.outputFiles[0].text;
15425
+ }
15228
15426
 
15229
15427
  // src/gallery/page.ts
15230
- import { readFileSync as readFileSync4 } from "fs";
15428
+ function galleryContentSecurityPolicy(nonce) {
15429
+ return [
15430
+ "default-src 'self'",
15431
+ `script-src 'nonce-${nonce}'`,
15432
+ `style-src 'nonce-${nonce}'`,
15433
+ "img-src 'self' data: blob:",
15434
+ "frame-src 'self'",
15435
+ "connect-src 'self'",
15436
+ "object-src 'none'",
15437
+ "base-uri 'none'",
15438
+ "form-action 'none'",
15439
+ "frame-ancestors 'none'"
15440
+ ].join("; ");
15441
+ }
15231
15442
  var CLIENT_DIR = new URL("./client/", import.meta.url);
15232
15443
  var client;
15233
15444
  function clientAssets() {
15445
+ const prebuilt = new URL("gallery.js", CLIENT_DIR);
15234
15446
  client ??= {
15235
15447
  css: readFileSync4(new URL("gallery.css", CLIENT_DIR), "utf8"),
15236
- js: readFileSync4(new URL("gallery.js", CLIENT_DIR), "utf8")
15448
+ js: existsSync21(prebuilt) ? readFileSync4(prebuilt, "utf8") : bundleGalleryClient()
15237
15449
  };
15238
15450
  return client;
15239
15451
  }
@@ -15245,6 +15457,7 @@ function renderGallery(snapshot, opts = {}) {
15245
15457
  const editor = JSON.stringify(Boolean(opts.editor && opts.session));
15246
15458
  const title = `${snapshot.project?.name ?? "workspace"} | pixelkiln`;
15247
15459
  const { css, js } = clientAssets();
15460
+ const nonce = opts.nonce ? ` nonce="${escapeHtml2(opts.nonce)}"` : "";
15248
15461
  return `<!doctype html>
15249
15462
  <html lang="en">
15250
15463
  <head>
@@ -15252,7 +15465,7 @@ function renderGallery(snapshot, opts = {}) {
15252
15465
  <meta name="viewport" content="width=device-width, initial-scale=1">
15253
15466
  <title>${escapeHtml2(title)}</title>
15254
15467
  <link rel="icon" href="data:,">
15255
- <style>
15468
+ <style${nonce}>
15256
15469
  ${css}</style>
15257
15470
  </head>
15258
15471
  <body>
@@ -15294,7 +15507,7 @@ ${css}</style>
15294
15507
  <div id="drawer-host"></div>
15295
15508
  <div id="dialog-host"></div>
15296
15509
  <datalist id="view-options"><option value="low top-down"><option value="high top-down"><option value="side"><option value="sidescroller"></datalist>
15297
- <script>
15510
+ <script${nonce}>
15298
15511
  const INITIAL = ${data};
15299
15512
  const SESSION = ${session};
15300
15513
  const EDITABLE = ${editable};
@@ -15334,7 +15547,7 @@ async function readJsonBody(req) {
15334
15547
  async function serveGallery(opts) {
15335
15548
  const log2 = opts.onProgress ?? (() => {
15336
15549
  });
15337
- const session = opts.edit || opts.generate || opts.editor ? randomBytes(16).toString("hex") : null;
15550
+ const session = opts.edit || opts.generate || opts.editor || opts.skeleton ? randomBytes(16).toString("hex") : null;
15338
15551
  let media = /* @__PURE__ */ new Map();
15339
15552
  let loading = null;
15340
15553
  const reviewAssets = /* @__PURE__ */ new Map();
@@ -15409,6 +15622,17 @@ async function serveGallery(opts) {
15409
15622
  }
15410
15623
  return;
15411
15624
  }
15625
+ if (req.method === "POST" && url2.pathname === "/api/skeleton/estimate") {
15626
+ if (!opts.skeleton || !session) return fail(405, "skeleton estimates need the gallery started with --edit");
15627
+ try {
15628
+ const body = await guardedBody("skeleton estimates");
15629
+ if (body === void 0) return;
15630
+ json(200, await opts.skeleton.estimate(body));
15631
+ } catch (err) {
15632
+ reportError("skeleton estimate", err);
15633
+ }
15634
+ return;
15635
+ }
15412
15636
  if (req.method === "POST" && url2.pathname === "/api/editor/install") {
15413
15637
  if (!opts.editor || !session) return fail(405, "the in-browser editor is off for this gallery");
15414
15638
  try {
@@ -15465,7 +15689,7 @@ async function serveGallery(opts) {
15465
15689
  const asset = reviewAssets.get(jobId)?.get(url2.pathname);
15466
15690
  if (!asset) return fail(404, "no such review asset");
15467
15691
  try {
15468
- const bytes = await readFile18(asset.path);
15692
+ const bytes = await readFile19(asset.path);
15469
15693
  res.writeHead(200, {
15470
15694
  "Content-Type": asset.contentType,
15471
15695
  "Cache-Control": "no-store",
@@ -15479,6 +15703,10 @@ async function serveGallery(opts) {
15479
15703
  }
15480
15704
  }
15481
15705
  if (req.method !== "GET" && req.method !== "HEAD") return fail(405, "the gallery is read-only");
15706
+ if (url2.pathname === "/api/skeleton") {
15707
+ if (!opts.skeleton) return fail(404, "skeleton estimates need the gallery started with --edit");
15708
+ return json(200, opts.skeleton.status());
15709
+ }
15482
15710
  if (url2.pathname === "/api/jobs") {
15483
15711
  if (!opts.generate) return fail(404, "generation is off");
15484
15712
  return json(200, opts.generate.status());
@@ -15512,12 +15740,15 @@ async function serveGallery(opts) {
15512
15740
  try {
15513
15741
  if (url2.pathname === "/") {
15514
15742
  const { snapshot } = await load();
15743
+ const nonce = randomBytes(16).toString("base64");
15515
15744
  res.writeHead(200, {
15516
15745
  "Content-Type": "text/html; charset=utf-8",
15517
15746
  "Cache-Control": "no-store",
15518
- "X-Content-Type-Options": "nosniff"
15747
+ "X-Content-Type-Options": "nosniff",
15748
+ "Content-Security-Policy": galleryContentSecurityPolicy(nonce)
15519
15749
  });
15520
15750
  res.end(renderGallery(snapshot, {
15751
+ nonce,
15521
15752
  ...session ? { session } : {},
15522
15753
  editable: Boolean(opts.edit),
15523
15754
  generation: Boolean(opts.generate),
@@ -15541,7 +15772,7 @@ async function serveGallery(opts) {
15541
15772
  if (!asset) return fail(404, "no such gallery media");
15542
15773
  let bytes;
15543
15774
  try {
15544
- bytes = await readFile18(asset.path);
15775
+ bytes = await readFile19(asset.path);
15545
15776
  } catch {
15546
15777
  return fail(404, "gallery media is no longer on disk; refresh the page");
15547
15778
  }
@@ -15585,7 +15816,7 @@ async function serveGallery(opts) {
15585
15816
 
15586
15817
  // src/gallery/generate.ts
15587
15818
  import { randomBytes as randomBytes2 } from "crypto";
15588
- import { z as z9 } from "zod";
15819
+ import { z as z10 } from "zod";
15589
15820
 
15590
15821
  // src/pipeline/poll.ts
15591
15822
  var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -15996,13 +16227,13 @@ function ownedReferenceImageId(spec, lock, providerId) {
15996
16227
  }
15997
16228
 
15998
16229
  // src/pick/server.ts
15999
- import { existsSync as existsSync21 } from "fs";
16000
- import { readFile as readFile20 } from "fs/promises";
16230
+ import { existsSync as existsSync23 } from "fs";
16231
+ import { readFile as readFile21 } from "fs/promises";
16001
16232
 
16002
16233
  // src/pipeline/init.ts
16003
- import { readdir, readFile as readFile19, writeFile as writeFile12 } from "fs/promises";
16004
- import { existsSync as existsSync20 } from "fs";
16005
- import path29 from "path";
16234
+ import { readdir, readFile as readFile20, writeFile as writeFile13 } from "fs/promises";
16235
+ import { existsSync as existsSync22 } from "fs";
16236
+ import path30 from "path";
16006
16237
  function pngSize(buf) {
16007
16238
  if (buf.length < 24) return null;
16008
16239
  const isPng = buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834;
@@ -16016,7 +16247,7 @@ function slugify(value) {
16016
16247
  async function walk(dir, exclude) {
16017
16248
  const out = [];
16018
16249
  for (const entry of await readdir(dir, { withFileTypes: true })) {
16019
- const full = path29.join(dir, entry.name);
16250
+ const full = path30.join(dir, entry.name);
16020
16251
  if (exclude.some((x) => full.includes(x))) continue;
16021
16252
  if (entry.isDirectory()) out.push(...await walk(full, exclude));
16022
16253
  else if (entry.name.toLowerCase().endsWith(".png")) out.push(full);
@@ -16030,14 +16261,14 @@ async function scanAssets(root, opts = {}) {
16030
16261
  const skipped = [];
16031
16262
  const seen = /* @__PURE__ */ new Map();
16032
16263
  for (const file of files.sort()) {
16033
- const size = pngSize(await readFile19(file));
16264
+ const size = pngSize(await readFile20(file));
16034
16265
  if (!size) {
16035
16266
  skipped.push(`${file} (not a readable PNG)`);
16036
16267
  continue;
16037
16268
  }
16038
- const rel = path29.relative(root, file);
16039
- const category = path29.dirname(rel) === "." ? "" : path29.dirname(rel);
16040
- let id = slugify(path29.basename(rel));
16269
+ const rel = path30.relative(root, file);
16270
+ const category = path30.dirname(rel) === "." ? "" : path30.dirname(rel);
16271
+ let id = slugify(path30.basename(rel));
16041
16272
  if (seen.has(id)) {
16042
16273
  const n = seen.get(id) + 1;
16043
16274
  seen.set(id, n);
@@ -16095,10 +16326,10 @@ function mode(values) {
16095
16326
  return [...counts.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? 64;
16096
16327
  }
16097
16328
  async function writeManifestFile(target, manifest) {
16098
- if (existsSync20(target)) {
16329
+ if (existsSync22(target)) {
16099
16330
  throw new Error(`${target} already exists; refusing to overwrite an existing manifest.`);
16100
16331
  }
16101
- await writeFile12(target, JSON.stringify(manifest, null, 2) + "\n");
16332
+ await writeFile13(target, JSON.stringify(manifest, null, 2) + "\n");
16102
16333
  }
16103
16334
 
16104
16335
  // src/pick/sheet.ts
@@ -16511,13 +16742,13 @@ async function currentArt(entry, spec) {
16511
16742
  const previous = primaryOutput({ ...entry, outputs: entry.supersededOutputs });
16512
16743
  if (!previous) return null;
16513
16744
  const file = resolveOutputPath(previous.path, spec.root);
16514
- if (!existsSync21(file)) return null;
16745
+ if (!existsSync23(file)) return null;
16515
16746
  const contentType = previous.mediaType ?? mediaTypeFromExtension(file);
16516
16747
  if (!contentType) return null;
16517
16748
  let width = entry.width;
16518
16749
  let height = entry.height;
16519
16750
  if (contentType === "image/png") {
16520
- const size = pngSize(await readFile20(file));
16751
+ const size = pngSize(await readFile21(file));
16521
16752
  if (size) ({ width, height } = size);
16522
16753
  }
16523
16754
  return { path: file, contentType, width, height };
@@ -16685,26 +16916,26 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
16685
16916
  }
16686
16917
 
16687
16918
  // src/gallery/generate.ts
16688
- var GenerateRequestSchema = z9.object({
16689
- project: z9.string().min(1).optional(),
16690
- keys: z9.array(z9.string().min(1)).min(1).max(500),
16919
+ var GenerateRequestSchema = z10.object({
16920
+ project: z10.string().min(1).optional(),
16921
+ keys: z10.array(z10.string().min(1)).min(1).max(500),
16691
16922
  /**
16692
16923
  * Regenerate work that is up to date, as `gen --force` does, and let the
16693
16924
  * fetch replace a locally modified or untracked file. With `restore`, put
16694
16925
  * the recorded bytes back over a modified file.
16695
16926
  */
16696
- force: z9.boolean().optional(),
16927
+ force: z10.boolean().optional(),
16697
16928
  /** Advance in-flight, review, or selected work without submitting anything. */
16698
- resume: z9.boolean().optional(),
16929
+ resume: z10.boolean().optional(),
16699
16930
  /** Re-download downloaded outputs and replace files whose object changed upstream. */
16700
- refresh: z9.boolean().optional(),
16931
+ refresh: z10.boolean().optional(),
16701
16932
  /** Put recorded outputs back on disk from the cache or the provider, without generating. */
16702
- restore: z9.boolean().optional(),
16933
+ restore: z10.boolean().optional(),
16703
16934
  /**
16704
16935
  * Bring a replaced generation back: 1-based position in the record's
16705
16936
  * history (newest first) or an output hash prefix. One key at a time.
16706
16937
  */
16707
- revert: z9.string().min(1).max(64).optional()
16938
+ revert: z10.string().min(1).max(64).optional()
16708
16939
  }).strict().refine((request) => [request.resume, request.refresh, request.restore, request.revert !== void 0].filter(Boolean).length <= 1, {
16709
16940
  message: "resume, refresh, restore, and revert are exclusive"
16710
16941
  }).refine((request) => request.revert === void 0 || request.keys.length === 1, { message: "revert takes one key" });
@@ -17014,15 +17245,15 @@ function createGenerateHandlers(opts) {
17014
17245
  }
17015
17246
 
17016
17247
  // src/gallery/editor.ts
17017
- import { readFile as readFile22 } from "fs/promises";
17018
- import path31 from "path";
17248
+ import { readFile as readFile23 } from "fs/promises";
17249
+ import path32 from "path";
17019
17250
 
17020
17251
  // src/editor/install.ts
17021
17252
  import { createHash as createHash4 } from "crypto";
17022
- import { existsSync as existsSync22 } from "fs";
17023
- import { mkdir as mkdir10, readFile as readFile21, rename as rename9, rm as rm9, stat as stat3, writeFile as writeFile13 } from "fs/promises";
17253
+ import { existsSync as existsSync24 } from "fs";
17254
+ import { mkdir as mkdir11, readFile as readFile22, rename as rename9, rm as rm10, stat as stat3, writeFile as writeFile14 } from "fs/promises";
17024
17255
  import os2 from "os";
17025
- import path30 from "path";
17256
+ import path31 from "path";
17026
17257
 
17027
17258
  // tools/pixelorama-bridge/pin.json
17028
17259
  var pin_default = {
@@ -17086,15 +17317,15 @@ var EditorInstallError = class extends Error {
17086
17317
  };
17087
17318
  function editorToolsRoot() {
17088
17319
  const override = process.env.PIXELKILN_TOOLS_DIR?.trim();
17089
- if (override) return path30.resolve(override);
17090
- if (process.platform === "darwin") return path30.join(os2.homedir(), "Library", "Caches", "pixelkiln", "tools");
17320
+ if (override) return path31.resolve(override);
17321
+ if (process.platform === "darwin") return path31.join(os2.homedir(), "Library", "Caches", "pixelkiln", "tools");
17091
17322
  if (process.platform === "win32") {
17092
- return path30.join(process.env.LOCALAPPDATA ?? path30.join(os2.homedir(), "AppData", "Local"), "pixelkiln", "tools");
17323
+ return path31.join(process.env.LOCALAPPDATA ?? path31.join(os2.homedir(), "AppData", "Local"), "pixelkiln", "tools");
17093
17324
  }
17094
- return path30.join(process.env.XDG_CACHE_HOME ?? path30.join(os2.homedir(), ".cache"), "pixelkiln", "tools");
17325
+ return path31.join(process.env.XDG_CACHE_HOME ?? path31.join(os2.homedir(), ".cache"), "pixelkiln", "tools");
17095
17326
  }
17096
17327
  function editorDir(pin = EDITOR_PIN, root = editorToolsRoot()) {
17097
- return path30.join(root, "pixelorama", pin.release ?? "unpublished");
17328
+ return path31.join(root, "pixelorama", pin.release ?? "unpublished");
17098
17329
  }
17099
17330
  function editorBaseUrl(pin = EDITOR_PIN) {
17100
17331
  const override = process.env.PIXELKILN_EDITOR_URL?.trim();
@@ -17104,7 +17335,7 @@ function editorBaseUrl(pin = EDITOR_PIN) {
17104
17335
  }
17105
17336
  async function sha256Of(file) {
17106
17337
  try {
17107
- return createHash4("sha256").update(await readFile21(file)).digest("hex");
17338
+ return createHash4("sha256").update(await readFile22(file)).digest("hex");
17108
17339
  } catch {
17109
17340
  return null;
17110
17341
  }
@@ -17117,8 +17348,8 @@ async function editorStatus(opts = {}) {
17117
17348
  let totalBytes = 0;
17118
17349
  for (const [name, expected] of Object.entries(pin.files)) {
17119
17350
  totalBytes += expected.bytes;
17120
- const file = path30.join(dir, name);
17121
- const info = existsSync22(file) ? await stat3(file).catch(() => null) : null;
17351
+ const file = path31.join(dir, name);
17352
+ const info = existsSync24(file) ? await stat3(file).catch(() => null) : null;
17122
17353
  if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
17123
17354
  installedBytes += expected.bytes;
17124
17355
  } else {
@@ -17147,7 +17378,7 @@ async function installEditor(opts = {}) {
17147
17378
  );
17148
17379
  }
17149
17380
  if (!baseUrl) throw new EditorInstallError("no download location for the editor build");
17150
- await mkdir10(dir, { recursive: true });
17381
+ await mkdir11(dir, { recursive: true });
17151
17382
  const downloaded = [];
17152
17383
  const skipped = Object.keys(pin.files).filter((name) => !before.missing.includes(name));
17153
17384
  let fetchedBytes = before.installedBytes;
@@ -17175,13 +17406,13 @@ async function installEditor(opts = {}) {
17175
17406
  if (digest2 !== expected.sha256) {
17176
17407
  throw new EditorInstallError(`${name}: sha256 ${digest2.slice(0, 12)}\u2026 does not match the pinned ${expected.sha256.slice(0, 12)}\u2026`, name);
17177
17408
  }
17178
- const final = path30.join(dir, name);
17409
+ const final = path31.join(dir, name);
17179
17410
  const tmp = `${final}.${process.pid}.part`;
17180
17411
  try {
17181
- await writeFile13(tmp, bytes);
17412
+ await writeFile14(tmp, bytes);
17182
17413
  await rename9(tmp, final);
17183
17414
  } finally {
17184
- await rm9(tmp, { force: true });
17415
+ await rm10(tmp, { force: true });
17185
17416
  }
17186
17417
  fetchedBytes += bytes.length;
17187
17418
  downloaded.push(name);
@@ -17221,7 +17452,7 @@ var EDITOR_CONTENT_TYPES = {
17221
17452
  ".json": "application/json; charset=utf-8"
17222
17453
  };
17223
17454
  function editorContentType(name) {
17224
- return EDITOR_CONTENT_TYPES[path30.extname(name).toLowerCase()] ?? null;
17455
+ return EDITOR_CONTENT_TYPES[path31.extname(name).toLowerCase()] ?? null;
17225
17456
  }
17226
17457
 
17227
17458
  // src/gallery/editor.ts
@@ -17297,7 +17528,7 @@ function createGalleryEditorHandlers(opts = {}) {
17297
17528
  if (!status.installed) return null;
17298
17529
  let bytes;
17299
17530
  try {
17300
- bytes = await readFile22(path31.join(dir, name));
17531
+ bytes = await readFile23(path32.join(dir, name));
17301
17532
  } catch {
17302
17533
  verified = null;
17303
17534
  return null;
@@ -17337,13 +17568,13 @@ async function serveUntilStopped(initial, reload, args, access2) {
17337
17568
  };
17338
17569
  const loadProject = async (project) => {
17339
17570
  const ctx = await access2.loadProject(project);
17340
- const dir = path32.dirname(ctx.loaded.path);
17571
+ const dir = path33.dirname(ctx.loaded.path);
17341
17572
  const declared = readEnvFiles(dir);
17342
17573
  const credentialNames = new Set(availableProviders().flatMap((id) => providerCredentialEnvs(providerFactory(id))));
17343
17574
  for (const [name, value] of Object.entries(declared)) {
17344
17575
  if (credentialNames.has(name) && name in process.env && process.env[name] !== value) {
17345
17576
  throw new Error(
17346
- `${path32.relative(process.cwd(), dir) || "."} sets ${name} to a different value than the one this gallery already loaded; run a separate gallery for that project so its work uses its own account.`
17577
+ `${path33.relative(process.cwd(), dir) || "."} sets ${name} to a different value than the one this gallery already loaded; run a separate gallery for that project so its work uses its own account.`
17347
17578
  );
17348
17579
  }
17349
17580
  }
@@ -17371,6 +17602,8 @@ async function serveUntilStopped(initial, reload, args, access2) {
17371
17602
  ),
17372
17603
  ...args.edit ? { edit: createGalleryEditHandler({ manifestFor: access2.manifestFor, loadProject: access2.loadProject, reload, onProgress: log }) } : {},
17373
17604
  ...budget ? { generate: createGenerateHandlers({ loadProject, providerFor, budget, reload, onProgress: log }) } : {},
17605
+ // Estimating acts on the author's behalf and feeds a manifest edit, so it follows the write gate.
17606
+ ...args.edit ? { skeleton: createGallerySkeletonHandlers({ loadProject, limit: args.estimateLimit, onProgress: log }) } : {},
17374
17607
  // The editor exists to write hand edits back, so it follows the write gate.
17375
17608
  ...args.edit && !args.noEditor ? { editor: createGalleryEditorHandlers({ onProgress: log }) } : {}
17376
17609
  });
@@ -17388,8 +17621,8 @@ async function serveUntilStopped(initial, reload, args, access2) {
17388
17621
  }
17389
17622
  async function runGallery(args) {
17390
17623
  if (args.workspace) {
17391
- const workspacePath = path32.resolve(args.workspace);
17392
- if (!existsSync23(workspacePath)) {
17624
+ const workspacePath = path33.resolve(args.workspace);
17625
+ if (!existsSync25(workspacePath)) {
17393
17626
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
17394
17627
  }
17395
17628
  const filter2 = { styles: args.styles, assets: args.assets };
@@ -17409,7 +17642,7 @@ async function runGallery(args) {
17409
17642
  const registered = async (projectId) => {
17410
17643
  const project = (await loadWorkspace(workspacePath)).projects.find((candidate) => candidate.id === projectId);
17411
17644
  if (!projectId || !project) throw new Error(`unknown workspace project "${projectId ?? ""}"`);
17412
- return resolveProject(path32.dirname(workspacePath), project);
17645
+ return resolveProject(path33.dirname(workspacePath), project);
17413
17646
  };
17414
17647
  await serveUntilStopped(initial, build2, args, {
17415
17648
  manifestFor: async (projectId) => (await registered(projectId)).manifestPath,
@@ -17434,7 +17667,7 @@ async function runGallery(args) {
17434
17667
  return buildGallerySnapshot({ ...fresh, filter });
17435
17668
  };
17436
17669
  await serveUntilStopped(await build(), reload, args, {
17437
- manifestFor: () => path32.resolve(args.manifest),
17670
+ manifestFor: () => path33.resolve(args.manifest),
17438
17671
  loadProject: () => openProject(args.manifest, { lockPath: args.lock, env: false })
17439
17672
  });
17440
17673
  }
@@ -17743,18 +17976,18 @@ async function runRestoreGeneration(args) {
17743
17976
  }
17744
17977
 
17745
17978
  // src/cli/commands/housekeeping.ts
17746
- import { existsSync as existsSync25 } from "fs";
17979
+ import { existsSync as existsSync27 } from "fs";
17747
17980
 
17748
17981
  // src/pipeline/cache-health.ts
17749
- import { existsSync as existsSync24 } from "fs";
17750
- import { readFile as readFile23, readdir as readdir2, rm as rm10 } from "fs/promises";
17751
- import path33 from "path";
17982
+ import { existsSync as existsSync26 } from "fs";
17983
+ import { readFile as readFile24, readdir as readdir2, rm as rm11 } from "fs/promises";
17984
+ import path34 from "path";
17752
17985
  async function inspectCaches(lock, lockPath, options = {}) {
17753
- if (options.prune && !existsSync24(lockPath)) {
17754
- throw new Error(`Refusing to prune without an existing lockfile at ${path33.resolve(lockPath)}`);
17986
+ if (options.prune && !existsSync26(lockPath)) {
17987
+ throw new Error(`Refusing to prune without an existing lockfile at ${path34.resolve(lockPath)}`);
17755
17988
  }
17756
- const contentDir = path33.resolve(path33.dirname(lockPath), ".pixelkiln", "cache");
17757
- const remotePath = path33.resolve(cachePathFor(lockPath));
17989
+ const contentDir = path34.resolve(path34.dirname(lockPath), ".pixelkiln", "cache");
17990
+ const remotePath = path34.resolve(cachePathFor(lockPath));
17758
17991
  const referenced = referencedHashes(lock);
17759
17992
  let content = await inspectContentCache(contentDir, referenced);
17760
17993
  let remoteHashes = await inspectRemoteHashCache(remotePath);
@@ -17766,7 +17999,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
17766
17999
  ]);
17767
18000
  for (const name of names) {
17768
18001
  try {
17769
- await rm10(path33.join(contentDir, name), { force: true });
18002
+ await rm11(path34.join(contentDir, name), { force: true });
17770
18003
  removed.contentFiles++;
17771
18004
  } catch {
17772
18005
  }
@@ -17775,7 +18008,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
17775
18008
  await saveCache(remotePath, { version: 1, hashes: {} });
17776
18009
  removed.resetRemoteHashCache = true;
17777
18010
  } else if (remoteHashes.invalidIds.length) {
17778
- const cache = parseCache(JSON.parse(await readFile23(remotePath, "utf8")));
18011
+ const cache = parseCache(JSON.parse(await readFile24(remotePath, "utf8")));
17779
18012
  for (const id of remoteHashes.invalidIds) delete cache.hashes[id];
17780
18013
  removed.remoteHashEntries = remoteHashes.invalidIds.length;
17781
18014
  await saveCache(remotePath, cache);
@@ -17793,7 +18026,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
17793
18026
  async function inspectContentCache(contentDir, referenced) {
17794
18027
  const report = {
17795
18028
  path: contentDir,
17796
- exists: existsSync24(contentDir),
18029
+ exists: existsSync26(contentDir),
17797
18030
  files: 0,
17798
18031
  bytes: 0,
17799
18032
  valid: 0,
@@ -17814,12 +18047,12 @@ async function inspectContentCache(contentDir, referenced) {
17814
18047
  continue;
17815
18048
  }
17816
18049
  report.files++;
17817
- const file = path33.join(contentDir, entry.name);
18050
+ const file = path34.join(contentDir, entry.name);
17818
18051
  const mediaType = mediaTypeFromExtension(entry.name);
17819
18052
  const expected = mediaType ? entry.name.slice(0, -4) : "";
17820
18053
  let bytes;
17821
18054
  try {
17822
- bytes = await readFile23(file);
18055
+ bytes = await readFile24(file);
17823
18056
  report.bytes += bytes.length;
17824
18057
  } catch (err) {
17825
18058
  report.invalid.push({
@@ -17857,7 +18090,7 @@ async function inspectContentCache(contentDir, referenced) {
17857
18090
  async function inspectRemoteHashCache(remotePath) {
17858
18091
  const report = {
17859
18092
  path: remotePath,
17860
- exists: existsSync24(remotePath),
18093
+ exists: existsSync26(remotePath),
17861
18094
  entries: 0,
17862
18095
  valid: 0,
17863
18096
  invalidIds: [],
@@ -17866,7 +18099,7 @@ async function inspectRemoteHashCache(remotePath) {
17866
18099
  if (!report.exists) return report;
17867
18100
  let cache;
17868
18101
  try {
17869
- cache = parseCache(JSON.parse(await readFile23(remotePath, "utf8")));
18102
+ cache = parseCache(JSON.parse(await readFile24(remotePath, "utf8")));
17870
18103
  } catch (err) {
17871
18104
  report.error = err instanceof Error ? err.message : String(err);
17872
18105
  return report;
@@ -17930,7 +18163,7 @@ async function runAccept(args) {
17930
18163
  let intact = true;
17931
18164
  for (const output of entry.outputs) {
17932
18165
  const file = resolveOutputPath(output.path, item.spec.root);
17933
- if (!existsSync25(file) || await sha256File(file) !== output.sha256) {
18166
+ if (!existsSync27(file) || await sha256File(file) !== output.sha256) {
17934
18167
  intact = false;
17935
18168
  break;
17936
18169
  }
@@ -17985,42 +18218,42 @@ This drops their provenance from the lockfile. The art on disk is`);
17985
18218
  }
17986
18219
 
17987
18220
  // src/cli/commands/init.ts
17988
- import path34 from "path";
17989
- import { existsSync as existsSync26 } from "fs";
18221
+ import path35 from "path";
18222
+ import { existsSync as existsSync28 } from "fs";
17990
18223
  async function runInit(args) {
17991
18224
  if (!args.from) throw new Error("init needs --from <dir> pointing at your existing PNGs.");
17992
- const root = path34.resolve(args.from);
17993
- if (!existsSync26(root)) throw new Error(`No directory at ${root}`);
18225
+ const root = path35.resolve(args.from);
18226
+ if (!existsSync28(root)) throw new Error(`No directory at ${root}`);
17994
18227
  const generator = args.generator ?? "map";
17995
18228
  if (generator !== "1dir" && generator !== "map") {
17996
18229
  throw new Error(`--generator must be "1dir" or "map", got "${args.generator}".`);
17997
18230
  }
17998
- const target = path34.resolve(args.out ?? "pixelkiln.manifest.json");
18231
+ const target = path35.resolve(args.out ?? "pixelkiln.manifest.json");
17999
18232
  const { assets, skipped } = await scanAssets(root, { exclude: args.exclude });
18000
18233
  if (!assets.length) throw new Error(`No PNGs found under ${root}`);
18001
18234
  const manifest = buildManifest(
18002
- args.name ?? path34.basename(path34.dirname(target)),
18235
+ args.name ?? path35.basename(path35.dirname(target)),
18003
18236
  args.styles[0] ?? "base",
18004
18237
  generator,
18005
- path34.relative(path34.dirname(target), root) || ".",
18238
+ path35.relative(path35.dirname(target), root) || ".",
18006
18239
  assets
18007
18240
  );
18008
18241
  await writeManifestFile(target, manifest);
18009
- log(` scanned ${assets.length} PNG(s) under ${path34.relative(process.cwd(), root)}`);
18242
+ log(` scanned ${assets.length} PNG(s) under ${path35.relative(process.cwd(), root)}`);
18010
18243
  if (skipped.length) log(` skipped ${skipped.length} unreadable file(s)`);
18011
- log(` wrote ${path34.relative(process.cwd(), target)}`);
18244
+ log(` wrote ${path35.relative(process.cwd(), target)}`);
18012
18245
  log(`
18013
18246
  Prompts are intentionally empty. To recover the real ones from your`);
18014
18247
  log(` PixelLab account instead of inventing them:`);
18015
18248
  log(`
18016
- pixelkiln adopt --manifest ${path34.relative(process.cwd(), target)} --write-prompts
18249
+ pixelkiln adopt --manifest ${path35.relative(process.cwd(), target)} --write-prompts
18017
18250
  `);
18018
18251
  }
18019
18252
 
18020
18253
  // src/pipeline/doctor.ts
18021
- import { constants, existsSync as existsSync27 } from "fs";
18254
+ import { constants, existsSync as existsSync29 } from "fs";
18022
18255
  import { access, stat as stat4 } from "fs/promises";
18023
- import path35 from "path";
18256
+ import path36 from "path";
18024
18257
  async function doctor(loaded, specs, lock, lockPath, opts = {}) {
18025
18258
  const checks = [];
18026
18259
  const add = (id, level, message6) => checks.push({ id, level, message: message6 });
@@ -18031,8 +18264,8 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
18031
18264
  );
18032
18265
  add("lock", "ok", `${Object.keys(lock.entries).length} valid v2 lock entr(ies)`);
18033
18266
  const dirs = [...new Set(specs.flatMap((spec) => [
18034
- path35.dirname(spec.outFile),
18035
- ...spec.quality ? [path35.dirname(spec.quality.outFile)] : []
18267
+ path36.dirname(spec.outFile),
18268
+ ...spec.quality ? [path36.dirname(spec.quality.outFile)] : []
18036
18269
  ]))];
18037
18270
  const unwritable = [];
18038
18271
  for (const dir of dirs) {
@@ -18055,7 +18288,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
18055
18288
  for (let index = 0; index < entry.outputs.length; index++) {
18056
18289
  const output = entry.outputs[index];
18057
18290
  const spec = specByKey.get(key);
18058
- const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, path35.dirname(lockPath));
18291
+ const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, path36.dirname(lockPath));
18059
18292
  const owner = outputOwners.get(absolute);
18060
18293
  if (owner && owner !== key) duplicateOutputs.push(`${output.path} (${owner}, ${key})`);
18061
18294
  outputOwners.set(absolute, key);
@@ -18070,7 +18303,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
18070
18303
  } else {
18071
18304
  add("lock-outputs", "ok", `${outputOwners.size} recorded output path(s) are uniquely owned`);
18072
18305
  }
18073
- const cacheDir = path35.join(path35.dirname(lockPath), ".pixelkiln", "cache");
18306
+ const cacheDir = path36.join(path36.dirname(lockPath), ".pixelkiln", "cache");
18074
18307
  const stranded = [];
18075
18308
  const invalidState = [];
18076
18309
  const staleMapUrls = [];
@@ -18086,11 +18319,11 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
18086
18319
  const hasSource = Boolean(entry.sourceUrl || entry.sourceUrls?.length);
18087
18320
  let hasCache = false;
18088
18321
  for (const output of entry.outputs) {
18089
- const cached = path35.join(
18322
+ const cached = path36.join(
18090
18323
  cacheDir,
18091
18324
  cacheFileName(output.sha256, output.mediaType ?? MediaType.PNG)
18092
18325
  );
18093
- if (existsSync27(cached) && await sha256File(cached) === output.sha256) {
18326
+ if (existsSync29(cached) && await sha256File(cached) === output.sha256) {
18094
18327
  hasCache = true;
18095
18328
  break;
18096
18329
  }
@@ -18186,16 +18419,16 @@ async function nearestExistingDirectory(start) {
18186
18419
  if ((await stat4(current)).isDirectory()) return current;
18187
18420
  } catch {
18188
18421
  }
18189
- const parent = path35.dirname(current);
18422
+ const parent = path36.dirname(current);
18190
18423
  if (parent === current) return current;
18191
18424
  current = parent;
18192
18425
  }
18193
18426
  }
18194
18427
 
18195
18428
  // src/pipeline/audit.ts
18196
- import { readFile as readFile24 } from "fs/promises";
18197
- import { existsSync as existsSync28 } from "fs";
18198
- import path36 from "path";
18429
+ import { readFile as readFile25 } from "fs/promises";
18430
+ import { existsSync as existsSync30 } from "fs";
18431
+ import path37 from "path";
18199
18432
  function paletteDistance(asset, reference) {
18200
18433
  if (!asset.length || !reference.length) return 0;
18201
18434
  let total = 0;
@@ -18231,12 +18464,12 @@ async function auditStyle(loaded, specs, styleId, lock) {
18231
18464
  const unreadable = [];
18232
18465
  for (const spec of mine) {
18233
18466
  for (const output of resolveSpecOutputs(spec, lock, loaded.root)) {
18234
- if (!existsSync28(output.absolutePath)) {
18467
+ if (!existsSync30(output.absolutePath)) {
18235
18468
  missing.push(output.id);
18236
18469
  continue;
18237
18470
  }
18238
18471
  try {
18239
- const png = decodePng(await readFile24(output.absolutePath));
18472
+ const png = decodePng(await readFile25(output.absolutePath));
18240
18473
  const palette = extractPalette(png, 12);
18241
18474
  assets.push({
18242
18475
  assetId: spec.assetId,
@@ -18260,10 +18493,10 @@ async function auditStyle(loaded, specs, styleId, lock) {
18260
18493
  let referenceFromStyleImages = false;
18261
18494
  const refPalettes = [];
18262
18495
  for (const img of style.styleImages) {
18263
- const abs = path36.resolve(loaded.root, img.path);
18264
- if (!existsSync28(abs)) continue;
18496
+ const abs = path37.resolve(loaded.root, img.path);
18497
+ if (!existsSync30(abs)) continue;
18265
18498
  try {
18266
- refPalettes.push(extractPalette(decodePng(await readFile24(abs)), 12));
18499
+ refPalettes.push(extractPalette(decodePng(await readFile25(abs)), 12));
18267
18500
  } catch {
18268
18501
  }
18269
18502
  }
@@ -18491,24 +18724,24 @@ async function runAudit(args) {
18491
18724
  }
18492
18725
 
18493
18726
  // src/cli/commands/pack.ts
18494
- import path38 from "path";
18495
- import { readFile as readFile25 } from "fs/promises";
18727
+ import path39 from "path";
18728
+ import { readFile as readFile26 } from "fs/promises";
18496
18729
 
18497
18730
  // src/pipeline/pack.ts
18498
18731
  import { readFileSync as readFileSync5 } from "fs";
18499
- import path37 from "path";
18732
+ import path38 from "path";
18500
18733
  var DEFAULT_FRAME_SET_FPS = 12;
18501
18734
  function resolvePackInputs(raw, inputsFilePath) {
18502
18735
  if (!Array.isArray(raw) || !raw.length) {
18503
18736
  throw new Error("--inputs must be a non-empty JSON array of { id, path }");
18504
18737
  }
18505
- const baseDir = path37.dirname(path37.resolve(inputsFilePath));
18738
+ const baseDir = path38.dirname(path38.resolve(inputsFilePath));
18506
18739
  return raw.map((entry, i) => {
18507
18740
  const e = entry;
18508
18741
  if (typeof e.id !== "string" || typeof e.path !== "string") {
18509
18742
  throw new Error(`--inputs[${i}] needs string "id" and "path"`);
18510
18743
  }
18511
- return { id: e.id, path: path37.resolve(baseDir, e.path) };
18744
+ return { id: e.id, path: path38.resolve(baseDir, e.path) };
18512
18745
  });
18513
18746
  }
18514
18747
  function packSprites(inputs, options = {}) {
@@ -18592,7 +18825,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
18592
18825
  }
18593
18826
  const inputs2 = Object.entries(options.sourceOverrides).map(([id, source]) => ({
18594
18827
  id,
18595
- path: path37.resolve(source)
18828
+ path: path38.resolve(source)
18596
18829
  }));
18597
18830
  if (!inputs2.length) throw new Error(`No approved quality outputs for style "${styleId}".`);
18598
18831
  const packed2 = packSprites(inputs2, options);
@@ -18616,7 +18849,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
18616
18849
  let outputs = resolveEntryOutputs(entry, id, manifestDir);
18617
18850
  const source = sources[id];
18618
18851
  if (source && outputs.length <= 1) {
18619
- inputs.push({ id, path: path37.resolve(manifestDir, source) });
18852
+ inputs.push({ id, path: path38.resolve(manifestDir, source) });
18620
18853
  continue;
18621
18854
  }
18622
18855
  if (source && sourceIsStem(source, entry, manifestDir)) {
@@ -18650,7 +18883,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
18650
18883
  }
18651
18884
  }
18652
18885
  for (const [id, source] of Object.entries(sources)) {
18653
- if (!locked.has(id)) inputs.push({ id, path: path37.resolve(manifestDir, source) });
18886
+ if (!locked.has(id)) inputs.push({ id, path: path38.resolve(manifestDir, source) });
18654
18887
  }
18655
18888
  if (!inputs.length) {
18656
18889
  throw new Error(`No readable sprites for style "${styleId}"; ${noOutput.length} skipped.`);
@@ -18758,7 +18991,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
18758
18991
  const source = sources[id];
18759
18992
  const entry = lock.entries[`${styleId}/${id}`];
18760
18993
  if (source && !(entry && sourceIsStem(source, entry, manifestDir))) {
18761
- placements.push({ id, path: path37.resolve(manifestDir, source), cell });
18994
+ placements.push({ id, path: path38.resolve(manifestDir, source), cell });
18762
18995
  continue;
18763
18996
  }
18764
18997
  if (source && entry) {
@@ -18791,7 +19024,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
18791
19024
  `Nothing to mount for style "${styleId}": ${ids.length} asset(s) declare a cell, but none has generated output or a \`source\`. Run \`pixelkiln gen --style ${styleId}\`, or point each at a file.${detail}`
18792
19025
  );
18793
19026
  }
18794
- const basePng = mount.base ? readFileSync5(path37.resolve(manifestDir, mount.base)) : void 0;
19027
+ const basePng = mount.base ? readFileSync5(path38.resolve(manifestDir, mount.base)) : void 0;
18795
19028
  const mounted = mountSprites(placements, {
18796
19029
  cellWidth: mount.cellWidth,
18797
19030
  cellHeight: mount.cellHeight,
@@ -18801,7 +19034,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
18801
19034
  ...mounted,
18802
19035
  sources: mount.base ? [{
18803
19036
  id: "$base",
18804
- path: path37.resolve(manifestDir, mount.base),
19037
+ path: path38.resolve(manifestDir, mount.base),
18805
19038
  sha256: sha256(basePng),
18806
19039
  included: true
18807
19040
  }, ...mounted.sources] : mounted.sources,
@@ -19205,12 +19438,12 @@ async function runPack(args) {
19205
19438
  if (args.primaryOnly || args.outputRoles.length) {
19206
19439
  throw new Error("--primary-only and --output-role require manifest-driven pack");
19207
19440
  }
19208
- const raw = JSON.parse(await readFile25(path38.resolve(args.inputs), "utf8"));
19441
+ const raw = JSON.parse(await readFile26(path39.resolve(args.inputs), "utf8"));
19209
19442
  const inputs = resolvePackInputs(raw, args.inputs);
19210
19443
  const { png, atlas, skipped, sources } = packSprites(inputs, { columns: args.columns });
19211
19444
  const format = sheetFormat(args);
19212
- const base = path38.resolve(args.out.replace(/\.(?:png|json|tres)$/i, ""));
19213
- const { extension, document } = renderSheetDocument(format, atlas, { imageName: path38.basename(`${base}.png`) });
19445
+ const base = path39.resolve(args.out.replace(/\.(?:png|json|tres)$/i, ""));
19446
+ const { extension, document } = renderSheetDocument(format, atlas, { imageName: path39.basename(`${base}.png`) });
19214
19447
  const outputs = [
19215
19448
  { path: `${base}.png`, data: png },
19216
19449
  { path: `${base}${extension}`, data: document }
@@ -19223,7 +19456,7 @@ async function runPack(args) {
19223
19456
  log(
19224
19457
  ` ${atlas.frames.length} sprite(s), ${atlas.sheet.width}x${atlas.sheet.height} in ${atlas.columns} column(s), ${(png.length / 1024).toFixed(1)} KB (${format})`
19225
19458
  );
19226
- log(` ${path38.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
19459
+ log(` ${path39.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
19227
19460
  for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
19228
19461
  return;
19229
19462
  }
@@ -19231,7 +19464,7 @@ async function runPack(args) {
19231
19464
  if (args.primaryOnly && args.outputRoles.length) {
19232
19465
  throw new Error("pack accepts either --primary-only or --output-role, not both");
19233
19466
  }
19234
- const manifestDir = path38.dirname(path38.resolve(args.manifest));
19467
+ const manifestDir = path39.dirname(path39.resolve(args.manifest));
19235
19468
  const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
19236
19469
  for (const styleId of styleIds) {
19237
19470
  const style = loaded.manifest.styles[styleId];
@@ -19246,9 +19479,9 @@ async function runPack(args) {
19246
19479
  sourceOverrides: qualitySources,
19247
19480
  sources: manifestSources(loaded.manifest, styleId)
19248
19481
  });
19249
- const base = args.out ? path38.resolve(args.out.replace(/\.(?:png|json|tres)$/i, "")) : path38.resolve(manifestDir, style.outDir, `${styleId}-sheet`);
19482
+ const base = args.out ? path39.resolve(args.out.replace(/\.(?:png|json|tres)$/i, "")) : path39.resolve(manifestDir, style.outDir, `${styleId}-sheet`);
19250
19483
  const format = sheetFormat(args);
19251
- const { extension, document } = renderSheetDocument(format, atlas, { imageName: path38.basename(`${base}.png`) });
19484
+ const { extension, document } = renderSheetDocument(format, atlas, { imageName: path39.basename(`${base}.png`) });
19252
19485
  const outputs = [
19253
19486
  { path: `${base}.png`, data: png },
19254
19487
  { path: `${base}${extension}`, data: document }
@@ -19280,13 +19513,13 @@ async function runPack(args) {
19280
19513
  log(
19281
19514
  ` ${styleId}: ${atlas.frames.length} sprite(s), ${atlas.sheet.width}x${atlas.sheet.height} in ${atlas.columns} column(s)` + (atlas.sets?.length ? `, ${atlas.sets.length} set(s)` : "") + ` (${format})`
19282
19515
  );
19283
- log(` ${path38.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
19516
+ log(` ${path39.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
19284
19517
  for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
19285
19518
  }
19286
19519
  }
19287
19520
  async function runMount(args) {
19288
19521
  const { loaded, lock } = await openProject2(args);
19289
- const manifestDir = path38.dirname(path38.resolve(args.manifest));
19522
+ const manifestDir = path39.dirname(path39.resolve(args.manifest));
19290
19523
  const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
19291
19524
  for (const styleId of styleIds) {
19292
19525
  const style = loaded.manifest.styles[styleId];
@@ -19325,9 +19558,9 @@ async function runMount(args) {
19325
19558
  sources,
19326
19559
  outputRoles
19327
19560
  );
19328
- const out = path38.resolve(manifestDir, style.mount.out);
19561
+ const out = path39.resolve(manifestDir, style.mount.out);
19329
19562
  const format = sheetFormat(args);
19330
- const { extension, document } = renderSheetDocument(format, atlas, { imageName: path38.basename(out) });
19563
+ const { extension, document } = renderSheetDocument(format, atlas, { imageName: path39.basename(out) });
19331
19564
  const metadata = out.replace(/\.png$/, "") + extension;
19332
19565
  const companion = out.replace(/\.png$/, "") + ".pixelkiln.json";
19333
19566
  const outputs = [
@@ -19347,7 +19580,7 @@ async function runMount(args) {
19347
19580
  await provenanceFile("$lock", args.lock),
19348
19581
  ...qualityRecords,
19349
19582
  ...artifactSources.filter(
19350
- (source) => source.id !== "$base" || path38.resolve(source.path) !== out
19583
+ (source) => source.id !== "$base" || path39.resolve(source.path) !== out
19351
19584
  )
19352
19585
  ],
19353
19586
  options: {
@@ -19361,14 +19594,14 @@ async function runMount(args) {
19361
19594
  log(
19362
19595
  ` ${styleId}: ${atlas.frames.length} cell(s) into ${atlas.sheet.width}x${atlas.sheet.height}` + (overBase ? ` over ${style.mount.base}` : " (new sheet)")
19363
19596
  );
19364
- log(` ${path38.relative(process.cwd(), out)} + atlas/provenance JSON`);
19597
+ log(` ${path39.relative(process.cwd(), out)} + atlas/provenance JSON`);
19365
19598
  for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
19366
19599
  }
19367
19600
  }
19368
19601
  async function runExport(args) {
19369
19602
  const { loaded, specs, lock } = await openProject2(args);
19370
19603
  const format = args.format ?? "generic";
19371
- const manifestDir = path38.dirname(path38.resolve(args.manifest));
19604
+ const manifestDir = path39.dirname(path39.resolve(args.manifest));
19372
19605
  const selected = specs.filter((spec) => {
19373
19606
  if (spec.generator !== "tiles" && spec.generator !== "terrain") return false;
19374
19607
  if (args.styles.length && !args.styles.includes(spec.styleId)) return false;
@@ -19384,12 +19617,12 @@ async function runExport(args) {
19384
19617
  for (const spec of selected) {
19385
19618
  const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
19386
19619
  const style = loaded.manifest.styles[spec.styleId];
19387
- const defaultBase = path38.resolve(manifestDir, style.outDir, `${spec.assetId}-tileset`);
19388
- const base = args.out ? path38.resolve(args.out.replace(/\.(?:png|json|tsj|tres)$/i, "")) : defaultBase;
19620
+ const defaultBase = path39.resolve(manifestDir, style.outDir, `${spec.assetId}-tileset`);
19621
+ const base = args.out ? path39.resolve(args.out.replace(/\.(?:png|json|tsj|tres)$/i, "")) : defaultBase;
19389
19622
  const result = exportTileset(entry, spec, {
19390
19623
  format,
19391
19624
  manifestDir,
19392
- imageName: path38.basename(`${base}.png`),
19625
+ imageName: path39.basename(`${base}.png`),
19393
19626
  columns: args.columns
19394
19627
  });
19395
19628
  const outputs = [
@@ -19407,7 +19640,7 @@ async function runExport(args) {
19407
19640
  asset: spec.assetId,
19408
19641
  columns: args.columns ?? null,
19409
19642
  format,
19410
- image: path38.basename(`${base}.png`),
19643
+ image: path39.basename(`${base}.png`),
19411
19644
  providerRules: result.generic.providerRules,
19412
19645
  style: spec.styleId,
19413
19646
  tileType: spec.tileType ?? null
@@ -19417,7 +19650,7 @@ async function runExport(args) {
19417
19650
  ` ${spec.styleId}/${spec.assetId}: ${result.generic.tiles.length} tile(s), ${result.generic.sheet.width}x${result.generic.sheet.height} (${format})`
19418
19651
  );
19419
19652
  log(
19420
- ` ${path38.relative(process.cwd(), base)}.png + ${path38.basename(base)}${result.extension} + .pixelkiln.json`
19653
+ ` ${path39.relative(process.cwd(), base)}.png + ${path39.basename(base)}${result.extension} + .pixelkiln.json`
19421
19654
  );
19422
19655
  }
19423
19656
  }
@@ -19430,23 +19663,23 @@ function sheetFormat(args) {
19430
19663
  }
19431
19664
 
19432
19665
  // src/cli/commands/pixellab-utils.ts
19433
- import path40 from "path";
19666
+ import path41 from "path";
19434
19667
 
19435
19668
  // src/pixellab-utils.ts
19436
- import { existsSync as existsSync29 } from "fs";
19437
- import { mkdir as mkdir11, readFile as readFile26, writeFile as writeFile14 } from "fs/promises";
19438
- import path39 from "path";
19669
+ import { existsSync as existsSync31 } from "fs";
19670
+ import { mkdir as mkdir12, readFile as readFile27, writeFile as writeFile15 } from "fs/promises";
19671
+ import path40 from "path";
19439
19672
  var UNZOOM_MIN_SIDE = 256;
19440
19673
  var UNZOOM_MAX_AREA = 2048 * 2048;
19441
19674
  function defaultUnzoomOut(input) {
19442
- const ext = path39.extname(input);
19675
+ const ext = path40.extname(input);
19443
19676
  return `${ext ? input.slice(0, -ext.length) : input}.unzoomed.png`;
19444
19677
  }
19445
19678
  async function unzoomFile(client2, input, opts = {}) {
19446
- const out = path39.resolve(opts.out ?? defaultUnzoomOut(input));
19447
- if (path39.resolve(input) === out) throw new Error("unzoom will not overwrite its own input; pass a different --out");
19448
- if (existsSync29(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
19449
- const bytes = await readFile26(input);
19679
+ const out = path40.resolve(opts.out ?? defaultUnzoomOut(input));
19680
+ if (path40.resolve(input) === out) throw new Error("unzoom will not overwrite its own input; pass a different --out");
19681
+ if (existsSync31(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
19682
+ const bytes = await readFile27(input);
19450
19683
  const metadata = imageMetadata(bytes);
19451
19684
  if (!metadata) throw new Error(`${input} is not a readable PNG or JPEG`);
19452
19685
  const { width, height, format } = metadata;
@@ -19459,22 +19692,22 @@ async function unzoomFile(client2, input, opts = {}) {
19459
19692
  throw new Error(`${input} is ${width}x${height}; PixelLab's unzoom takes at most 2048x2048 worth of pixels`);
19460
19693
  }
19461
19694
  const res = await client2.unzoom({ image: { base64: bytes.toString("base64"), format }, quantize: opts.quantize });
19462
- await mkdir11(path39.dirname(out), { recursive: true });
19463
- await writeFile14(out, res.png);
19695
+ await mkdir12(path40.dirname(out), { recursive: true });
19696
+ await writeFile15(out, res.png);
19464
19697
  return { out, original: res.originalSize, unzoomed: res.unzoomedSize, zoomFactor: res.zoomFactor, usage: res.usage };
19465
19698
  }
19466
19699
  var FONT_WEIGHTS = ["Bold", "Regular"];
19467
19700
  var FONT_GLYPH_SIZES = [8, 16, 32, 64];
19468
19701
  var FONT_COST_GENERATIONS = 25;
19469
19702
  function fontOutputPaths(out) {
19470
- const base = path39.resolve(out.replace(/\.(ttf|png)$/i, ""));
19703
+ const base = path40.resolve(out.replace(/\.(ttf|png)$/i, ""));
19471
19704
  return { ttf: `${base}.ttf`, atlas: `${base}.png` };
19472
19705
  }
19473
19706
  async function generateFont(client2, opts) {
19474
19707
  const { ttf, atlas } = fontOutputPaths(opts.out);
19475
19708
  if (!opts.force) {
19476
19709
  for (const file of [ttf, atlas]) {
19477
- if (existsSync29(file)) throw new Error(`${file} already exists; pass --force to replace it`);
19710
+ if (existsSync31(file)) throw new Error(`${file} already exists; pass --force to replace it`);
19478
19711
  }
19479
19712
  }
19480
19713
  const sleep3 = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
@@ -19501,9 +19734,9 @@ async function generateFont(client2, opts) {
19501
19734
  client2.download(job.download_ttf_url),
19502
19735
  client2.download(job.download_atlas_url)
19503
19736
  ]);
19504
- await mkdir11(path39.dirname(ttf), { recursive: true });
19505
- await writeFile14(ttf, ttfBytes);
19506
- await writeFile14(atlas, atlasBytes);
19737
+ await mkdir12(path40.dirname(ttf), { recursive: true });
19738
+ await writeFile15(ttf, ttfBytes);
19739
+ await writeFile15(atlas, atlasBytes);
19507
19740
  return { jobId, ttf, atlas, glyphPx: job.glyph_px ?? null, usage: job.usage };
19508
19741
  }
19509
19742
  if (Date.now() - started > timeout) {
@@ -19517,7 +19750,7 @@ async function generateFont(client2, opts) {
19517
19750
 
19518
19751
  // src/cli/commands/pixellab-utils.ts
19519
19752
  function loadKeys(args) {
19520
- loadEnvFiles(path40.dirname(path40.resolve(args.manifest)));
19753
+ loadEnvFiles(path41.dirname(path41.resolve(args.manifest)));
19521
19754
  loadEnvFiles(process.cwd());
19522
19755
  }
19523
19756
  async function runUnzoom(args) {
@@ -19583,19 +19816,19 @@ async function runFont(args) {
19583
19816
  }
19584
19817
 
19585
19818
  // src/cli/commands/quality.ts
19586
- import path42 from "path";
19587
- import { readFile as readFile28 } from "fs/promises";
19819
+ import path43 from "path";
19820
+ import { readFile as readFile29 } from "fs/promises";
19588
19821
 
19589
19822
  // src/pipeline/quality-regression.ts
19590
- import path41 from "path";
19591
- import { existsSync as existsSync30 } from "fs";
19592
- import { readFile as readFile27 } from "fs/promises";
19593
- import { z as z10 } from "zod";
19823
+ import path42 from "path";
19824
+ import { existsSync as existsSync32 } from "fs";
19825
+ import { readFile as readFile28 } from "fs/promises";
19826
+ import { z as z11 } from "zod";
19594
19827
  var SHA256_RE = /^[0-9a-f]{64}$/;
19595
19828
  var HEX_RE = /^#[0-9a-f]{6}$/;
19596
19829
  var MAX_REDMEAN_DISTANCE = 765;
19597
- var QualityPathSchema = z10.string().min(1).refine(
19598
- (value) => !path41.posix.isAbsolute(value) && !value.includes("\\"),
19830
+ var QualityPathSchema = z11.string().min(1).refine(
19831
+ (value) => !path42.posix.isAbsolute(value) && !value.includes("\\"),
19599
19832
  "expected a portable relative path"
19600
19833
  );
19601
19834
  var DEFAULT_QUALITY_TOLERANCES = {
@@ -19608,53 +19841,53 @@ var DEFAULT_QUALITY_TOLERANCES = {
19608
19841
  maxEdgeContrastDrop: 0.03,
19609
19842
  maxIsolatedPixelIncrease: 5e-3
19610
19843
  };
19611
- var QualityToleranceSchema = z10.object({
19612
- requireExactHash: z10.boolean(),
19613
- maxColorCountIncrease: z10.number().int().min(0),
19614
- maxNewColors: z10.number().int().min(0),
19615
- maxTransparencyDelta: z10.number().min(0).max(1),
19616
- maxPartialAlphaIncrease: z10.number().min(0).max(1),
19617
- maxEdgeDensityDelta: z10.number().min(0).max(1),
19618
- maxEdgeContrastDrop: z10.number().min(0).max(1),
19619
- maxIsolatedPixelIncrease: z10.number().min(0).max(1)
19844
+ var QualityToleranceSchema = z11.object({
19845
+ requireExactHash: z11.boolean(),
19846
+ maxColorCountIncrease: z11.number().int().min(0),
19847
+ maxNewColors: z11.number().int().min(0),
19848
+ maxTransparencyDelta: z11.number().min(0).max(1),
19849
+ maxPartialAlphaIncrease: z11.number().min(0).max(1),
19850
+ maxEdgeDensityDelta: z11.number().min(0).max(1),
19851
+ maxEdgeContrastDrop: z11.number().min(0).max(1),
19852
+ maxIsolatedPixelIncrease: z11.number().min(0).max(1)
19620
19853
  }).strict();
19621
- var ImageQualityMetricsSchema = z10.object({
19622
- sha256: z10.string().regex(SHA256_RE),
19623
- width: z10.number().int().min(1),
19624
- height: z10.number().int().min(1),
19625
- colorCount: z10.number().int().min(0),
19626
- colors: z10.array(z10.string().regex(HEX_RE)),
19627
- transparency: z10.number().min(0).max(1),
19628
- partialAlpha: z10.number().min(0).max(1),
19629
- edgeDensity: z10.number().min(0).max(1),
19630
- meanEdgeContrast: z10.number().min(0).max(1),
19631
- isolatedPixelRatio: z10.number().min(0).max(1)
19854
+ var ImageQualityMetricsSchema = z11.object({
19855
+ sha256: z11.string().regex(SHA256_RE),
19856
+ width: z11.number().int().min(1),
19857
+ height: z11.number().int().min(1),
19858
+ colorCount: z11.number().int().min(0),
19859
+ colors: z11.array(z11.string().regex(HEX_RE)),
19860
+ transparency: z11.number().min(0).max(1),
19861
+ partialAlpha: z11.number().min(0).max(1),
19862
+ edgeDensity: z11.number().min(0).max(1),
19863
+ meanEdgeContrast: z11.number().min(0).max(1),
19864
+ isolatedPixelRatio: z11.number().min(0).max(1)
19632
19865
  }).strict();
19633
- var QualityBaselineCaseSchema = z10.object({
19634
- id: z10.string().regex(/^[a-z0-9][a-z0-9/_-]*$/),
19866
+ var QualityBaselineCaseSchema = z11.object({
19867
+ id: z11.string().regex(/^[a-z0-9][a-z0-9/_-]*$/),
19635
19868
  file: QualityPathSchema,
19636
- record: z10.object({
19869
+ record: z11.object({
19637
19870
  path: QualityPathSchema,
19638
- sha256: z10.string().regex(SHA256_RE)
19871
+ sha256: z11.string().regex(SHA256_RE)
19639
19872
  }).strict().optional(),
19640
19873
  expected: ImageQualityMetricsSchema,
19641
19874
  tolerances: QualityToleranceSchema
19642
19875
  }).strict();
19643
- var QualityBaselineSchema = z10.object({
19644
- $schema: z10.string().url().optional(),
19645
- format: z10.literal("pixelkiln-quality-baseline"),
19646
- schemaVersion: z10.literal(1),
19647
- cases: z10.array(QualityBaselineCaseSchema).min(1)
19876
+ var QualityBaselineSchema = z11.object({
19877
+ $schema: z11.string().url().optional(),
19878
+ format: z11.literal("pixelkiln-quality-baseline"),
19879
+ schemaVersion: z11.literal(1),
19880
+ cases: z11.array(QualityBaselineCaseSchema).min(1)
19648
19881
  }).strict().superRefine((baseline, context) => {
19649
19882
  const ids = baseline.cases.map((entry) => entry.id);
19650
19883
  if (new Set(ids).size !== ids.length) {
19651
- context.addIssue({ code: z10.ZodIssueCode.custom, path: ["cases"], message: "duplicate case id" });
19884
+ context.addIssue({ code: z11.ZodIssueCode.custom, path: ["cases"], message: "duplicate case id" });
19652
19885
  }
19653
19886
  });
19654
- var QualityInputSchema = z10.object({
19655
- id: z10.string().regex(/^[a-z0-9][a-z0-9/_-]*$/),
19656
- path: z10.string().min(1),
19657
- record: z10.string().min(1).optional(),
19887
+ var QualityInputSchema = z11.object({
19888
+ id: z11.string().regex(/^[a-z0-9][a-z0-9/_-]*$/),
19889
+ path: z11.string().min(1),
19890
+ record: z11.string().min(1).optional(),
19658
19891
  tolerances: QualityToleranceSchema.partial().optional()
19659
19892
  }).strict();
19660
19893
  function message4(error) {
@@ -19742,10 +19975,10 @@ function measureDecodedImageQuality(png, digest2) {
19742
19975
  };
19743
19976
  }
19744
19977
  async function measureImageQuality(file) {
19745
- const absolute = path41.resolve(file);
19978
+ const absolute = path42.resolve(file);
19746
19979
  let bytes;
19747
19980
  try {
19748
- bytes = await readFile27(absolute);
19981
+ bytes = await readFile28(absolute);
19749
19982
  } catch (error) {
19750
19983
  throw new Error(`Cannot read quality image ${absolute}: ${message4(error)}`, { cause: error });
19751
19984
  }
@@ -19756,7 +19989,7 @@ async function measureImageQuality(file) {
19756
19989
  }
19757
19990
  }
19758
19991
  function resolveQualityInputs(raw, inputsFilePath) {
19759
- const parsed = z10.array(QualityInputSchema).min(1).safeParse(raw);
19992
+ const parsed = z11.array(QualityInputSchema).min(1).safeParse(raw);
19760
19993
  if (!parsed.success) {
19761
19994
  throw new Error(
19762
19995
  "--inputs must be a non-empty JSON array of quality cases:\n" + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
@@ -19764,11 +19997,11 @@ function resolveQualityInputs(raw, inputsFilePath) {
19764
19997
  }
19765
19998
  const ids = parsed.data.map((entry) => entry.id);
19766
19999
  if (new Set(ids).size !== ids.length) throw new Error("--inputs contains a duplicate quality case id.");
19767
- const root = path41.dirname(path41.resolve(inputsFilePath));
20000
+ const root = path42.dirname(path42.resolve(inputsFilePath));
19768
20001
  return parsed.data.map((entry) => ({
19769
20002
  id: entry.id,
19770
- file: path41.resolve(root, entry.path),
19771
- ...entry.record ? { record: path41.resolve(root, entry.record) } : {},
20003
+ file: path42.resolve(root, entry.path),
20004
+ ...entry.record ? { record: path42.resolve(root, entry.record) } : {},
19772
20005
  tolerances: QualityToleranceSchema.parse({
19773
20006
  ...DEFAULT_QUALITY_TOLERANCES,
19774
20007
  ...entry.tolerances
@@ -19776,7 +20009,7 @@ function resolveQualityInputs(raw, inputsFilePath) {
19776
20009
  }));
19777
20010
  }
19778
20011
  function portableRelative2(from, to) {
19779
- return path41.relative(from, path41.resolve(to)).split(path41.sep).join("/") || ".";
20012
+ return path42.relative(from, path42.resolve(to)).split(path42.sep).join("/") || ".";
19780
20013
  }
19781
20014
  async function verifyRecordForImage(recordPath, imagePath) {
19782
20015
  const verification = await verifyArtifactBundle(recordPath);
@@ -19791,20 +20024,20 @@ async function verifyRecordForImage(recordPath, imagePath) {
19791
20024
  if (record.kind !== "refine" || options === null || typeof options !== "object" || options.schema !== "pixelkiln-quality") {
19792
20025
  throw new Error(`Quality record ${recordPath} is not a PixelKiln refinement record.`);
19793
20026
  }
19794
- const outputs = record.outputs.map((output) => path41.resolve(path41.dirname(recordPath), output.path));
19795
- if (!outputs.includes(path41.resolve(imagePath))) {
20027
+ const outputs = record.outputs.map((output) => path42.resolve(path42.dirname(recordPath), output.path));
20028
+ if (!outputs.includes(path42.resolve(imagePath))) {
19796
20029
  throw new Error(`Quality record ${recordPath} does not own ${imagePath}.`);
19797
20030
  }
19798
20031
  }
19799
20032
  async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
19800
20033
  if (!inputs.length) throw new Error("Cannot snapshot an empty quality baseline.");
19801
- const absolute = path41.resolve(baselinePath);
20034
+ const absolute = path42.resolve(baselinePath);
19802
20035
  for (const input of inputs) {
19803
- if (path41.resolve(input.file) === absolute || input.record && path41.resolve(input.record) === absolute) {
20036
+ if (path42.resolve(input.file) === absolute || input.record && path42.resolve(input.record) === absolute) {
19804
20037
  throw new Error(`Quality baseline output would overwrite an input: ${absolute}.`);
19805
20038
  }
19806
20039
  }
19807
- const root = path41.dirname(absolute);
20040
+ const root = path42.dirname(absolute);
19808
20041
  const cases = [];
19809
20042
  for (const input of [...inputs].sort((a, b) => a.id.localeCompare(b.id))) {
19810
20043
  const expected = await measureImageQuality(input.file);
@@ -19831,8 +20064,8 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
19831
20064
  cases
19832
20065
  });
19833
20066
  const data = Buffer.from(JSON.stringify(baseline, null, 2) + "\n");
19834
- if (existsSync30(absolute) && !options.force) {
19835
- const current = await readFile27(absolute);
20067
+ if (existsSync32(absolute) && !options.force) {
20068
+ const current = await readFile28(absolute);
19836
20069
  if (!current.equals(data)) {
19837
20070
  throw new OverwriteRefusedError(
19838
20071
  `Quality baseline already exists with different content: ${absolute}. Pass --force to replace it.`,
@@ -19844,10 +20077,10 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
19844
20077
  return { baseline, path: absolute, changed: result.changed.length > 0 };
19845
20078
  }
19846
20079
  async function readQualityBaseline(baselinePath) {
19847
- const absolute = path41.resolve(baselinePath);
20080
+ const absolute = path42.resolve(baselinePath);
19848
20081
  let raw;
19849
20082
  try {
19850
- raw = JSON.parse(await readFile27(absolute, "utf8"));
20083
+ raw = JSON.parse(await readFile28(absolute, "utf8"));
19851
20084
  } catch (error) {
19852
20085
  throw new Error(`Could not read quality baseline ${absolute}: ${message4(error)}`, { cause: error });
19853
20086
  }
@@ -19917,12 +20150,12 @@ function regressionViolations(expected, actual, tolerances) {
19917
20150
  return { violations, warnings };
19918
20151
  }
19919
20152
  async function checkQualityBaseline(baselinePath) {
19920
- const absolute = path41.resolve(baselinePath);
20153
+ const absolute = path42.resolve(baselinePath);
19921
20154
  const baseline = await readQualityBaseline(absolute);
19922
- const root = path41.dirname(absolute);
20155
+ const root = path42.dirname(absolute);
19923
20156
  const cases = [];
19924
20157
  for (const entry of baseline.cases) {
19925
- const file = path41.resolve(root, entry.file);
20158
+ const file = path42.resolve(root, entry.file);
19926
20159
  let actual = null;
19927
20160
  const violations = [];
19928
20161
  const warnings = [];
@@ -19935,7 +20168,7 @@ async function checkQualityBaseline(baselinePath) {
19935
20168
  violations.push(message4(error));
19936
20169
  }
19937
20170
  if (entry.record) {
19938
- const recordPath = path41.resolve(root, entry.record.path);
20171
+ const recordPath = path42.resolve(root, entry.record.path);
19939
20172
  try {
19940
20173
  const recordHash = await sha256File(recordPath);
19941
20174
  if (recordHash !== entry.record.sha256) violations.push("quality record hash changed");
@@ -19977,15 +20210,15 @@ async function runQuality(args) {
19977
20210
  if (!args.out) throw new Error("quality snapshot needs --out <pixelkiln.quality.json>.");
19978
20211
  let raw;
19979
20212
  try {
19980
- raw = JSON.parse(await readFile28(path42.resolve(args.inputs), "utf8"));
20213
+ raw = JSON.parse(await readFile29(path43.resolve(args.inputs), "utf8"));
19981
20214
  } catch (error) {
19982
20215
  throw new Error(
19983
- `Could not read quality inputs ${path42.resolve(args.inputs)}: ${error instanceof Error ? error.message : String(error)}`,
20216
+ `Could not read quality inputs ${path43.resolve(args.inputs)}: ${error instanceof Error ? error.message : String(error)}`,
19984
20217
  { cause: error }
19985
20218
  );
19986
20219
  }
19987
20220
  const inputs = resolveQualityInputs(raw, args.inputs);
19988
- if (path42.resolve(args.inputs) === path42.resolve(args.out)) {
20221
+ if (path43.resolve(args.inputs) === path43.resolve(args.out)) {
19989
20222
  throw new Error("quality snapshot --out must not overwrite its --inputs file.");
19990
20223
  }
19991
20224
  const result = await snapshotQualityBaseline(inputs, args.out, { force: args.force });
@@ -19998,7 +20231,7 @@ async function runQuality(args) {
19998
20231
  }, null, 2));
19999
20232
  } else {
20000
20233
  log(
20001
- ` ${result.changed ? "wrote" : "unchanged"} ${path42.relative(process.cwd(), result.path)} (${result.baseline.cases.length} case(s))`
20234
+ ` ${result.changed ? "wrote" : "unchanged"} ${path43.relative(process.cwd(), result.path)} (${result.baseline.cases.length} case(s))`
20002
20235
  );
20003
20236
  log(` Review the tolerances, commit the baseline, then run quality check in CI.`);
20004
20237
  }
@@ -20024,54 +20257,54 @@ async function runQuality(args) {
20024
20257
  }
20025
20258
 
20026
20259
  // src/cli/commands/recipe.ts
20027
- import path44 from "path";
20260
+ import path45 from "path";
20028
20261
 
20029
20262
  // src/recipes.ts
20030
- import path43 from "path";
20031
- import { existsSync as existsSync31 } from "fs";
20032
- import { readdir as readdir3, readFile as readFile29 } from "fs/promises";
20033
- import { fileURLToPath } from "url";
20034
- import { z as z11 } from "zod";
20263
+ import path44 from "path";
20264
+ import { existsSync as existsSync33 } from "fs";
20265
+ import { readdir as readdir3, readFile as readFile30 } from "fs/promises";
20266
+ import { fileURLToPath as fileURLToPath2 } from "url";
20267
+ import { z as z12 } from "zod";
20035
20268
  var SHA256_RE2 = /^[0-9a-f]{64}$/;
20036
20269
  var RECIPE_ID_RE = /^[a-z0-9][a-z0-9-]*\/[a-z0-9][a-z0-9-]*$/;
20037
20270
  var VERSION_RE = /^\d+\.\d+\.\d+$/;
20038
20271
  var RECIPE_FILE = "pixelkiln.recipe.json";
20039
20272
  var RECIPE_DIR_TOKEN = "{{recipeDir}}";
20040
- var PortablePathSchema = z11.string().min(1).refine((value) => {
20041
- if (value.includes("\\") || path43.posix.isAbsolute(value)) return false;
20273
+ var PortablePathSchema = z12.string().min(1).refine((value) => {
20274
+ if (value.includes("\\") || path44.posix.isAbsolute(value)) return false;
20042
20275
  const parts = value.split("/");
20043
20276
  return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
20044
20277
  }, "expected a portable relative path without '.', '..', or backslashes");
20045
- var RecipeBindingSchema = z11.object({
20046
- nodeId: z11.string().min(1),
20047
- input: z11.string().min(1)
20278
+ var RecipeBindingSchema = z12.object({
20279
+ nodeId: z12.string().min(1),
20280
+ input: z12.string().min(1)
20048
20281
  }).strict();
20049
- var RecipeSchema = z11.object({
20050
- $schema: z11.string().url().optional(),
20051
- format: z11.literal("pixelkiln-recipe"),
20052
- schemaVersion: z11.literal(1),
20053
- id: z11.string().regex(RECIPE_ID_RE, "expected provider/name"),
20054
- version: z11.string().regex(VERSION_RE, "expected x.y.z"),
20055
- provider: z11.string().regex(/^[a-z0-9][a-z0-9-]*$/),
20056
- summary: z11.string().min(1).max(240),
20057
- files: z11.array(z11.object({
20282
+ var RecipeSchema = z12.object({
20283
+ $schema: z12.string().url().optional(),
20284
+ format: z12.literal("pixelkiln-recipe"),
20285
+ schemaVersion: z12.literal(1),
20286
+ id: z12.string().regex(RECIPE_ID_RE, "expected provider/name"),
20287
+ version: z12.string().regex(VERSION_RE, "expected x.y.z"),
20288
+ provider: z12.string().regex(/^[a-z0-9][a-z0-9-]*$/),
20289
+ summary: z12.string().min(1).max(240),
20290
+ files: z12.array(z12.object({
20058
20291
  path: PortablePathSchema,
20059
- role: z11.enum(["workflow", "reference"]),
20060
- sha256: z11.string().regex(SHA256_RE2)
20292
+ role: z12.enum(["workflow", "reference"]),
20293
+ sha256: z12.string().regex(SHA256_RE2)
20061
20294
  }).strict()).default([]),
20062
- models: z11.array(z11.object({
20295
+ models: z12.array(z12.object({
20063
20296
  path: PortablePathSchema,
20064
- sha256: z11.string().regex(SHA256_RE2),
20065
- source: z11.string().url(),
20066
- license: z11.string().min(1)
20297
+ sha256: z12.string().regex(SHA256_RE2),
20298
+ source: z12.string().url(),
20299
+ license: z12.string().min(1)
20067
20300
  }).strict()).default([]),
20068
- styleId: z11.string().regex(/^[a-z0-9][a-z0-9-]*$/),
20301
+ styleId: z12.string().regex(/^[a-z0-9][a-z0-9-]*$/),
20069
20302
  style: StyleSchema,
20070
- workflow: z11.object({
20303
+ workflow: z12.object({
20071
20304
  path: PortablePathSchema,
20072
- outputNodeId: z11.string().min(1),
20073
- numImages: z11.number().int().min(1).max(16),
20074
- bindings: z11.object({
20305
+ outputNodeId: z12.string().min(1),
20306
+ numImages: z12.number().int().min(1).max(16),
20307
+ bindings: z12.object({
20075
20308
  prompt: RecipeBindingSchema,
20076
20309
  width: RecipeBindingSchema.optional(),
20077
20310
  height: RecipeBindingSchema.optional(),
@@ -20082,67 +20315,67 @@ var RecipeSchema = z11.object({
20082
20315
  strength: RecipeBindingSchema.optional()
20083
20316
  }).strict()
20084
20317
  }).strict().optional(),
20085
- quality: z11.object({
20086
- stage: z11.enum(["composition-source", "production-candidate"]),
20087
- workingCanvas: z11.object({
20088
- width: z11.number().int().min(16),
20089
- height: z11.number().int().min(16)
20318
+ quality: z12.object({
20319
+ stage: z12.enum(["composition-source", "production-candidate"]),
20320
+ workingCanvas: z12.object({
20321
+ width: z12.number().int().min(16),
20322
+ height: z12.number().int().min(16)
20090
20323
  }).strict(),
20091
- recommendedNativeSize: z11.object({
20092
- min: z11.number().int().min(1),
20093
- max: z11.number().int().min(1)
20324
+ recommendedNativeSize: z12.object({
20325
+ min: z12.number().int().min(1),
20326
+ max: z12.number().int().min(1)
20094
20327
  }).strict(),
20095
- paletteColors: z11.object({
20096
- min: z11.number().int().min(1),
20097
- max: z11.number().int().min(1)
20328
+ paletteColors: z12.object({
20329
+ min: z12.number().int().min(1),
20330
+ max: z12.number().int().min(1)
20098
20331
  }).strict(),
20099
- background: z11.enum(["opaque", "transparent", "full-bleed"]),
20100
- checks: z11.array(z11.enum([
20332
+ background: z12.enum(["opaque", "transparent", "full-bleed"]),
20333
+ checks: z12.array(z12.enum([
20101
20334
  "prompt-coverage",
20102
20335
  "native-grid",
20103
20336
  "final-palette",
20104
20337
  "alpha-edge",
20105
20338
  "human-1x"
20106
20339
  ])).min(1),
20107
- notes: z11.array(z11.string().min(1)).default([])
20340
+ notes: z12.array(z12.string().min(1)).default([])
20108
20341
  }).strict(),
20109
- integrity: z11.object({
20110
- algorithm: z11.literal("sha256"),
20111
- digest: z11.string().regex(SHA256_RE2)
20342
+ integrity: z12.object({
20343
+ algorithm: z12.literal("sha256"),
20344
+ digest: z12.string().regex(SHA256_RE2)
20112
20345
  }).strict()
20113
20346
  }).strict().superRefine((recipe, context) => {
20114
20347
  if (recipe.style.provider && recipe.style.provider !== recipe.provider) {
20115
20348
  context.addIssue({
20116
- code: z11.ZodIssueCode.custom,
20349
+ code: z12.ZodIssueCode.custom,
20117
20350
  path: ["style", "provider"],
20118
20351
  message: "must match the recipe provider"
20119
20352
  });
20120
20353
  }
20121
20354
  const filePaths = recipe.files.map((file) => file.path);
20122
20355
  if (new Set(filePaths).size !== filePaths.length) {
20123
- context.addIssue({ code: z11.ZodIssueCode.custom, path: ["files"], message: "duplicate file path" });
20356
+ context.addIssue({ code: z12.ZodIssueCode.custom, path: ["files"], message: "duplicate file path" });
20124
20357
  }
20125
20358
  const modelPaths = recipe.models.map((model) => model.path);
20126
20359
  if (new Set(modelPaths).size !== modelPaths.length) {
20127
- context.addIssue({ code: z11.ZodIssueCode.custom, path: ["models"], message: "duplicate model path" });
20360
+ context.addIssue({ code: z12.ZodIssueCode.custom, path: ["models"], message: "duplicate model path" });
20128
20361
  }
20129
20362
  if (filePaths.includes(RECIPE_FILE)) {
20130
20363
  context.addIssue({
20131
- code: z11.ZodIssueCode.custom,
20364
+ code: z12.ZodIssueCode.custom,
20132
20365
  path: ["files"],
20133
20366
  message: `${RECIPE_FILE} cannot hash itself`
20134
20367
  });
20135
20368
  }
20136
20369
  if (recipe.quality.recommendedNativeSize.min > recipe.quality.recommendedNativeSize.max) {
20137
20370
  context.addIssue({
20138
- code: z11.ZodIssueCode.custom,
20371
+ code: z12.ZodIssueCode.custom,
20139
20372
  path: ["quality", "recommendedNativeSize"],
20140
20373
  message: "min cannot exceed max"
20141
20374
  });
20142
20375
  }
20143
20376
  if (recipe.quality.paletteColors.min > recipe.quality.paletteColors.max) {
20144
20377
  context.addIssue({
20145
- code: z11.ZodIssueCode.custom,
20378
+ code: z12.ZodIssueCode.custom,
20146
20379
  path: ["quality", "paletteColors"],
20147
20380
  message: "min cannot exceed max"
20148
20381
  });
@@ -20151,7 +20384,7 @@ var RecipeSchema = z11.object({
20151
20384
  (file) => file.path === recipe.workflow.path && file.role === "workflow"
20152
20385
  )) {
20153
20386
  context.addIssue({
20154
- code: z11.ZodIssueCode.custom,
20387
+ code: z12.ZodIssueCode.custom,
20155
20388
  path: ["workflow", "path"],
20156
20389
  message: "must name a file whose role is workflow"
20157
20390
  });
@@ -20159,7 +20392,7 @@ var RecipeSchema = z11.object({
20159
20392
  if (recipe.provider === "comfyui") {
20160
20393
  if (!recipe.workflow) {
20161
20394
  context.addIssue({
20162
- code: z11.ZodIssueCode.custom,
20395
+ code: z12.ZodIssueCode.custom,
20163
20396
  path: ["workflow"],
20164
20397
  message: "ComfyUI recipes require workflow metadata"
20165
20398
  });
@@ -20168,7 +20401,7 @@ var RecipeSchema = z11.object({
20168
20401
  const options = recipe.style.providerOptions.comfyui;
20169
20402
  if (!options || typeof options !== "object" || Array.isArray(options)) {
20170
20403
  context.addIssue({
20171
- code: z11.ZodIssueCode.custom,
20404
+ code: z12.ZodIssueCode.custom,
20172
20405
  path: ["style", "providerOptions", "comfyui"],
20173
20406
  message: "ComfyUI recipes require ComfyUI provider options"
20174
20407
  });
@@ -20182,7 +20415,7 @@ var RecipeSchema = z11.object({
20182
20415
  };
20183
20416
  if (JSON.stringify(canonical3(options)) !== JSON.stringify(canonical3(expected))) {
20184
20417
  context.addIssue({
20185
- code: z11.ZodIssueCode.custom,
20418
+ code: z12.ZodIssueCode.custom,
20186
20419
  path: ["style", "providerOptions", "comfyui"],
20187
20420
  message: "must match workflow metadata and use {{recipeDir}} for workflowFile"
20188
20421
  });
@@ -20214,10 +20447,10 @@ function message5(error) {
20214
20447
  return error instanceof Error ? error.message : String(error);
20215
20448
  }
20216
20449
  async function readRecipeFile(recipePath, bundled) {
20217
- const absolute = path43.resolve(recipePath);
20450
+ const absolute = path44.resolve(recipePath);
20218
20451
  let raw;
20219
20452
  try {
20220
- raw = JSON.parse(await readFile29(absolute, "utf8"));
20453
+ raw = JSON.parse(await readFile30(absolute, "utf8"));
20221
20454
  } catch (error) {
20222
20455
  throw new Error(`Could not read recipe ${absolute}: ${message5(error)}`, { cause: error });
20223
20456
  }
@@ -20228,24 +20461,24 @@ async function readRecipeFile(recipePath, bundled) {
20228
20461
  ` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
20229
20462
  );
20230
20463
  }
20231
- return { recipe: parsed.data, authored: raw, path: absolute, dir: path43.dirname(absolute), bundled };
20464
+ return { recipe: parsed.data, authored: raw, path: absolute, dir: path44.dirname(absolute), bundled };
20232
20465
  }
20233
20466
  async function walkRecipeFiles(root) {
20234
- if (!existsSync31(root)) return [];
20467
+ if (!existsSync33(root)) return [];
20235
20468
  const out = [];
20236
20469
  for (const entry of await readdir3(root, { withFileTypes: true })) {
20237
- const resolved = path43.join(root, entry.name);
20470
+ const resolved = path44.join(root, entry.name);
20238
20471
  if (entry.isDirectory()) out.push(...await walkRecipeFiles(resolved));
20239
20472
  else if (entry.isFile() && entry.name === RECIPE_FILE) out.push(resolved);
20240
20473
  }
20241
20474
  return out;
20242
20475
  }
20243
20476
  function bundledRecipeRoot() {
20244
- return fileURLToPath(new URL("../recipes/", import.meta.url));
20477
+ return fileURLToPath2(new URL("../recipes/", import.meta.url));
20245
20478
  }
20246
20479
  async function listBundledRecipes(root = bundledRecipeRoot()) {
20247
20480
  const recipes = await Promise.all(
20248
- (await walkRecipeFiles(path43.resolve(root))).map((file) => readRecipeFile(file, true))
20481
+ (await walkRecipeFiles(path44.resolve(root))).map((file) => readRecipeFile(file, true))
20249
20482
  );
20250
20483
  return recipes.sort(
20251
20484
  (a, b) => a.recipe.id.localeCompare(b.recipe.id) || compareVersions(b.recipe.version, a.recipe.version)
@@ -20268,9 +20501,9 @@ function parseSelector(selector) {
20268
20501
  return { id: match[1], version: match[2] };
20269
20502
  }
20270
20503
  async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
20271
- const local = path43.resolve(target);
20272
- if (existsSync31(local)) {
20273
- const recipePath = path43.basename(local) === RECIPE_FILE ? local : path43.join(local, RECIPE_FILE);
20504
+ const local = path44.resolve(target);
20505
+ if (existsSync33(local)) {
20506
+ const recipePath = path44.basename(local) === RECIPE_FILE ? local : path44.join(local, RECIPE_FILE);
20274
20507
  return readRecipeFile(recipePath, false);
20275
20508
  }
20276
20509
  if (target.startsWith(".") || target.startsWith("/") || target.includes("\\")) {
@@ -20284,8 +20517,8 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
20284
20517
  return matches[0];
20285
20518
  }
20286
20519
  async function verifyFile(root, file) {
20287
- const absolute = path43.resolve(root, ...file.path.split("/"));
20288
- if (!existsSync31(absolute)) {
20520
+ const absolute = path44.resolve(root, ...file.path.split("/"));
20521
+ if (!existsSync33(absolute)) {
20289
20522
  return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
20290
20523
  }
20291
20524
  const actualSha256 = await sha256File(absolute);
@@ -20300,7 +20533,7 @@ async function verifyRecipe(target, options = {}) {
20300
20533
  const loaded = await resolveRecipe(target, options.bundledRoot);
20301
20534
  const actualIntegrity = recipeDigest(loaded.authored);
20302
20535
  const files = await Promise.all(loaded.recipe.files.map((file) => verifyFile(loaded.dir, file)));
20303
- const modelRoot = options.modelRoot ? path43.resolve(options.modelRoot) : null;
20536
+ const modelRoot = options.modelRoot ? path44.resolve(options.modelRoot) : null;
20304
20537
  const models = modelRoot ? await Promise.all(loaded.recipe.models.map(async (model) => ({
20305
20538
  ...await verifyFile(modelRoot, model),
20306
20539
  source: model.source,
@@ -20334,7 +20567,7 @@ async function verifyRecipe(target, options = {}) {
20334
20567
  };
20335
20568
  }
20336
20569
  function renderStyle(style, destination, cwd) {
20337
- const portableDir = path43.relative(cwd, destination).split(path43.sep).join("/") || ".";
20570
+ const portableDir = path44.relative(cwd, destination).split(path44.sep).join("/") || ".";
20338
20571
  const replace = (value) => {
20339
20572
  if (typeof value === "string") return value.replaceAll(RECIPE_DIR_TOKEN, portableDir);
20340
20573
  if (Array.isArray(value)) return value.map(replace);
@@ -20346,25 +20579,25 @@ function renderStyle(style, destination, cwd) {
20346
20579
  return replace(style);
20347
20580
  }
20348
20581
  async function installRecipe(target, options = {}) {
20349
- const cwd = path43.resolve(options.cwd ?? process.cwd());
20582
+ const cwd = path44.resolve(options.cwd ?? process.cwd());
20350
20583
  const loaded = await resolveRecipe(target, options.bundledRoot);
20351
20584
  const verification = await verifyRecipe(loaded.path);
20352
20585
  if (!verification.ok) {
20353
20586
  throw new Error(`Recipe ${loaded.recipe.id}@${loaded.recipe.version} failed integrity verification.`);
20354
20587
  }
20355
- const destination = options.out ? path43.resolve(cwd, options.out) : path43.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
20588
+ const destination = options.out ? path44.resolve(cwd, options.out) : path44.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
20356
20589
  const sources = [
20357
20590
  { path: RECIPE_FILE, source: loaded.path },
20358
- ...loaded.recipe.files.map((file) => ({ path: file.path, source: path43.join(loaded.dir, ...file.path.split("/")) }))
20591
+ ...loaded.recipe.files.map((file) => ({ path: file.path, source: path44.join(loaded.dir, ...file.path.split("/")) }))
20359
20592
  ];
20360
20593
  const files = await Promise.all(sources.map(async (file) => ({
20361
- path: path43.join(destination, ...file.path.split("/")),
20362
- data: await readFile29(file.source)
20594
+ path: path44.join(destination, ...file.path.split("/")),
20595
+ data: await readFile30(file.source)
20363
20596
  })));
20364
20597
  if (!options.force) {
20365
20598
  for (const file of files) {
20366
- if (!existsSync31(file.path)) continue;
20367
- const current = await readFile29(file.path);
20599
+ if (!existsSync33(file.path)) continue;
20600
+ const current = await readFile30(file.path);
20368
20601
  if (!current.equals(file.data)) {
20369
20602
  throw new OverwriteRefusedError(
20370
20603
  `Recipe destination has local changes: ${file.path}. Choose another --out or pass --force to replace declared recipe files.`,
@@ -20458,14 +20691,14 @@ async function runRecipe(args) {
20458
20691
  }, null, 2));
20459
20692
  } else {
20460
20693
  log(` installed ${result.recipe.id}@${result.recipe.version}`);
20461
- log(` ${path44.relative(process.cwd(), result.destination) || "."}`);
20694
+ log(` ${path45.relative(process.cwd(), result.destination) || "."}`);
20462
20695
  log(`
20463
20696
  Add this entry under your manifest's styles object:`);
20464
20697
  log(JSON.stringify({ [result.styleId]: result.style }, null, 2));
20465
20698
  if (result.recipe.models.length) {
20466
20699
  log(`
20467
20700
  Models are not downloaded automatically. Verify them with:`);
20468
- log(` pixelkiln recipe verify ${path44.relative(process.cwd(), result.destination)} --model-root <ComfyUI/models>`);
20701
+ log(` pixelkiln recipe verify ${path45.relative(process.cwd(), result.destination)} --model-root <ComfyUI/models>`);
20469
20702
  }
20470
20703
  }
20471
20704
  return;
@@ -20473,7 +20706,7 @@ async function runRecipe(args) {
20473
20706
  }
20474
20707
 
20475
20708
  // src/cli/commands/refine.ts
20476
- import path45 from "path";
20709
+ import path46 from "path";
20477
20710
  async function runRefine(args) {
20478
20711
  if (args.from || args.subcommand === "approve") {
20479
20712
  if (!args.from) {
@@ -20503,12 +20736,12 @@ async function runRefine(args) {
20503
20736
  ` recovered ${result3.detection.columns}x${result3.detection.rows} native grid (${result3.detection.confidence}: ${result3.detection.consensus})`
20504
20737
  );
20505
20738
  log(` enforced ${result3.palette.length}-color palette without dithering`);
20506
- log(` wrote ${path45.relative(process.cwd(), result3.output)}`);
20507
- log(` quality record: ${path45.relative(process.cwd(), result3.record)}`);
20739
+ log(` wrote ${path46.relative(process.cwd(), result3.output)}`);
20740
+ log(` quality record: ${path46.relative(process.cwd(), result3.record)}`);
20508
20741
  log(`
20509
20742
  Automated checks passed. Human 1\xD7 review is still required:`);
20510
20743
  log(
20511
- ` pixelkiln refine approve --from ${path45.relative(process.cwd(), result3.record)} --reviewer "Your Name"`
20744
+ ` pixelkiln refine approve --from ${path46.relative(process.cwd(), result3.record)} --reviewer "Your Name"`
20512
20745
  );
20513
20746
  }
20514
20747
  return;
@@ -20536,7 +20769,7 @@ async function runRefine(args) {
20536
20769
  if (args.json) log(JSON.stringify(result3, null, 2));
20537
20770
  else {
20538
20771
  log(
20539
- result3.options.frameSet ? ` approved ${result3.outputs.length} frames in ${path45.relative(process.cwd(), result3.record)} by ${args.reviewer.trim()}` : ` approved ${path45.relative(process.cwd(), result3.output)} by ${args.reviewer.trim()}`
20772
+ result3.options.frameSet ? ` approved ${result3.outputs.length} frames in ${path46.relative(process.cwd(), result3.record)} by ${args.reviewer.trim()}` : ` approved ${path46.relative(process.cwd(), result3.output)} by ${args.reviewer.trim()}`
20540
20773
  );
20541
20774
  }
20542
20775
  return;
@@ -20545,7 +20778,7 @@ async function runRefine(args) {
20545
20778
  if (args.json) {
20546
20779
  log(JSON.stringify(result2, null, 2));
20547
20780
  } else {
20548
- log(` ${result2.safe ? "release-ready" : "not release-ready"}: ${path45.relative(process.cwd(), result2.output || result2.record)}`);
20781
+ log(` ${result2.safe ? "release-ready" : "not release-ready"}: ${path46.relative(process.cwd(), result2.output || result2.record)}`);
20549
20782
  for (const reason of result2.reasons) log(` ${reason}`);
20550
20783
  }
20551
20784
  if (!result2.safe) process.exitCode = 1;
@@ -20591,7 +20824,7 @@ async function runRefine(args) {
20591
20824
  log(` ${item.state.padEnd(16)} ${item.key} ${item.reason}`);
20592
20825
  if (item.state === "needs-approval") {
20593
20826
  log(
20594
- ` pixelkiln refine approve --from ${path45.relative(process.cwd(), item.record)} --reviewer "Your Name"`
20827
+ ` pixelkiln refine approve --from ${path46.relative(process.cwd(), item.record)} --reviewer "Your Name"`
20595
20828
  );
20596
20829
  }
20597
20830
  }
@@ -20602,9 +20835,9 @@ async function runRefine(args) {
20602
20835
  }
20603
20836
 
20604
20837
  // src/cli/commands/skeleton.ts
20605
- import path46 from "path";
20606
- import { existsSync as existsSync32 } from "fs";
20607
- import { readFile as readFile30, writeFile as writeFile15 } from "fs/promises";
20838
+ import path47 from "path";
20839
+ import { existsSync as existsSync34 } from "fs";
20840
+ import { readFile as readFile31, writeFile as writeFile16 } from "fs/promises";
20608
20841
 
20609
20842
  // src/skeleton-preview.ts
20610
20843
  var RIGHT = [255, 140, 40, 255];
@@ -20712,23 +20945,19 @@ var Canvas = class {
20712
20945
  };
20713
20946
 
20714
20947
  // src/cli/commands/skeleton.ts
20715
- var ESTIMATE_SKELETON_SIZES = [16, 32, 64, 128, 256];
20716
20948
  async function runEstimateSkeleton(args) {
20717
20949
  const image = args.target;
20718
20950
  if (!image) throw new UsageError("estimate-skeleton needs an image path: pixelkiln estimate-skeleton <image> [--out keypoints.json]");
20719
- if (args.out && existsSync32(path46.resolve(args.out)) && !args.force) {
20951
+ if (args.out && existsSync34(path47.resolve(args.out)) && !args.force) {
20720
20952
  throw new UsageError(`${args.out} already exists; pass --force to replace it`);
20721
20953
  }
20722
- loadEnvFiles(path46.dirname(path46.resolve(args.manifest)));
20954
+ loadEnvFiles(path47.dirname(path47.resolve(args.manifest)));
20723
20955
  loadEnvFiles(process.cwd());
20724
- const bytes = await readFile30(path46.resolve(image));
20956
+ const bytes = await readFile31(path47.resolve(image));
20725
20957
  const metadata = imageMetadata(bytes);
20726
20958
  if (!metadata) throw new UsageError(`${image} is not a readable PNG or JPEG`);
20727
- if (metadata.width !== metadata.height || !ESTIMATE_SKELETON_SIZES.includes(metadata.width)) {
20728
- throw new UsageError(
20729
- `${image} is ${metadata.width}x${metadata.height}; estimate-skeleton takes a square image of ${ESTIMATE_SKELETON_SIZES.join(", ")} pixels`
20730
- );
20731
- }
20959
+ const sizeProblem = estimateSkeletonSizeProblem(metadata.width, metadata.height);
20960
+ if (sizeProblem) throw new UsageError(`${image} is ${sizeProblem}`);
20732
20961
  const client2 = clientFromEnv();
20733
20962
  const res = await client2.estimateSkeleton({
20734
20963
  image: { base64: bytes.toString("base64"), format: metadata.format }
@@ -20736,7 +20965,7 @@ async function runEstimateSkeleton(args) {
20736
20965
  const frames = args.frames ?? DEFAULT_SCAFFOLD_FRAMES;
20737
20966
  const json = JSON.stringify(scaffoldSkeletonSet(res.keypoints, frames), null, 2);
20738
20967
  if (args.out) {
20739
- await writeFile15(path46.resolve(args.out), json + "\n");
20968
+ await writeFile16(path47.resolve(args.out), json + "\n");
20740
20969
  log(` wrote ${args.out}: the estimated pose and ${frames} frames to edit`);
20741
20970
  log(` preview it with: pixelkiln skeleton-preview ${args.out} --from ${image}`);
20742
20971
  } else {
@@ -20749,8 +20978,8 @@ async function runSkeletonPreview(args) {
20749
20978
  let sourceFile;
20750
20979
  let label;
20751
20980
  if (target.endsWith(".json")) {
20752
- keypointsFile = path46.resolve(target);
20753
- sourceFile = args.from ? path46.resolve(args.from) : void 0;
20981
+ keypointsFile = path47.resolve(target);
20982
+ sourceFile = args.from ? path47.resolve(args.from) : void 0;
20754
20983
  label = target;
20755
20984
  } else {
20756
20985
  const loaded = await loadManifest(args.manifest);
@@ -20762,31 +20991,31 @@ async function runSkeletonPreview(args) {
20762
20991
  );
20763
20992
  }
20764
20993
  keypointsFile = spec.revision.keypointsFile;
20765
- sourceFile = args.from ? path46.resolve(args.from) : spec.revision.sourceFile;
20994
+ sourceFile = args.from ? path47.resolve(args.from) : spec.revision.sourceFile;
20766
20995
  label = `${spec.styleId}/${spec.assetId}`;
20767
20996
  }
20768
- if (!existsSync32(keypointsFile)) throw new UsageError(`Keypoints file does not exist yet: ${keypointsFile}`);
20997
+ if (!existsSync34(keypointsFile)) throw new UsageError(`Keypoints file does not exist yet: ${keypointsFile}`);
20769
20998
  let json;
20770
20999
  try {
20771
- json = JSON.parse(await readFile30(keypointsFile, "utf8"));
21000
+ json = JSON.parse(await readFile31(keypointsFile, "utf8"));
20772
21001
  } catch (error) {
20773
21002
  throw new UsageError(`${keypointsFile} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
20774
21003
  }
20775
21004
  const set = parseSkeletonSet(json, keypointsFile);
20776
21005
  let source;
20777
21006
  if (sourceFile) {
20778
- if (!existsSync32(sourceFile)) {
21007
+ if (!existsSync34(sourceFile)) {
20779
21008
  log(` source image does not exist yet, drawing on a plain ground: ${sourceFile}`);
20780
21009
  } else {
20781
- const bytes = await readFile30(sourceFile);
21010
+ const bytes = await readFile31(sourceFile);
20782
21011
  if (imageMetadata(bytes)?.format !== "png") throw new UsageError(`${sourceFile} must be a PNG to preview over`);
20783
21012
  source = decodePng(bytes);
20784
21013
  }
20785
21014
  }
20786
- const out = path46.resolve(args.out ?? keypointsFile.replace(/\.json$/, "") + ".preview.png");
20787
- await writeFile15(out, renderSkeletonSheet(set, { source }));
21015
+ const out = path47.resolve(args.out ?? keypointsFile.replace(/\.json$/, "") + ".preview.png");
21016
+ await writeFile16(out, renderSkeletonSheet(set, { source }));
20788
21017
  log(` ${label}: ${set.frames.length} frames after the starting pose`);
20789
- const shown = path46.relative(process.cwd(), out);
21018
+ const shown = path47.relative(process.cwd(), out);
20790
21019
  log(` wrote ${shown.startsWith("..") ? out : shown}`);
20791
21020
  }
20792
21021
 
@@ -20832,15 +21061,15 @@ async function runTools(args) {
20832
21061
  }
20833
21062
 
20834
21063
  // src/cli/commands/workspace.ts
20835
- import path47 from "path";
20836
- import { existsSync as existsSync33 } from "fs";
21064
+ import path48 from "path";
21065
+ import { existsSync as existsSync35 } from "fs";
20837
21066
  async function runWorkspace(args) {
20838
- const workspacePath = path47.resolve(args.workspace ?? "pixelkiln.workspace.json");
20839
- const dir = path47.dirname(workspacePath);
21067
+ const workspacePath = path48.resolve(args.workspace ?? "pixelkiln.workspace.json");
21068
+ const dir = path48.dirname(workspacePath);
20840
21069
  if (args.subcommand === "add") {
20841
- const manifestPath = path47.resolve(args.target);
21070
+ const manifestPath = path48.resolve(args.target);
20842
21071
  const loadedTarget = await loadManifest(manifestPath);
20843
- const lockPath = args.explicitLock ? path47.resolve(args.explicitLock) : path47.join(path47.dirname(manifestPath), "pixelkiln.lock.json");
21072
+ const lockPath = args.explicitLock ? path48.resolve(args.explicitLock) : path48.join(path48.dirname(manifestPath), "pixelkiln.lock.json");
20844
21073
  const ws = await loadWorkspace(workspacePath);
20845
21074
  const id = args.name ?? loadedTarget.manifest.name;
20846
21075
  if (ws.projects.some((p) => p.id === id)) {
@@ -20860,10 +21089,10 @@ async function runWorkspace(args) {
20860
21089
  ...args.account ? { account: args.account } : {}
20861
21090
  };
20862
21091
  await saveWorkspace(workspacePath, { version: 1, projects: [...ws.projects, project] });
20863
- log(` registered "${id}" in ${path47.relative(process.cwd(), workspacePath)}`);
21092
+ log(` registered "${id}" in ${path48.relative(process.cwd(), workspacePath)}`);
20864
21093
  log(` manifest: ${project.manifest}`);
20865
21094
  log(` lock: ${project.lock}`);
20866
- if (!existsSync33(lockPath)) {
21095
+ if (!existsSync35(lockPath)) {
20867
21096
  log(
20868
21097
  ` warning: no lockfile there yet; this project contributes no claims until one is generated`
20869
21098
  );
@@ -20872,7 +21101,7 @@ async function runWorkspace(args) {
20872
21101
  }
20873
21102
  if (args.subcommand === "remove") {
20874
21103
  const ws = await loadWorkspace(workspacePath);
20875
- const resolvedTarget = path47.resolve(args.target);
21104
+ const resolvedTarget = path48.resolve(args.target);
20876
21105
  const match = ws.projects.find(
20877
21106
  (p) => p.id === args.target || resolveProject(dir, p).manifestPath === resolvedTarget
20878
21107
  );
@@ -20883,11 +21112,11 @@ async function runWorkspace(args) {
20883
21112
  version: 1,
20884
21113
  projects: ws.projects.filter((p) => p !== match)
20885
21114
  });
20886
- log(` removed "${match.id}" from ${path47.relative(process.cwd(), workspacePath)}`);
21115
+ log(` removed "${match.id}" from ${path48.relative(process.cwd(), workspacePath)}`);
20887
21116
  return;
20888
21117
  }
20889
21118
  if (args.subcommand === "list") {
20890
- if (!existsSync33(workspacePath)) {
21119
+ if (!existsSync35(workspacePath)) {
20891
21120
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
20892
21121
  }
20893
21122
  const ws = await loadWorkspace(workspacePath);
@@ -20895,9 +21124,9 @@ async function runWorkspace(args) {
20895
21124
  if (args.json) {
20896
21125
  log(JSON.stringify({ version: 1, workspace: workspacePath, projects: ws.projects, diagnostics }, null, 2));
20897
21126
  } else if (!ws.projects.length) {
20898
- log(` no projects registered in ${path47.relative(process.cwd(), workspacePath)}`);
21127
+ log(` no projects registered in ${path48.relative(process.cwd(), workspacePath)}`);
20899
21128
  } else {
20900
- log(` ${ws.projects.length} project(s) in ${path47.relative(process.cwd(), workspacePath)}:`);
21129
+ log(` ${ws.projects.length} project(s) in ${path48.relative(process.cwd(), workspacePath)}:`);
20901
21130
  for (const p of ws.projects) {
20902
21131
  log(` ${p.id.padEnd(24)} ${p.manifest.padEnd(40)} (${p.provider}${p.account ? `, ${p.account}` : ""})`);
20903
21132
  }
@@ -20907,7 +21136,7 @@ async function runWorkspace(args) {
20907
21136
  return;
20908
21137
  }
20909
21138
  if (args.subcommand === "status") {
20910
- if (!existsSync33(workspacePath)) {
21139
+ if (!existsSync35(workspacePath)) {
20911
21140
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
20912
21141
  }
20913
21142
  const ws = await loadWorkspace(workspacePath);
@@ -20915,7 +21144,7 @@ async function runWorkspace(args) {
20915
21144
  if (args.json) {
20916
21145
  log(JSON.stringify({ ...report, workspace: workspacePath }, null, 2));
20917
21146
  } else {
20918
- log(` workspace: ${path47.relative(process.cwd(), workspacePath)}`);
21147
+ log(` workspace: ${path48.relative(process.cwd(), workspacePath)}`);
20919
21148
  for (const p of report.projects) {
20920
21149
  if (p.error) {
20921
21150
  log(`
@@ -20969,8 +21198,8 @@ async function runHelp() {
20969
21198
  async function runVersion() {
20970
21199
  for (const up of ["../package.json", "../../package.json"]) {
20971
21200
  const url = new URL(up, import.meta.url);
20972
- if (!existsSync34(url)) continue;
20973
- const pkg = JSON.parse(await readFile31(url, "utf8"));
21201
+ if (!existsSync36(url)) continue;
21202
+ const pkg = JSON.parse(await readFile32(url, "utf8"));
20974
21203
  if (pkg.name === "pixelkiln") {
20975
21204
  log(`${pkg.name} ${pkg.version}`);
20976
21205
  return;