pixelkiln 0.70.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,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,7 @@ var SkeletonAnimationRequestSchema = z8.object({
14416
14419
  /** Replace an existing `keypointsFile` with `set`. */
14417
14420
  overwrite: z8.boolean().optional()
14418
14421
  }).strict();
14422
+ var DEFAULT_ESTIMATE_LIMIT = 10;
14419
14423
  function skeletonSourceFile(spec) {
14420
14424
  if (spec.source) return path26.resolve(spec.root, spec.source);
14421
14425
  if (spec.quality && existsSync18(spec.quality.outFile)) return spec.quality.outFile;
@@ -14424,7 +14428,12 @@ function skeletonSourceFile(spec) {
14424
14428
  function createGallerySkeletonHandlers(opts) {
14425
14429
  const log2 = opts.onProgress ?? (() => {
14426
14430
  });
14431
+ const limit = opts.limit ?? DEFAULT_ESTIMATE_LIMIT;
14432
+ let used = 0;
14433
+ const spent = {};
14434
+ const status = () => ({ used, limit, spent: { ...spent } });
14427
14435
  return {
14436
+ status,
14428
14437
  async estimate(body) {
14429
14438
  const parsed = SkeletonEstimateRequestSchema.safeParse(body);
14430
14439
  if (!parsed.success) throw new ManifestEditError(`invalid estimate request: ${parsed.error.issues[0]?.message}`);
@@ -14440,16 +14449,33 @@ function createGallerySkeletonHandlers(opts) {
14440
14449
  if (!metadata) throw new ManifestEditError(`${path26.basename(file)} is not a readable PNG or JPEG`);
14441
14450
  const problem = estimateSkeletonSizeProblem(metadata.width, metadata.height);
14442
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
+ }
14443
14458
  const client2 = (opts.client ?? clientFromEnv)();
14459
+ used++;
14444
14460
  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)`);
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}` : ""})`);
14446
14464
  return {
14447
14465
  set: scaffoldSkeletonSet(res.keypoints, request.frames ?? DEFAULT_SCAFFOLD_FRAMES),
14448
- source: path26.relative(ctx.loaded.root, file).split(path26.sep).join("/")
14466
+ source: path26.relative(ctx.loaded.root, file).split(path26.sep).join("/"),
14467
+ session: status()
14449
14468
  };
14450
14469
  }
14451
14470
  };
14452
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
+ }
14453
14479
  async function createSkeletonAnimation(manifestPath, request) {
14454
14480
  const root = path26.dirname(path26.resolve(manifestPath));
14455
14481
  const file = path26.resolve(root, request.keypointsFile);
@@ -15378,13 +15404,48 @@ import { createServer as createServer2 } from "http";
15378
15404
  import { readFile as readFile19 } from "fs/promises";
15379
15405
 
15380
15406
  // src/gallery/page.ts
15381
- import { readFileSync as readFileSync4 } from "fs";
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
+ }
15426
+
15427
+ // src/gallery/page.ts
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
+ }
15382
15442
  var CLIENT_DIR = new URL("./client/", import.meta.url);
15383
15443
  var client;
15384
15444
  function clientAssets() {
15445
+ const prebuilt = new URL("gallery.js", CLIENT_DIR);
15385
15446
  client ??= {
15386
15447
  css: readFileSync4(new URL("gallery.css", CLIENT_DIR), "utf8"),
15387
- js: readFileSync4(new URL("gallery.js", CLIENT_DIR), "utf8")
15448
+ js: existsSync21(prebuilt) ? readFileSync4(prebuilt, "utf8") : bundleGalleryClient()
15388
15449
  };
15389
15450
  return client;
15390
15451
  }
@@ -15396,6 +15457,7 @@ function renderGallery(snapshot, opts = {}) {
15396
15457
  const editor = JSON.stringify(Boolean(opts.editor && opts.session));
15397
15458
  const title = `${snapshot.project?.name ?? "workspace"} | pixelkiln`;
15398
15459
  const { css, js } = clientAssets();
15460
+ const nonce = opts.nonce ? ` nonce="${escapeHtml2(opts.nonce)}"` : "";
15399
15461
  return `<!doctype html>
15400
15462
  <html lang="en">
15401
15463
  <head>
@@ -15403,7 +15465,7 @@ function renderGallery(snapshot, opts = {}) {
15403
15465
  <meta name="viewport" content="width=device-width, initial-scale=1">
15404
15466
  <title>${escapeHtml2(title)}</title>
15405
15467
  <link rel="icon" href="data:,">
15406
- <style>
15468
+ <style${nonce}>
15407
15469
  ${css}</style>
15408
15470
  </head>
15409
15471
  <body>
@@ -15445,7 +15507,7 @@ ${css}</style>
15445
15507
  <div id="drawer-host"></div>
15446
15508
  <div id="dialog-host"></div>
15447
15509
  <datalist id="view-options"><option value="low top-down"><option value="high top-down"><option value="side"><option value="sidescroller"></datalist>
15448
- <script>
15510
+ <script${nonce}>
15449
15511
  const INITIAL = ${data};
15450
15512
  const SESSION = ${session};
15451
15513
  const EDITABLE = ${editable};
@@ -15641,6 +15703,10 @@ async function serveGallery(opts) {
15641
15703
  }
15642
15704
  }
15643
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
+ }
15644
15710
  if (url2.pathname === "/api/jobs") {
15645
15711
  if (!opts.generate) return fail(404, "generation is off");
15646
15712
  return json(200, opts.generate.status());
@@ -15674,12 +15740,15 @@ async function serveGallery(opts) {
15674
15740
  try {
15675
15741
  if (url2.pathname === "/") {
15676
15742
  const { snapshot } = await load();
15743
+ const nonce = randomBytes(16).toString("base64");
15677
15744
  res.writeHead(200, {
15678
15745
  "Content-Type": "text/html; charset=utf-8",
15679
15746
  "Cache-Control": "no-store",
15680
- "X-Content-Type-Options": "nosniff"
15747
+ "X-Content-Type-Options": "nosniff",
15748
+ "Content-Security-Policy": galleryContentSecurityPolicy(nonce)
15681
15749
  });
15682
15750
  res.end(renderGallery(snapshot, {
15751
+ nonce,
15683
15752
  ...session ? { session } : {},
15684
15753
  editable: Boolean(opts.edit),
15685
15754
  generation: Boolean(opts.generate),
@@ -16158,12 +16227,12 @@ function ownedReferenceImageId(spec, lock, providerId) {
16158
16227
  }
16159
16228
 
16160
16229
  // src/pick/server.ts
16161
- import { existsSync as existsSync22 } from "fs";
16230
+ import { existsSync as existsSync23 } from "fs";
16162
16231
  import { readFile as readFile21 } from "fs/promises";
16163
16232
 
16164
16233
  // src/pipeline/init.ts
16165
16234
  import { readdir, readFile as readFile20, writeFile as writeFile13 } from "fs/promises";
16166
- import { existsSync as existsSync21 } from "fs";
16235
+ import { existsSync as existsSync22 } from "fs";
16167
16236
  import path30 from "path";
16168
16237
  function pngSize(buf) {
16169
16238
  if (buf.length < 24) return null;
@@ -16257,7 +16326,7 @@ function mode(values) {
16257
16326
  return [...counts.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? 64;
16258
16327
  }
16259
16328
  async function writeManifestFile(target, manifest) {
16260
- if (existsSync21(target)) {
16329
+ if (existsSync22(target)) {
16261
16330
  throw new Error(`${target} already exists; refusing to overwrite an existing manifest.`);
16262
16331
  }
16263
16332
  await writeFile13(target, JSON.stringify(manifest, null, 2) + "\n");
@@ -16673,7 +16742,7 @@ async function currentArt(entry, spec) {
16673
16742
  const previous = primaryOutput({ ...entry, outputs: entry.supersededOutputs });
16674
16743
  if (!previous) return null;
16675
16744
  const file = resolveOutputPath(previous.path, spec.root);
16676
- if (!existsSync22(file)) return null;
16745
+ if (!existsSync23(file)) return null;
16677
16746
  const contentType = previous.mediaType ?? mediaTypeFromExtension(file);
16678
16747
  if (!contentType) return null;
16679
16748
  let width = entry.width;
@@ -17181,7 +17250,7 @@ import path32 from "path";
17181
17250
 
17182
17251
  // src/editor/install.ts
17183
17252
  import { createHash as createHash4 } from "crypto";
17184
- import { existsSync as existsSync23 } from "fs";
17253
+ import { existsSync as existsSync24 } from "fs";
17185
17254
  import { mkdir as mkdir11, readFile as readFile22, rename as rename9, rm as rm10, stat as stat3, writeFile as writeFile14 } from "fs/promises";
17186
17255
  import os2 from "os";
17187
17256
  import path31 from "path";
@@ -17280,7 +17349,7 @@ async function editorStatus(opts = {}) {
17280
17349
  for (const [name, expected] of Object.entries(pin.files)) {
17281
17350
  totalBytes += expected.bytes;
17282
17351
  const file = path31.join(dir, name);
17283
- const info = existsSync23(file) ? await stat3(file).catch(() => null) : null;
17352
+ const info = existsSync24(file) ? await stat3(file).catch(() => null) : null;
17284
17353
  if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
17285
17354
  installedBytes += expected.bytes;
17286
17355
  } else {
@@ -17534,7 +17603,7 @@ async function serveUntilStopped(initial, reload, args, access2) {
17534
17603
  ...args.edit ? { edit: createGalleryEditHandler({ manifestFor: access2.manifestFor, loadProject: access2.loadProject, reload, onProgress: log }) } : {},
17535
17604
  ...budget ? { generate: createGenerateHandlers({ loadProject, providerFor, budget, reload, onProgress: log }) } : {},
17536
17605
  // 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 }) } : {},
17606
+ ...args.edit ? { skeleton: createGallerySkeletonHandlers({ loadProject, limit: args.estimateLimit, onProgress: log }) } : {},
17538
17607
  // The editor exists to write hand edits back, so it follows the write gate.
17539
17608
  ...args.edit && !args.noEditor ? { editor: createGalleryEditorHandlers({ onProgress: log }) } : {}
17540
17609
  });
@@ -17553,7 +17622,7 @@ async function serveUntilStopped(initial, reload, args, access2) {
17553
17622
  async function runGallery(args) {
17554
17623
  if (args.workspace) {
17555
17624
  const workspacePath = path33.resolve(args.workspace);
17556
- if (!existsSync24(workspacePath)) {
17625
+ if (!existsSync25(workspacePath)) {
17557
17626
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
17558
17627
  }
17559
17628
  const filter2 = { styles: args.styles, assets: args.assets };
@@ -17907,14 +17976,14 @@ async function runRestoreGeneration(args) {
17907
17976
  }
17908
17977
 
17909
17978
  // src/cli/commands/housekeeping.ts
17910
- import { existsSync as existsSync26 } from "fs";
17979
+ import { existsSync as existsSync27 } from "fs";
17911
17980
 
17912
17981
  // src/pipeline/cache-health.ts
17913
- import { existsSync as existsSync25 } from "fs";
17982
+ import { existsSync as existsSync26 } from "fs";
17914
17983
  import { readFile as readFile24, readdir as readdir2, rm as rm11 } from "fs/promises";
17915
17984
  import path34 from "path";
17916
17985
  async function inspectCaches(lock, lockPath, options = {}) {
17917
- if (options.prune && !existsSync25(lockPath)) {
17986
+ if (options.prune && !existsSync26(lockPath)) {
17918
17987
  throw new Error(`Refusing to prune without an existing lockfile at ${path34.resolve(lockPath)}`);
17919
17988
  }
17920
17989
  const contentDir = path34.resolve(path34.dirname(lockPath), ".pixelkiln", "cache");
@@ -17957,7 +18026,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
17957
18026
  async function inspectContentCache(contentDir, referenced) {
17958
18027
  const report = {
17959
18028
  path: contentDir,
17960
- exists: existsSync25(contentDir),
18029
+ exists: existsSync26(contentDir),
17961
18030
  files: 0,
17962
18031
  bytes: 0,
17963
18032
  valid: 0,
@@ -18021,7 +18090,7 @@ async function inspectContentCache(contentDir, referenced) {
18021
18090
  async function inspectRemoteHashCache(remotePath) {
18022
18091
  const report = {
18023
18092
  path: remotePath,
18024
- exists: existsSync25(remotePath),
18093
+ exists: existsSync26(remotePath),
18025
18094
  entries: 0,
18026
18095
  valid: 0,
18027
18096
  invalidIds: [],
@@ -18094,7 +18163,7 @@ async function runAccept(args) {
18094
18163
  let intact = true;
18095
18164
  for (const output of entry.outputs) {
18096
18165
  const file = resolveOutputPath(output.path, item.spec.root);
18097
- if (!existsSync26(file) || await sha256File(file) !== output.sha256) {
18166
+ if (!existsSync27(file) || await sha256File(file) !== output.sha256) {
18098
18167
  intact = false;
18099
18168
  break;
18100
18169
  }
@@ -18150,11 +18219,11 @@ This drops their provenance from the lockfile. The art on disk is`);
18150
18219
 
18151
18220
  // src/cli/commands/init.ts
18152
18221
  import path35 from "path";
18153
- import { existsSync as existsSync27 } from "fs";
18222
+ import { existsSync as existsSync28 } from "fs";
18154
18223
  async function runInit(args) {
18155
18224
  if (!args.from) throw new Error("init needs --from <dir> pointing at your existing PNGs.");
18156
18225
  const root = path35.resolve(args.from);
18157
- if (!existsSync27(root)) throw new Error(`No directory at ${root}`);
18226
+ if (!existsSync28(root)) throw new Error(`No directory at ${root}`);
18158
18227
  const generator = args.generator ?? "map";
18159
18228
  if (generator !== "1dir" && generator !== "map") {
18160
18229
  throw new Error(`--generator must be "1dir" or "map", got "${args.generator}".`);
@@ -18182,7 +18251,7 @@ async function runInit(args) {
18182
18251
  }
18183
18252
 
18184
18253
  // src/pipeline/doctor.ts
18185
- import { constants, existsSync as existsSync28 } from "fs";
18254
+ import { constants, existsSync as existsSync29 } from "fs";
18186
18255
  import { access, stat as stat4 } from "fs/promises";
18187
18256
  import path36 from "path";
18188
18257
  async function doctor(loaded, specs, lock, lockPath, opts = {}) {
@@ -18254,7 +18323,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
18254
18323
  cacheDir,
18255
18324
  cacheFileName(output.sha256, output.mediaType ?? MediaType.PNG)
18256
18325
  );
18257
- if (existsSync28(cached) && await sha256File(cached) === output.sha256) {
18326
+ if (existsSync29(cached) && await sha256File(cached) === output.sha256) {
18258
18327
  hasCache = true;
18259
18328
  break;
18260
18329
  }
@@ -18358,7 +18427,7 @@ async function nearestExistingDirectory(start) {
18358
18427
 
18359
18428
  // src/pipeline/audit.ts
18360
18429
  import { readFile as readFile25 } from "fs/promises";
18361
- import { existsSync as existsSync29 } from "fs";
18430
+ import { existsSync as existsSync30 } from "fs";
18362
18431
  import path37 from "path";
18363
18432
  function paletteDistance(asset, reference) {
18364
18433
  if (!asset.length || !reference.length) return 0;
@@ -18395,7 +18464,7 @@ async function auditStyle(loaded, specs, styleId, lock) {
18395
18464
  const unreadable = [];
18396
18465
  for (const spec of mine) {
18397
18466
  for (const output of resolveSpecOutputs(spec, lock, loaded.root)) {
18398
- if (!existsSync29(output.absolutePath)) {
18467
+ if (!existsSync30(output.absolutePath)) {
18399
18468
  missing.push(output.id);
18400
18469
  continue;
18401
18470
  }
@@ -18425,7 +18494,7 @@ async function auditStyle(loaded, specs, styleId, lock) {
18425
18494
  const refPalettes = [];
18426
18495
  for (const img of style.styleImages) {
18427
18496
  const abs = path37.resolve(loaded.root, img.path);
18428
- if (!existsSync29(abs)) continue;
18497
+ if (!existsSync30(abs)) continue;
18429
18498
  try {
18430
18499
  refPalettes.push(extractPalette(decodePng(await readFile25(abs)), 12));
18431
18500
  } catch {
@@ -19597,7 +19666,7 @@ function sheetFormat(args) {
19597
19666
  import path41 from "path";
19598
19667
 
19599
19668
  // src/pixellab-utils.ts
19600
- import { existsSync as existsSync30 } from "fs";
19669
+ import { existsSync as existsSync31 } from "fs";
19601
19670
  import { mkdir as mkdir12, readFile as readFile27, writeFile as writeFile15 } from "fs/promises";
19602
19671
  import path40 from "path";
19603
19672
  var UNZOOM_MIN_SIDE = 256;
@@ -19609,7 +19678,7 @@ function defaultUnzoomOut(input) {
19609
19678
  async function unzoomFile(client2, input, opts = {}) {
19610
19679
  const out = path40.resolve(opts.out ?? defaultUnzoomOut(input));
19611
19680
  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`);
19681
+ if (existsSync31(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
19613
19682
  const bytes = await readFile27(input);
19614
19683
  const metadata = imageMetadata(bytes);
19615
19684
  if (!metadata) throw new Error(`${input} is not a readable PNG or JPEG`);
@@ -19638,7 +19707,7 @@ async function generateFont(client2, opts) {
19638
19707
  const { ttf, atlas } = fontOutputPaths(opts.out);
19639
19708
  if (!opts.force) {
19640
19709
  for (const file of [ttf, atlas]) {
19641
- if (existsSync30(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`);
19642
19711
  }
19643
19712
  }
19644
19713
  const sleep3 = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
@@ -19752,7 +19821,7 @@ import { readFile as readFile29 } from "fs/promises";
19752
19821
 
19753
19822
  // src/pipeline/quality-regression.ts
19754
19823
  import path42 from "path";
19755
- import { existsSync as existsSync31 } from "fs";
19824
+ import { existsSync as existsSync32 } from "fs";
19756
19825
  import { readFile as readFile28 } from "fs/promises";
19757
19826
  import { z as z11 } from "zod";
19758
19827
  var SHA256_RE = /^[0-9a-f]{64}$/;
@@ -19995,7 +20064,7 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
19995
20064
  cases
19996
20065
  });
19997
20066
  const data = Buffer.from(JSON.stringify(baseline, null, 2) + "\n");
19998
- if (existsSync31(absolute) && !options.force) {
20067
+ if (existsSync32(absolute) && !options.force) {
19999
20068
  const current = await readFile28(absolute);
20000
20069
  if (!current.equals(data)) {
20001
20070
  throw new OverwriteRefusedError(
@@ -20192,9 +20261,9 @@ import path45 from "path";
20192
20261
 
20193
20262
  // src/recipes.ts
20194
20263
  import path44 from "path";
20195
- import { existsSync as existsSync32 } from "fs";
20264
+ import { existsSync as existsSync33 } from "fs";
20196
20265
  import { readdir as readdir3, readFile as readFile30 } from "fs/promises";
20197
- import { fileURLToPath } from "url";
20266
+ import { fileURLToPath as fileURLToPath2 } from "url";
20198
20267
  import { z as z12 } from "zod";
20199
20268
  var SHA256_RE2 = /^[0-9a-f]{64}$/;
20200
20269
  var RECIPE_ID_RE = /^[a-z0-9][a-z0-9-]*\/[a-z0-9][a-z0-9-]*$/;
@@ -20395,7 +20464,7 @@ async function readRecipeFile(recipePath, bundled) {
20395
20464
  return { recipe: parsed.data, authored: raw, path: absolute, dir: path44.dirname(absolute), bundled };
20396
20465
  }
20397
20466
  async function walkRecipeFiles(root) {
20398
- if (!existsSync32(root)) return [];
20467
+ if (!existsSync33(root)) return [];
20399
20468
  const out = [];
20400
20469
  for (const entry of await readdir3(root, { withFileTypes: true })) {
20401
20470
  const resolved = path44.join(root, entry.name);
@@ -20405,7 +20474,7 @@ async function walkRecipeFiles(root) {
20405
20474
  return out;
20406
20475
  }
20407
20476
  function bundledRecipeRoot() {
20408
- return fileURLToPath(new URL("../recipes/", import.meta.url));
20477
+ return fileURLToPath2(new URL("../recipes/", import.meta.url));
20409
20478
  }
20410
20479
  async function listBundledRecipes(root = bundledRecipeRoot()) {
20411
20480
  const recipes = await Promise.all(
@@ -20433,7 +20502,7 @@ function parseSelector(selector) {
20433
20502
  }
20434
20503
  async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
20435
20504
  const local = path44.resolve(target);
20436
- if (existsSync32(local)) {
20505
+ if (existsSync33(local)) {
20437
20506
  const recipePath = path44.basename(local) === RECIPE_FILE ? local : path44.join(local, RECIPE_FILE);
20438
20507
  return readRecipeFile(recipePath, false);
20439
20508
  }
@@ -20449,7 +20518,7 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
20449
20518
  }
20450
20519
  async function verifyFile(root, file) {
20451
20520
  const absolute = path44.resolve(root, ...file.path.split("/"));
20452
- if (!existsSync32(absolute)) {
20521
+ if (!existsSync33(absolute)) {
20453
20522
  return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
20454
20523
  }
20455
20524
  const actualSha256 = await sha256File(absolute);
@@ -20527,7 +20596,7 @@ async function installRecipe(target, options = {}) {
20527
20596
  })));
20528
20597
  if (!options.force) {
20529
20598
  for (const file of files) {
20530
- if (!existsSync32(file.path)) continue;
20599
+ if (!existsSync33(file.path)) continue;
20531
20600
  const current = await readFile30(file.path);
20532
20601
  if (!current.equals(file.data)) {
20533
20602
  throw new OverwriteRefusedError(
@@ -20767,7 +20836,7 @@ async function runRefine(args) {
20767
20836
 
20768
20837
  // src/cli/commands/skeleton.ts
20769
20838
  import path47 from "path";
20770
- import { existsSync as existsSync33 } from "fs";
20839
+ import { existsSync as existsSync34 } from "fs";
20771
20840
  import { readFile as readFile31, writeFile as writeFile16 } from "fs/promises";
20772
20841
 
20773
20842
  // src/skeleton-preview.ts
@@ -20879,7 +20948,7 @@ var Canvas = class {
20879
20948
  async function runEstimateSkeleton(args) {
20880
20949
  const image = args.target;
20881
20950
  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) {
20951
+ if (args.out && existsSync34(path47.resolve(args.out)) && !args.force) {
20883
20952
  throw new UsageError(`${args.out} already exists; pass --force to replace it`);
20884
20953
  }
20885
20954
  loadEnvFiles(path47.dirname(path47.resolve(args.manifest)));
@@ -20925,7 +20994,7 @@ async function runSkeletonPreview(args) {
20925
20994
  sourceFile = args.from ? path47.resolve(args.from) : spec.revision.sourceFile;
20926
20995
  label = `${spec.styleId}/${spec.assetId}`;
20927
20996
  }
20928
- if (!existsSync33(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}`);
20929
20998
  let json;
20930
20999
  try {
20931
21000
  json = JSON.parse(await readFile31(keypointsFile, "utf8"));
@@ -20935,7 +21004,7 @@ async function runSkeletonPreview(args) {
20935
21004
  const set = parseSkeletonSet(json, keypointsFile);
20936
21005
  let source;
20937
21006
  if (sourceFile) {
20938
- if (!existsSync33(sourceFile)) {
21007
+ if (!existsSync34(sourceFile)) {
20939
21008
  log(` source image does not exist yet, drawing on a plain ground: ${sourceFile}`);
20940
21009
  } else {
20941
21010
  const bytes = await readFile31(sourceFile);
@@ -20993,7 +21062,7 @@ async function runTools(args) {
20993
21062
 
20994
21063
  // src/cli/commands/workspace.ts
20995
21064
  import path48 from "path";
20996
- import { existsSync as existsSync34 } from "fs";
21065
+ import { existsSync as existsSync35 } from "fs";
20997
21066
  async function runWorkspace(args) {
20998
21067
  const workspacePath = path48.resolve(args.workspace ?? "pixelkiln.workspace.json");
20999
21068
  const dir = path48.dirname(workspacePath);
@@ -21023,7 +21092,7 @@ async function runWorkspace(args) {
21023
21092
  log(` registered "${id}" in ${path48.relative(process.cwd(), workspacePath)}`);
21024
21093
  log(` manifest: ${project.manifest}`);
21025
21094
  log(` lock: ${project.lock}`);
21026
- if (!existsSync34(lockPath)) {
21095
+ if (!existsSync35(lockPath)) {
21027
21096
  log(
21028
21097
  ` warning: no lockfile there yet; this project contributes no claims until one is generated`
21029
21098
  );
@@ -21047,7 +21116,7 @@ async function runWorkspace(args) {
21047
21116
  return;
21048
21117
  }
21049
21118
  if (args.subcommand === "list") {
21050
- if (!existsSync34(workspacePath)) {
21119
+ if (!existsSync35(workspacePath)) {
21051
21120
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
21052
21121
  }
21053
21122
  const ws = await loadWorkspace(workspacePath);
@@ -21067,7 +21136,7 @@ async function runWorkspace(args) {
21067
21136
  return;
21068
21137
  }
21069
21138
  if (args.subcommand === "status") {
21070
- if (!existsSync34(workspacePath)) {
21139
+ if (!existsSync35(workspacePath)) {
21071
21140
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
21072
21141
  }
21073
21142
  const ws = await loadWorkspace(workspacePath);
@@ -21129,7 +21198,7 @@ async function runHelp() {
21129
21198
  async function runVersion() {
21130
21199
  for (const up of ["../package.json", "../../package.json"]) {
21131
21200
  const url = new URL(up, import.meta.url);
21132
- if (!existsSync35(url)) continue;
21201
+ if (!existsSync36(url)) continue;
21133
21202
  const pkg = JSON.parse(await readFile32(url, "utf8"));
21134
21203
  if (pkg.name === "pixelkiln") {
21135
21204
  log(`${pkg.name} ${pkg.version}`);