pixelkiln 0.70.0 → 0.71.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -67,7 +67,7 @@ function exitCodeFor(err) {
67
67
  }
68
68
 
69
69
  // src/cli/main.ts
70
- import { existsSync as existsSync35 } from "fs";
70
+ import { existsSync as existsSync36 } from "fs";
71
71
  import { readFile as readFile32 } from "fs/promises";
72
72
 
73
73
  // src/cli/args.ts
@@ -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
@@ -14373,7 +14376,7 @@ async function runEdit(args) {
14373
14376
 
14374
14377
  // src/cli/commands/gallery.ts
14375
14378
  import path33 from "path";
14376
- import { existsSync as existsSync24 } from "fs";
14379
+ import { existsSync as existsSync25 } from "fs";
14377
14380
 
14378
14381
  // src/gallery/snapshot.ts
14379
14382
  import { createHash as createHash3 } from "crypto";
@@ -14416,6 +14419,16 @@ var SkeletonAnimationRequestSchema = z8.object({
14416
14419
  /** Replace an existing `keypointsFile` with `set`. */
14417
14420
  overwrite: z8.boolean().optional()
14418
14421
  }).strict();
14422
+ var SkeletonKeypointsUpdateSchema = z8.object({
14423
+ action: z8.literal("update-skeleton-keypoints"),
14424
+ project: z8.string().min(1).optional(),
14425
+ styleId: z8.string().min(1),
14426
+ assetId: z8.string().min(1),
14427
+ /** The file's SHA-256 when the page loaded it; anything else on disk is a concurrent edit. */
14428
+ expectedSha256: z8.string().regex(/^[0-9a-f]{64}$/),
14429
+ set: SkeletonSetSchema
14430
+ }).strict();
14431
+ var DEFAULT_ESTIMATE_LIMIT = 10;
14419
14432
  function skeletonSourceFile(spec) {
14420
14433
  if (spec.source) return path26.resolve(spec.root, spec.source);
14421
14434
  if (spec.quality && existsSync18(spec.quality.outFile)) return spec.quality.outFile;
@@ -14424,7 +14437,12 @@ function skeletonSourceFile(spec) {
14424
14437
  function createGallerySkeletonHandlers(opts) {
14425
14438
  const log2 = opts.onProgress ?? (() => {
14426
14439
  });
14440
+ const limit = opts.limit ?? DEFAULT_ESTIMATE_LIMIT;
14441
+ let used = 0;
14442
+ const spent = {};
14443
+ const status = () => ({ used, limit, spent: { ...spent } });
14427
14444
  return {
14445
+ status,
14428
14446
  async estimate(body) {
14429
14447
  const parsed = SkeletonEstimateRequestSchema.safeParse(body);
14430
14448
  if (!parsed.success) throw new ManifestEditError(`invalid estimate request: ${parsed.error.issues[0]?.message}`);
@@ -14440,16 +14458,33 @@ function createGallerySkeletonHandlers(opts) {
14440
14458
  if (!metadata) throw new ManifestEditError(`${path26.basename(file)} is not a readable PNG or JPEG`);
14441
14459
  const problem = estimateSkeletonSizeProblem(metadata.width, metadata.height);
14442
14460
  if (problem) throw new ManifestEditError(`${key} is ${problem}`);
14461
+ if (used >= limit) {
14462
+ throw Object.assign(
14463
+ new Error(`this gallery has made its ${limit} skeleton estimate${limit === 1 ? "" : "s"}; restart it with --estimate-limit to allow more`),
14464
+ { status: 429 }
14465
+ );
14466
+ }
14443
14467
  const client2 = (opts.client ?? clientFromEnv)();
14468
+ used++;
14444
14469
  const res = await client2.estimateSkeleton({ image: { base64: bytes.toString("base64"), format: metadata.format } });
14445
- log2(` estimated a skeleton for ${key} (PixelLab estimate-skeleton, not budgeted)`);
14470
+ const billed = billedAmount(res.usage);
14471
+ if (billed) spent[billed.unit] = (spent[billed.unit] ?? 0) + billed.amount;
14472
+ log2(` estimated a skeleton for ${key} (PixelLab estimate-skeleton, ${used}/${limit} this session${billed ? `, billed ${billed.amount} ${billed.unit}` : ""})`);
14446
14473
  return {
14447
14474
  set: scaffoldSkeletonSet(res.keypoints, request.frames ?? DEFAULT_SCAFFOLD_FRAMES),
14448
- source: path26.relative(ctx.loaded.root, file).split(path26.sep).join("/")
14475
+ source: path26.relative(ctx.loaded.root, file).split(path26.sep).join("/"),
14476
+ session: status()
14449
14477
  };
14450
14478
  }
14451
14479
  };
14452
14480
  }
14481
+ function billedAmount(usage) {
14482
+ if (!usage || typeof usage !== "object") return null;
14483
+ const { type } = usage;
14484
+ if (type !== "usd" && type !== "generations") return null;
14485
+ const amount = usage[type];
14486
+ return typeof amount === "number" && Number.isFinite(amount) ? { unit: type, amount } : null;
14487
+ }
14453
14488
  async function createSkeletonAnimation(manifestPath, request) {
14454
14489
  const root = path26.dirname(path26.resolve(manifestPath));
14455
14490
  const file = path26.resolve(root, request.keypointsFile);
@@ -14495,6 +14530,26 @@ async function createSkeletonAnimation(manifestPath, request) {
14495
14530
  throw error;
14496
14531
  }
14497
14532
  }
14533
+ async function updateSkeletonKeypoints(ctx, request) {
14534
+ const key = lockKey(request.styleId, request.assetId);
14535
+ const spec = ctx.specs.find((candidate) => lockKey(candidate.styleId, candidate.assetId) === key);
14536
+ if (!spec) throw new ManifestEditError(`"${key}" is not declared by the manifest`);
14537
+ const file = spec.revision?.mode === "animate-skeleton" ? spec.revision.keypointsFile : void 0;
14538
+ if (!file) throw new ManifestEditError(`${key} is not an animate-skeleton revision`);
14539
+ const relative = path26.relative(ctx.loaded.root, file);
14540
+ if (relative.startsWith("..") || path26.isAbsolute(relative)) {
14541
+ throw new ManifestEditError(`${key}'s keypoints file is outside the project; edit it by hand`);
14542
+ }
14543
+ const current = existsSync18(file) ? sha256(await readFile17(file)) : null;
14544
+ if (current !== request.expectedSha256) {
14545
+ throw Object.assign(
14546
+ new Error(`${relative.split(path26.sep).join("/")} changed on disk since this page loaded it; press Refresh and edit again`),
14547
+ { status: 409 }
14548
+ );
14549
+ }
14550
+ await writeFile12(file, JSON.stringify(request.set, null, 2) + "\n");
14551
+ return relative.split(path26.sep).join("/");
14552
+ }
14498
14553
 
14499
14554
  // src/gallery/edit.ts
14500
14555
  var Base64 = z9.string().regex(/^[A-Za-z0-9+/]*={0,2}$/).max(Math.ceil(MAX_HAND_EDIT_BYTES / 3) * 4);
@@ -14571,6 +14626,18 @@ function createGalleryEditHandler(opts) {
14571
14626
  }
14572
14627
  return opts.reload();
14573
14628
  }
14629
+ const poses = SkeletonKeypointsUpdateSchema.safeParse(body);
14630
+ if (poses.success) {
14631
+ if (!opts.loadProject) throw new ManifestEditError("pose edits are not available in this gallery");
14632
+ const written = await updateSkeletonKeypoints(await opts.loadProject(poses.data.project), poses.data);
14633
+ log2(` poses saved: ${written} (${poses.data.styleId}/${poses.data.assetId} is stale until generated again)`);
14634
+ return opts.reload();
14635
+ }
14636
+ if (body?.action === "update-skeleton-keypoints") {
14637
+ throw new ManifestEditError(
14638
+ "invalid pose edit: " + poses.error.issues.slice(0, 3).map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")
14639
+ );
14640
+ }
14574
14641
  const skeleton = SkeletonAnimationRequestSchema.safeParse(body);
14575
14642
  if (skeleton.success) {
14576
14643
  let manifestPath2;
@@ -15086,7 +15153,8 @@ async function buildGallerySnapshot(opts) {
15086
15153
  mirrorOfKey: spec.mirror ? lockKey(spec.styleId, spec.mirror.sourceAssetId) : entry?.mirror ? lockKey(spec.styleId, entry.mirror.sourceAssetId) : null,
15087
15154
  skeleton: spec.revision?.mode === "animate-skeleton" && spec.revision.keypointsFile ? {
15088
15155
  keypointsFile: portableOutputPath(spec.revision.keypointsFile, root),
15089
- set: spec.revision.skeleton ?? null
15156
+ set: spec.revision.skeleton ?? null,
15157
+ sha256: spec.revision.keypointsSha256 ?? null
15090
15158
  } : null,
15091
15159
  character: describeCharacter(spec, entry),
15092
15160
  outputs,
@@ -15378,13 +15446,48 @@ import { createServer as createServer2 } from "http";
15378
15446
  import { readFile as readFile19 } from "fs/promises";
15379
15447
 
15380
15448
  // src/gallery/page.ts
15381
- import { readFileSync as readFileSync4 } from "fs";
15449
+ import { existsSync as existsSync21, readFileSync as readFileSync4 } from "fs";
15450
+
15451
+ // src/gallery/client-bundle.ts
15452
+ import { createRequire } from "module";
15453
+ import { fileURLToPath } from "url";
15454
+ function bundleGalleryClient(entry = fileURLToPath(new URL("./client/src/main.ts", import.meta.url))) {
15455
+ const esbuild = createRequire(import.meta.url)("esbuild");
15456
+ const result = esbuild.buildSync({
15457
+ entryPoints: [entry],
15458
+ bundle: true,
15459
+ format: "iife",
15460
+ target: "es2020",
15461
+ platform: "browser",
15462
+ write: false,
15463
+ legalComments: "none",
15464
+ charset: "utf8"
15465
+ });
15466
+ return result.outputFiles[0].text;
15467
+ }
15468
+
15469
+ // src/gallery/page.ts
15470
+ function galleryContentSecurityPolicy(nonce) {
15471
+ return [
15472
+ "default-src 'self'",
15473
+ `script-src 'nonce-${nonce}'`,
15474
+ `style-src 'nonce-${nonce}'`,
15475
+ "img-src 'self' data: blob:",
15476
+ "frame-src 'self'",
15477
+ "connect-src 'self'",
15478
+ "object-src 'none'",
15479
+ "base-uri 'none'",
15480
+ "form-action 'none'",
15481
+ "frame-ancestors 'none'"
15482
+ ].join("; ");
15483
+ }
15382
15484
  var CLIENT_DIR = new URL("./client/", import.meta.url);
15383
15485
  var client;
15384
15486
  function clientAssets() {
15487
+ const prebuilt = new URL("gallery.js", CLIENT_DIR);
15385
15488
  client ??= {
15386
15489
  css: readFileSync4(new URL("gallery.css", CLIENT_DIR), "utf8"),
15387
- js: readFileSync4(new URL("gallery.js", CLIENT_DIR), "utf8")
15490
+ js: existsSync21(prebuilt) ? readFileSync4(prebuilt, "utf8") : bundleGalleryClient()
15388
15491
  };
15389
15492
  return client;
15390
15493
  }
@@ -15396,6 +15499,7 @@ function renderGallery(snapshot, opts = {}) {
15396
15499
  const editor = JSON.stringify(Boolean(opts.editor && opts.session));
15397
15500
  const title = `${snapshot.project?.name ?? "workspace"} | pixelkiln`;
15398
15501
  const { css, js } = clientAssets();
15502
+ const nonce = opts.nonce ? ` nonce="${escapeHtml2(opts.nonce)}"` : "";
15399
15503
  return `<!doctype html>
15400
15504
  <html lang="en">
15401
15505
  <head>
@@ -15403,7 +15507,7 @@ function renderGallery(snapshot, opts = {}) {
15403
15507
  <meta name="viewport" content="width=device-width, initial-scale=1">
15404
15508
  <title>${escapeHtml2(title)}</title>
15405
15509
  <link rel="icon" href="data:,">
15406
- <style>
15510
+ <style${nonce}>
15407
15511
  ${css}</style>
15408
15512
  </head>
15409
15513
  <body>
@@ -15445,7 +15549,7 @@ ${css}</style>
15445
15549
  <div id="drawer-host"></div>
15446
15550
  <div id="dialog-host"></div>
15447
15551
  <datalist id="view-options"><option value="low top-down"><option value="high top-down"><option value="side"><option value="sidescroller"></datalist>
15448
- <script>
15552
+ <script${nonce}>
15449
15553
  const INITIAL = ${data};
15450
15554
  const SESSION = ${session};
15451
15555
  const EDITABLE = ${editable};
@@ -15641,6 +15745,10 @@ async function serveGallery(opts) {
15641
15745
  }
15642
15746
  }
15643
15747
  if (req.method !== "GET" && req.method !== "HEAD") return fail(405, "the gallery is read-only");
15748
+ if (url2.pathname === "/api/skeleton") {
15749
+ if (!opts.skeleton) return fail(404, "skeleton estimates need the gallery started with --edit");
15750
+ return json(200, opts.skeleton.status());
15751
+ }
15644
15752
  if (url2.pathname === "/api/jobs") {
15645
15753
  if (!opts.generate) return fail(404, "generation is off");
15646
15754
  return json(200, opts.generate.status());
@@ -15674,12 +15782,15 @@ async function serveGallery(opts) {
15674
15782
  try {
15675
15783
  if (url2.pathname === "/") {
15676
15784
  const { snapshot } = await load();
15785
+ const nonce = randomBytes(16).toString("base64");
15677
15786
  res.writeHead(200, {
15678
15787
  "Content-Type": "text/html; charset=utf-8",
15679
15788
  "Cache-Control": "no-store",
15680
- "X-Content-Type-Options": "nosniff"
15789
+ "X-Content-Type-Options": "nosniff",
15790
+ "Content-Security-Policy": galleryContentSecurityPolicy(nonce)
15681
15791
  });
15682
15792
  res.end(renderGallery(snapshot, {
15793
+ nonce,
15683
15794
  ...session ? { session } : {},
15684
15795
  editable: Boolean(opts.edit),
15685
15796
  generation: Boolean(opts.generate),
@@ -16158,12 +16269,12 @@ function ownedReferenceImageId(spec, lock, providerId) {
16158
16269
  }
16159
16270
 
16160
16271
  // src/pick/server.ts
16161
- import { existsSync as existsSync22 } from "fs";
16272
+ import { existsSync as existsSync23 } from "fs";
16162
16273
  import { readFile as readFile21 } from "fs/promises";
16163
16274
 
16164
16275
  // src/pipeline/init.ts
16165
16276
  import { readdir, readFile as readFile20, writeFile as writeFile13 } from "fs/promises";
16166
- import { existsSync as existsSync21 } from "fs";
16277
+ import { existsSync as existsSync22 } from "fs";
16167
16278
  import path30 from "path";
16168
16279
  function pngSize(buf) {
16169
16280
  if (buf.length < 24) return null;
@@ -16257,7 +16368,7 @@ function mode(values) {
16257
16368
  return [...counts.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? 64;
16258
16369
  }
16259
16370
  async function writeManifestFile(target, manifest) {
16260
- if (existsSync21(target)) {
16371
+ if (existsSync22(target)) {
16261
16372
  throw new Error(`${target} already exists; refusing to overwrite an existing manifest.`);
16262
16373
  }
16263
16374
  await writeFile13(target, JSON.stringify(manifest, null, 2) + "\n");
@@ -16673,7 +16784,7 @@ async function currentArt(entry, spec) {
16673
16784
  const previous = primaryOutput({ ...entry, outputs: entry.supersededOutputs });
16674
16785
  if (!previous) return null;
16675
16786
  const file = resolveOutputPath(previous.path, spec.root);
16676
- if (!existsSync22(file)) return null;
16787
+ if (!existsSync23(file)) return null;
16677
16788
  const contentType = previous.mediaType ?? mediaTypeFromExtension(file);
16678
16789
  if (!contentType) return null;
16679
16790
  let width = entry.width;
@@ -17181,7 +17292,7 @@ import path32 from "path";
17181
17292
 
17182
17293
  // src/editor/install.ts
17183
17294
  import { createHash as createHash4 } from "crypto";
17184
- import { existsSync as existsSync23 } from "fs";
17295
+ import { existsSync as existsSync24 } from "fs";
17185
17296
  import { mkdir as mkdir11, readFile as readFile22, rename as rename9, rm as rm10, stat as stat3, writeFile as writeFile14 } from "fs/promises";
17186
17297
  import os2 from "os";
17187
17298
  import path31 from "path";
@@ -17280,7 +17391,7 @@ async function editorStatus(opts = {}) {
17280
17391
  for (const [name, expected] of Object.entries(pin.files)) {
17281
17392
  totalBytes += expected.bytes;
17282
17393
  const file = path31.join(dir, name);
17283
- const info = existsSync23(file) ? await stat3(file).catch(() => null) : null;
17394
+ const info = existsSync24(file) ? await stat3(file).catch(() => null) : null;
17284
17395
  if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
17285
17396
  installedBytes += expected.bytes;
17286
17397
  } else {
@@ -17534,7 +17645,7 @@ async function serveUntilStopped(initial, reload, args, access2) {
17534
17645
  ...args.edit ? { edit: createGalleryEditHandler({ manifestFor: access2.manifestFor, loadProject: access2.loadProject, reload, onProgress: log }) } : {},
17535
17646
  ...budget ? { generate: createGenerateHandlers({ loadProject, providerFor, budget, reload, onProgress: log }) } : {},
17536
17647
  // Estimating acts on the author's behalf and feeds a manifest edit, so it follows the write gate.
17537
- ...args.edit ? { skeleton: createGallerySkeletonHandlers({ loadProject, onProgress: log }) } : {},
17648
+ ...args.edit ? { skeleton: createGallerySkeletonHandlers({ loadProject, limit: args.estimateLimit, onProgress: log }) } : {},
17538
17649
  // The editor exists to write hand edits back, so it follows the write gate.
17539
17650
  ...args.edit && !args.noEditor ? { editor: createGalleryEditorHandlers({ onProgress: log }) } : {}
17540
17651
  });
@@ -17553,7 +17664,7 @@ async function serveUntilStopped(initial, reload, args, access2) {
17553
17664
  async function runGallery(args) {
17554
17665
  if (args.workspace) {
17555
17666
  const workspacePath = path33.resolve(args.workspace);
17556
- if (!existsSync24(workspacePath)) {
17667
+ if (!existsSync25(workspacePath)) {
17557
17668
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
17558
17669
  }
17559
17670
  const filter2 = { styles: args.styles, assets: args.assets };
@@ -17907,14 +18018,14 @@ async function runRestoreGeneration(args) {
17907
18018
  }
17908
18019
 
17909
18020
  // src/cli/commands/housekeeping.ts
17910
- import { existsSync as existsSync26 } from "fs";
18021
+ import { existsSync as existsSync27 } from "fs";
17911
18022
 
17912
18023
  // src/pipeline/cache-health.ts
17913
- import { existsSync as existsSync25 } from "fs";
18024
+ import { existsSync as existsSync26 } from "fs";
17914
18025
  import { readFile as readFile24, readdir as readdir2, rm as rm11 } from "fs/promises";
17915
18026
  import path34 from "path";
17916
18027
  async function inspectCaches(lock, lockPath, options = {}) {
17917
- if (options.prune && !existsSync25(lockPath)) {
18028
+ if (options.prune && !existsSync26(lockPath)) {
17918
18029
  throw new Error(`Refusing to prune without an existing lockfile at ${path34.resolve(lockPath)}`);
17919
18030
  }
17920
18031
  const contentDir = path34.resolve(path34.dirname(lockPath), ".pixelkiln", "cache");
@@ -17957,7 +18068,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
17957
18068
  async function inspectContentCache(contentDir, referenced) {
17958
18069
  const report = {
17959
18070
  path: contentDir,
17960
- exists: existsSync25(contentDir),
18071
+ exists: existsSync26(contentDir),
17961
18072
  files: 0,
17962
18073
  bytes: 0,
17963
18074
  valid: 0,
@@ -18021,7 +18132,7 @@ async function inspectContentCache(contentDir, referenced) {
18021
18132
  async function inspectRemoteHashCache(remotePath) {
18022
18133
  const report = {
18023
18134
  path: remotePath,
18024
- exists: existsSync25(remotePath),
18135
+ exists: existsSync26(remotePath),
18025
18136
  entries: 0,
18026
18137
  valid: 0,
18027
18138
  invalidIds: [],
@@ -18094,7 +18205,7 @@ async function runAccept(args) {
18094
18205
  let intact = true;
18095
18206
  for (const output of entry.outputs) {
18096
18207
  const file = resolveOutputPath(output.path, item.spec.root);
18097
- if (!existsSync26(file) || await sha256File(file) !== output.sha256) {
18208
+ if (!existsSync27(file) || await sha256File(file) !== output.sha256) {
18098
18209
  intact = false;
18099
18210
  break;
18100
18211
  }
@@ -18150,11 +18261,11 @@ This drops their provenance from the lockfile. The art on disk is`);
18150
18261
 
18151
18262
  // src/cli/commands/init.ts
18152
18263
  import path35 from "path";
18153
- import { existsSync as existsSync27 } from "fs";
18264
+ import { existsSync as existsSync28 } from "fs";
18154
18265
  async function runInit(args) {
18155
18266
  if (!args.from) throw new Error("init needs --from <dir> pointing at your existing PNGs.");
18156
18267
  const root = path35.resolve(args.from);
18157
- if (!existsSync27(root)) throw new Error(`No directory at ${root}`);
18268
+ if (!existsSync28(root)) throw new Error(`No directory at ${root}`);
18158
18269
  const generator = args.generator ?? "map";
18159
18270
  if (generator !== "1dir" && generator !== "map") {
18160
18271
  throw new Error(`--generator must be "1dir" or "map", got "${args.generator}".`);
@@ -18182,7 +18293,7 @@ async function runInit(args) {
18182
18293
  }
18183
18294
 
18184
18295
  // src/pipeline/doctor.ts
18185
- import { constants, existsSync as existsSync28 } from "fs";
18296
+ import { constants, existsSync as existsSync29 } from "fs";
18186
18297
  import { access, stat as stat4 } from "fs/promises";
18187
18298
  import path36 from "path";
18188
18299
  async function doctor(loaded, specs, lock, lockPath, opts = {}) {
@@ -18254,7 +18365,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
18254
18365
  cacheDir,
18255
18366
  cacheFileName(output.sha256, output.mediaType ?? MediaType.PNG)
18256
18367
  );
18257
- if (existsSync28(cached) && await sha256File(cached) === output.sha256) {
18368
+ if (existsSync29(cached) && await sha256File(cached) === output.sha256) {
18258
18369
  hasCache = true;
18259
18370
  break;
18260
18371
  }
@@ -18358,7 +18469,7 @@ async function nearestExistingDirectory(start) {
18358
18469
 
18359
18470
  // src/pipeline/audit.ts
18360
18471
  import { readFile as readFile25 } from "fs/promises";
18361
- import { existsSync as existsSync29 } from "fs";
18472
+ import { existsSync as existsSync30 } from "fs";
18362
18473
  import path37 from "path";
18363
18474
  function paletteDistance(asset, reference) {
18364
18475
  if (!asset.length || !reference.length) return 0;
@@ -18395,7 +18506,7 @@ async function auditStyle(loaded, specs, styleId, lock) {
18395
18506
  const unreadable = [];
18396
18507
  for (const spec of mine) {
18397
18508
  for (const output of resolveSpecOutputs(spec, lock, loaded.root)) {
18398
- if (!existsSync29(output.absolutePath)) {
18509
+ if (!existsSync30(output.absolutePath)) {
18399
18510
  missing.push(output.id);
18400
18511
  continue;
18401
18512
  }
@@ -18425,7 +18536,7 @@ async function auditStyle(loaded, specs, styleId, lock) {
18425
18536
  const refPalettes = [];
18426
18537
  for (const img of style.styleImages) {
18427
18538
  const abs = path37.resolve(loaded.root, img.path);
18428
- if (!existsSync29(abs)) continue;
18539
+ if (!existsSync30(abs)) continue;
18429
18540
  try {
18430
18541
  refPalettes.push(extractPalette(decodePng(await readFile25(abs)), 12));
18431
18542
  } catch {
@@ -19597,7 +19708,7 @@ function sheetFormat(args) {
19597
19708
  import path41 from "path";
19598
19709
 
19599
19710
  // src/pixellab-utils.ts
19600
- import { existsSync as existsSync30 } from "fs";
19711
+ import { existsSync as existsSync31 } from "fs";
19601
19712
  import { mkdir as mkdir12, readFile as readFile27, writeFile as writeFile15 } from "fs/promises";
19602
19713
  import path40 from "path";
19603
19714
  var UNZOOM_MIN_SIDE = 256;
@@ -19609,7 +19720,7 @@ function defaultUnzoomOut(input) {
19609
19720
  async function unzoomFile(client2, input, opts = {}) {
19610
19721
  const out = path40.resolve(opts.out ?? defaultUnzoomOut(input));
19611
19722
  if (path40.resolve(input) === out) throw new Error("unzoom will not overwrite its own input; pass a different --out");
19612
- if (existsSync30(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
19723
+ if (existsSync31(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
19613
19724
  const bytes = await readFile27(input);
19614
19725
  const metadata = imageMetadata(bytes);
19615
19726
  if (!metadata) throw new Error(`${input} is not a readable PNG or JPEG`);
@@ -19638,7 +19749,7 @@ async function generateFont(client2, opts) {
19638
19749
  const { ttf, atlas } = fontOutputPaths(opts.out);
19639
19750
  if (!opts.force) {
19640
19751
  for (const file of [ttf, atlas]) {
19641
- if (existsSync30(file)) throw new Error(`${file} already exists; pass --force to replace it`);
19752
+ if (existsSync31(file)) throw new Error(`${file} already exists; pass --force to replace it`);
19642
19753
  }
19643
19754
  }
19644
19755
  const sleep3 = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
@@ -19752,7 +19863,7 @@ import { readFile as readFile29 } from "fs/promises";
19752
19863
 
19753
19864
  // src/pipeline/quality-regression.ts
19754
19865
  import path42 from "path";
19755
- import { existsSync as existsSync31 } from "fs";
19866
+ import { existsSync as existsSync32 } from "fs";
19756
19867
  import { readFile as readFile28 } from "fs/promises";
19757
19868
  import { z as z11 } from "zod";
19758
19869
  var SHA256_RE = /^[0-9a-f]{64}$/;
@@ -19995,7 +20106,7 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
19995
20106
  cases
19996
20107
  });
19997
20108
  const data = Buffer.from(JSON.stringify(baseline, null, 2) + "\n");
19998
- if (existsSync31(absolute) && !options.force) {
20109
+ if (existsSync32(absolute) && !options.force) {
19999
20110
  const current = await readFile28(absolute);
20000
20111
  if (!current.equals(data)) {
20001
20112
  throw new OverwriteRefusedError(
@@ -20192,9 +20303,9 @@ import path45 from "path";
20192
20303
 
20193
20304
  // src/recipes.ts
20194
20305
  import path44 from "path";
20195
- import { existsSync as existsSync32 } from "fs";
20306
+ import { existsSync as existsSync33 } from "fs";
20196
20307
  import { readdir as readdir3, readFile as readFile30 } from "fs/promises";
20197
- import { fileURLToPath } from "url";
20308
+ import { fileURLToPath as fileURLToPath2 } from "url";
20198
20309
  import { z as z12 } from "zod";
20199
20310
  var SHA256_RE2 = /^[0-9a-f]{64}$/;
20200
20311
  var RECIPE_ID_RE = /^[a-z0-9][a-z0-9-]*\/[a-z0-9][a-z0-9-]*$/;
@@ -20395,7 +20506,7 @@ async function readRecipeFile(recipePath, bundled) {
20395
20506
  return { recipe: parsed.data, authored: raw, path: absolute, dir: path44.dirname(absolute), bundled };
20396
20507
  }
20397
20508
  async function walkRecipeFiles(root) {
20398
- if (!existsSync32(root)) return [];
20509
+ if (!existsSync33(root)) return [];
20399
20510
  const out = [];
20400
20511
  for (const entry of await readdir3(root, { withFileTypes: true })) {
20401
20512
  const resolved = path44.join(root, entry.name);
@@ -20405,7 +20516,7 @@ async function walkRecipeFiles(root) {
20405
20516
  return out;
20406
20517
  }
20407
20518
  function bundledRecipeRoot() {
20408
- return fileURLToPath(new URL("../recipes/", import.meta.url));
20519
+ return fileURLToPath2(new URL("../recipes/", import.meta.url));
20409
20520
  }
20410
20521
  async function listBundledRecipes(root = bundledRecipeRoot()) {
20411
20522
  const recipes = await Promise.all(
@@ -20433,7 +20544,7 @@ function parseSelector(selector) {
20433
20544
  }
20434
20545
  async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
20435
20546
  const local = path44.resolve(target);
20436
- if (existsSync32(local)) {
20547
+ if (existsSync33(local)) {
20437
20548
  const recipePath = path44.basename(local) === RECIPE_FILE ? local : path44.join(local, RECIPE_FILE);
20438
20549
  return readRecipeFile(recipePath, false);
20439
20550
  }
@@ -20449,7 +20560,7 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
20449
20560
  }
20450
20561
  async function verifyFile(root, file) {
20451
20562
  const absolute = path44.resolve(root, ...file.path.split("/"));
20452
- if (!existsSync32(absolute)) {
20563
+ if (!existsSync33(absolute)) {
20453
20564
  return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
20454
20565
  }
20455
20566
  const actualSha256 = await sha256File(absolute);
@@ -20527,7 +20638,7 @@ async function installRecipe(target, options = {}) {
20527
20638
  })));
20528
20639
  if (!options.force) {
20529
20640
  for (const file of files) {
20530
- if (!existsSync32(file.path)) continue;
20641
+ if (!existsSync33(file.path)) continue;
20531
20642
  const current = await readFile30(file.path);
20532
20643
  if (!current.equals(file.data)) {
20533
20644
  throw new OverwriteRefusedError(
@@ -20767,7 +20878,7 @@ async function runRefine(args) {
20767
20878
 
20768
20879
  // src/cli/commands/skeleton.ts
20769
20880
  import path47 from "path";
20770
- import { existsSync as existsSync33 } from "fs";
20881
+ import { existsSync as existsSync34 } from "fs";
20771
20882
  import { readFile as readFile31, writeFile as writeFile16 } from "fs/promises";
20772
20883
 
20773
20884
  // src/skeleton-preview.ts
@@ -20879,7 +20990,7 @@ var Canvas = class {
20879
20990
  async function runEstimateSkeleton(args) {
20880
20991
  const image = args.target;
20881
20992
  if (!image) throw new UsageError("estimate-skeleton needs an image path: pixelkiln estimate-skeleton <image> [--out keypoints.json]");
20882
- if (args.out && existsSync33(path47.resolve(args.out)) && !args.force) {
20993
+ if (args.out && existsSync34(path47.resolve(args.out)) && !args.force) {
20883
20994
  throw new UsageError(`${args.out} already exists; pass --force to replace it`);
20884
20995
  }
20885
20996
  loadEnvFiles(path47.dirname(path47.resolve(args.manifest)));
@@ -20925,7 +21036,7 @@ async function runSkeletonPreview(args) {
20925
21036
  sourceFile = args.from ? path47.resolve(args.from) : spec.revision.sourceFile;
20926
21037
  label = `${spec.styleId}/${spec.assetId}`;
20927
21038
  }
20928
- if (!existsSync33(keypointsFile)) throw new UsageError(`Keypoints file does not exist yet: ${keypointsFile}`);
21039
+ if (!existsSync34(keypointsFile)) throw new UsageError(`Keypoints file does not exist yet: ${keypointsFile}`);
20929
21040
  let json;
20930
21041
  try {
20931
21042
  json = JSON.parse(await readFile31(keypointsFile, "utf8"));
@@ -20935,7 +21046,7 @@ async function runSkeletonPreview(args) {
20935
21046
  const set = parseSkeletonSet(json, keypointsFile);
20936
21047
  let source;
20937
21048
  if (sourceFile) {
20938
- if (!existsSync33(sourceFile)) {
21049
+ if (!existsSync34(sourceFile)) {
20939
21050
  log(` source image does not exist yet, drawing on a plain ground: ${sourceFile}`);
20940
21051
  } else {
20941
21052
  const bytes = await readFile31(sourceFile);
@@ -20993,7 +21104,7 @@ async function runTools(args) {
20993
21104
 
20994
21105
  // src/cli/commands/workspace.ts
20995
21106
  import path48 from "path";
20996
- import { existsSync as existsSync34 } from "fs";
21107
+ import { existsSync as existsSync35 } from "fs";
20997
21108
  async function runWorkspace(args) {
20998
21109
  const workspacePath = path48.resolve(args.workspace ?? "pixelkiln.workspace.json");
20999
21110
  const dir = path48.dirname(workspacePath);
@@ -21023,7 +21134,7 @@ async function runWorkspace(args) {
21023
21134
  log(` registered "${id}" in ${path48.relative(process.cwd(), workspacePath)}`);
21024
21135
  log(` manifest: ${project.manifest}`);
21025
21136
  log(` lock: ${project.lock}`);
21026
- if (!existsSync34(lockPath)) {
21137
+ if (!existsSync35(lockPath)) {
21027
21138
  log(
21028
21139
  ` warning: no lockfile there yet; this project contributes no claims until one is generated`
21029
21140
  );
@@ -21047,7 +21158,7 @@ async function runWorkspace(args) {
21047
21158
  return;
21048
21159
  }
21049
21160
  if (args.subcommand === "list") {
21050
- if (!existsSync34(workspacePath)) {
21161
+ if (!existsSync35(workspacePath)) {
21051
21162
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
21052
21163
  }
21053
21164
  const ws = await loadWorkspace(workspacePath);
@@ -21067,7 +21178,7 @@ async function runWorkspace(args) {
21067
21178
  return;
21068
21179
  }
21069
21180
  if (args.subcommand === "status") {
21070
- if (!existsSync34(workspacePath)) {
21181
+ if (!existsSync35(workspacePath)) {
21071
21182
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
21072
21183
  }
21073
21184
  const ws = await loadWorkspace(workspacePath);
@@ -21129,7 +21240,7 @@ async function runHelp() {
21129
21240
  async function runVersion() {
21130
21241
  for (const up of ["../package.json", "../../package.json"]) {
21131
21242
  const url = new URL(up, import.meta.url);
21132
- if (!existsSync35(url)) continue;
21243
+ if (!existsSync36(url)) continue;
21133
21244
  const pkg = JSON.parse(await readFile32(url, "utf8"));
21134
21245
  if (pkg.name === "pixelkiln") {
21135
21246
  log(`${pkg.name} ${pkg.version}`);