pixelkiln 0.11.0 → 0.12.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
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import path20 from "path";
5
- import { existsSync as existsSync15 } from "fs";
6
- import { readFile as readFile16 } from "fs/promises";
4
+ import path22 from "path";
5
+ import { existsSync as existsSync17 } from "fs";
6
+ import { readFile as readFile17 } from "fs/promises";
7
7
 
8
8
  // src/env.ts
9
9
  import { readFileSync, existsSync } from "fs";
@@ -105,17 +105,23 @@ function formatCost(unit, amount) {
105
105
 
106
106
  // src/providers/comfyui.ts
107
107
  import { randomUUID } from "crypto";
108
- import { readFile as readFile2 } from "fs/promises";
108
+ import { readFile } from "fs/promises";
109
109
  import path2 from "path";
110
110
 
111
111
  // src/hash.ts
112
112
  import { createHash } from "crypto";
113
- import { readFile } from "fs/promises";
113
+ import { createReadStream } from "fs";
114
114
  function sha256(data) {
115
115
  return createHash("sha256").update(data).digest("hex");
116
116
  }
117
- async function sha256File(path21) {
118
- return sha256(await readFile(path21));
117
+ async function sha256File(path23) {
118
+ return new Promise((resolve, reject) => {
119
+ const hash = createHash("sha256");
120
+ const stream = createReadStream(path23);
121
+ stream.on("data", (chunk2) => hash.update(chunk2));
122
+ stream.on("error", reject);
123
+ stream.on("end", () => resolve(hash.digest("hex")));
124
+ });
119
125
  }
120
126
  function specHash(spec, styleImageHashes, providerOptionIdentity = spec.providerOptions) {
121
127
  return sha256(
@@ -683,7 +689,7 @@ var ComfyUIProvider = class _ComfyUIProvider {
683
689
  const workflowPath = path2.resolve(context.root, options.workflowFile);
684
690
  let raw;
685
691
  try {
686
- raw = await readFile2(workflowPath, "utf8");
692
+ raw = await readFile(workflowPath, "utf8");
687
693
  } catch (err) {
688
694
  throw new Error(
689
695
  `ComfyUI workflow for style "${context.styleId}" could not be read at ${workflowPath}: ${err instanceof Error ? err.message : String(err)}`
@@ -974,9 +980,9 @@ function normalizeBaseUrl(value) {
974
980
  }
975
981
  function historyError(status) {
976
982
  const messages = Array.isArray(status.messages) ? status.messages : [];
977
- for (const message3 of messages) {
978
- if (!Array.isArray(message3) || message3[0] !== "execution_error" || !isObject(message3[1])) continue;
979
- const detail = message3[1].exception_message;
983
+ for (const message5 of messages) {
984
+ if (!Array.isArray(message5) || message5[0] !== "execution_error" || !isObject(message5[1])) continue;
985
+ const detail = message5[1].exception_message;
980
986
  if (typeof detail === "string" && detail.trim()) {
981
987
  return `ComfyUI execution failed: ${detail.trim().slice(0, 300)}`;
982
988
  }
@@ -1097,8 +1103,8 @@ function validateResponse(schema, raw, operation) {
1097
1103
  throw new Error(`Invalid PixelLab response for ${operation}: ${issues}`);
1098
1104
  }
1099
1105
  var PixelLabError = class extends Error {
1100
- constructor(message3, status, body) {
1101
- super(message3);
1106
+ constructor(message5, status, body) {
1107
+ super(message5);
1102
1108
  this.status = status;
1103
1109
  this.body = body;
1104
1110
  this.name = "PixelLabError";
@@ -1123,11 +1129,11 @@ var PixelLabClient = class {
1123
1129
  * common than the failure mode of retrying (a duplicate object), and a
1124
1130
  * duplicate is visible and free to delete whereas a silent gap is neither.
1125
1131
  */
1126
- async request(path21, init, attempt = 0) {
1132
+ async request(path23, init, attempt = 0) {
1127
1133
  const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
1128
1134
  let res;
1129
1135
  try {
1130
- res = await fetch(`${BASE}${path21}`, {
1136
+ res = await fetch(`${BASE}${path23}`, {
1131
1137
  ...init,
1132
1138
  signal: init?.signal ?? AbortSignal.timeout(this.timeoutMs),
1133
1139
  headers: {
@@ -1139,24 +1145,24 @@ var PixelLabClient = class {
1139
1145
  } catch (err) {
1140
1146
  if (attempt < MAX_RETRIES) {
1141
1147
  await sleep(backoffMs(attempt));
1142
- return this.request(path21, init, attempt + 1);
1148
+ return this.request(path23, init, attempt + 1);
1143
1149
  }
1144
1150
  throw err;
1145
1151
  }
1146
1152
  if (!res.ok && shouldRetry(res.status) && attempt < MAX_RETRIES) {
1147
1153
  const waitMs = retryAfterMs(res.headers.get("retry-after")) ?? backoffMs(attempt);
1148
1154
  await sleep(waitMs);
1149
- return this.request(path21, init, attempt + 1);
1155
+ return this.request(path23, init, attempt + 1);
1150
1156
  }
1151
1157
  const text = await res.text();
1152
1158
  if (!res.ok) {
1153
- throw new PixelLabError(`${init?.method ?? "GET"} ${path21} \u2192 ${res.status}`, res.status, text);
1159
+ throw new PixelLabError(`${init?.method ?? "GET"} ${path23} \u2192 ${res.status}`, res.status, text);
1154
1160
  }
1155
1161
  if (!text) return {};
1156
1162
  try {
1157
1163
  return JSON.parse(text);
1158
1164
  } catch {
1159
- throw new Error(`${init?.method ?? "GET"} ${path21} returned invalid JSON`);
1165
+ throw new Error(`${init?.method ?? "GET"} ${path23} returned invalid JSON`);
1160
1166
  }
1161
1167
  }
1162
1168
  async balance() {
@@ -1996,9 +2002,9 @@ var RetroDiffusionClient = class {
1996
2002
  }
1997
2003
  return balance;
1998
2004
  }
1999
- async call(path21, init = {}) {
2005
+ async call(path23, init = {}) {
2000
2006
  if (!this.token) throw new Error("RD_API_KEY is not set");
2001
- const response = await this.request(`${this.baseUrl}${path21}`, {
2007
+ const response = await this.request(`${this.baseUrl}${path23}`, {
2002
2008
  ...init,
2003
2009
  headers: {
2004
2010
  "Content-Type": "application/json",
@@ -2262,8 +2268,8 @@ function retroError(value) {
2262
2268
  if (details.length) return details.join("; ");
2263
2269
  }
2264
2270
  if (record.detail && typeof record.detail === "object") {
2265
- const message3 = record.detail.message;
2266
- if (typeof message3 === "string") return message3;
2271
+ const message5 = record.detail.message;
2272
+ if (typeof message5 === "string") return message5;
2267
2273
  }
2268
2274
  return JSON.stringify(value);
2269
2275
  }
@@ -2315,13 +2321,13 @@ registerProvider({
2315
2321
  });
2316
2322
 
2317
2323
  // src/manifest.ts
2318
- import { readFile as readFile3 } from "fs/promises";
2324
+ import { readFile as readFile2 } from "fs/promises";
2319
2325
  import { existsSync as existsSync3 } from "fs";
2320
2326
  import path4 from "path";
2321
2327
  async function loadManifest(manifestPath) {
2322
2328
  const abs = path4.resolve(manifestPath);
2323
2329
  if (!existsSync3(abs)) throw new Error(`No manifest at ${abs}`);
2324
- const parsed = ManifestSchema.safeParse(JSON.parse(await readFile3(abs, "utf8")));
2330
+ const parsed = ManifestSchema.safeParse(JSON.parse(await readFile2(abs, "utf8")));
2325
2331
  if (!parsed.success) {
2326
2332
  const issues = parsed.error.issues.map((i) => ` ${i.path.join(".")}: ${i.message}`).join("\n");
2327
2333
  throw new Error(`Manifest at ${abs} is invalid:
@@ -2370,7 +2376,7 @@ async function resolveSpecs(loaded, filter) {
2370
2376
  let hit = styleImageCache.get(abs);
2371
2377
  if (!hit) {
2372
2378
  if (!existsSync3(abs)) throw new Error(`Style image not found: ${abs}`);
2373
- const buf = await readFile3(abs);
2379
+ const buf = await readFile2(abs);
2374
2380
  const metadata = imageMetadata(buf);
2375
2381
  if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${abs}`);
2376
2382
  if (metadata.width < 1 || metadata.height < 1) {
@@ -2496,7 +2502,7 @@ async function resolveStyleImages(loaded, styleId) {
2496
2502
  const out = [];
2497
2503
  for (const img of style.styleImages) {
2498
2504
  const file = path4.resolve(loaded.root, img.path);
2499
- const buf = await readFile3(file);
2505
+ const buf = await readFile2(file);
2500
2506
  const metadata = imageMetadata(buf);
2501
2507
  if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${file}`);
2502
2508
  if (metadata.width < 1 || metadata.height < 1) {
@@ -2543,13 +2549,13 @@ import path7 from "path";
2543
2549
  import path6 from "path";
2544
2550
 
2545
2551
  // src/lock.ts
2546
- import { readFile as readFile4, writeFile, rename, mkdir, rmdir, rm, stat } from "fs/promises";
2552
+ import { readFile as readFile3, writeFile, rename, mkdir, rmdir, rm, stat } from "fs/promises";
2547
2553
  import { existsSync as existsSync4 } from "fs";
2548
2554
  import path5 from "path";
2549
2555
  async function loadLock(lockPath) {
2550
2556
  if (!existsSync4(lockPath)) return { version: 2, entries: {} };
2551
2557
  try {
2552
- return parseLock(JSON.parse(await readFile4(lockPath, "utf8")));
2558
+ return parseLock(JSON.parse(await readFile3(lockPath, "utf8")));
2553
2559
  } catch (err) {
2554
2560
  throw new Error(
2555
2561
  `Lockfile at ${lockPath} is malformed:
@@ -2585,7 +2591,7 @@ async function writeLockWhileHeld(file, lock) {
2585
2591
  let disk = { version: 2, entries: {} };
2586
2592
  if (existsSync4(file)) {
2587
2593
  try {
2588
- disk = parseLock(JSON.parse(await readFile4(file, "utf8")));
2594
+ disk = parseLock(JSON.parse(await readFile3(file, "utf8")));
2589
2595
  } catch (err) {
2590
2596
  throw new Error(
2591
2597
  `Refusing to overwrite malformed lockfile at ${file}: ${err instanceof Error ? err.message : String(err)}`
@@ -3262,9 +3268,9 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
3262
3268
  );
3263
3269
  } catch (err) {
3264
3270
  failed++;
3265
- const message3 = err instanceof Error ? err.message : String(err);
3266
- upsert(lock, key, { status: "failed", error: message3, cost: 0 });
3267
- log2(` FAILED ${key}: ${message3}`);
3271
+ const message5 = err instanceof Error ? err.message : String(err);
3272
+ upsert(lock, key, { status: "failed", error: message5, cost: 0 });
3273
+ log2(` FAILED ${key}: ${message5}`);
3268
3274
  }
3269
3275
  await saveLock(lockPath, lock);
3270
3276
  }
@@ -3335,7 +3341,7 @@ async function poll(provider, lock, lockPath, opts = {}) {
3335
3341
 
3336
3342
  // src/pipeline/fetch.ts
3337
3343
  import { existsSync as existsSync6 } from "fs";
3338
- import { mkdir as mkdir2, readFile as readFile5, rename as rename2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
3344
+ import { mkdir as mkdir2, readFile as readFile4, rename as rename2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
3339
3345
  import path9 from "path";
3340
3346
  async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
3341
3347
  const log2 = opts.onProgress ?? (() => {
@@ -3395,7 +3401,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
3395
3401
  if (cacheDir) {
3396
3402
  await cacheMedia(
3397
3403
  cacheDir,
3398
- await readFile5(target),
3404
+ await readFile4(target),
3399
3405
  recorded.mediaType ?? MediaType.PNG,
3400
3406
  recorded.sha256
3401
3407
  );
@@ -3452,14 +3458,14 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
3452
3458
  });
3453
3459
  result.downloaded++;
3454
3460
  } catch (err) {
3455
- const message3 = err instanceof Error ? err.message : String(err);
3461
+ const message5 = err instanceof Error ? err.message : String(err);
3456
3462
  upsert(lock, key, {
3457
3463
  status: "download-failed",
3458
3464
  outputs: mergeOutputs(entry.outputs, outputs),
3459
- error: `download failed: ${message3}`
3465
+ error: `download failed: ${message5}`
3460
3466
  });
3461
3467
  result.failed++;
3462
- log2(` FAILED ${key}: ${message3}`);
3468
+ log2(` FAILED ${key}: ${message5}`);
3463
3469
  }
3464
3470
  await saveLock(lockPath, lock);
3465
3471
  }
@@ -3472,7 +3478,7 @@ async function readCachedMedia(cacheDir, hash, mediaType) {
3472
3478
  const file = path9.join(cacheDir, cacheFileName(hash, mediaType));
3473
3479
  if (!existsSync6(file)) return null;
3474
3480
  try {
3475
- const buf = await readFile5(file);
3481
+ const buf = await readFile4(file);
3476
3482
  if (sha256(buf) !== hash) return null;
3477
3483
  validateMedia(buf, mediaType);
3478
3484
  return buf;
@@ -3529,7 +3535,7 @@ import { access, stat as stat2 } from "fs/promises";
3529
3535
  import path10 from "path";
3530
3536
  async function doctor(loaded, specs, lock, lockPath, opts = {}) {
3531
3537
  const checks = [];
3532
- const add = (id, level, message3) => checks.push({ id, level, message: message3 });
3538
+ const add = (id, level, message5) => checks.push({ id, level, message: message5 });
3533
3539
  add(
3534
3540
  "manifest",
3535
3541
  "ok",
@@ -3659,11 +3665,11 @@ async function nearestExistingDirectory(start) {
3659
3665
 
3660
3666
  // src/pipeline/adopt.ts
3661
3667
  import { existsSync as existsSync9 } from "fs";
3662
- import { readFile as readFile7, writeFile as writeFile4 } from "fs/promises";
3668
+ import { readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
3663
3669
  import path11 from "path";
3664
3670
 
3665
3671
  // src/cache.ts
3666
- import { mkdir as mkdir3, readFile as readFile6, rename as rename3, rm as rm3, writeFile as writeFile3 } from "fs/promises";
3672
+ import { mkdir as mkdir3, readFile as readFile5, rename as rename3, rm as rm3, writeFile as writeFile3 } from "fs/promises";
3667
3673
  import { existsSync as existsSync8 } from "fs";
3668
3674
  import pathModule from "path";
3669
3675
  import { z as z3 } from "zod";
@@ -3679,10 +3685,10 @@ function isSha256Hash(value) {
3679
3685
  function parseCache(value) {
3680
3686
  return HashCacheSchema.parse(value);
3681
3687
  }
3682
- async function loadCache(path21) {
3683
- if (!existsSync8(path21)) return { version: 1, hashes: {} };
3688
+ async function loadCache(path23) {
3689
+ if (!existsSync8(path23)) return { version: 1, hashes: {} };
3684
3690
  try {
3685
- const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile6(path21, "utf8")));
3691
+ const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile5(path23, "utf8")));
3686
3692
  if (!parsed.success) return { version: 1, hashes: {} };
3687
3693
  return {
3688
3694
  version: 1,
@@ -3694,18 +3700,18 @@ async function loadCache(path21) {
3694
3700
  return { version: 1, hashes: {} };
3695
3701
  }
3696
3702
  }
3697
- async function saveCache(path21, cache) {
3703
+ async function saveCache(path23, cache) {
3698
3704
  const sorted = {};
3699
3705
  for (const key of Object.keys(cache.hashes).sort()) {
3700
3706
  const hash = cache.hashes[key];
3701
3707
  if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
3702
3708
  sorted[key] = hash;
3703
3709
  }
3704
- await mkdir3(pathModule.dirname(pathModule.resolve(path21)), { recursive: true });
3705
- const tmp = `${path21}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
3710
+ await mkdir3(pathModule.dirname(pathModule.resolve(path23)), { recursive: true });
3711
+ const tmp = `${path23}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
3706
3712
  try {
3707
3713
  await writeFile3(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
3708
- await rename3(tmp, path21);
3714
+ await rename3(tmp, path23);
3709
3715
  } finally {
3710
3716
  await rm3(tmp, { force: true });
3711
3717
  }
@@ -3740,7 +3746,7 @@ async function adopt(provider, specs, lock, lockPath, opts = {}) {
3740
3746
  unmatchedLocal.push(`${spec.styleId}/${spec.assetId} (no file at ${spec.outFile})`);
3741
3747
  continue;
3742
3748
  }
3743
- const bytes = await readFile7(spec.outFile);
3749
+ const bytes = await readFile6(spec.outFile);
3744
3750
  try {
3745
3751
  decodePng(bytes);
3746
3752
  } catch (err) {
@@ -3857,7 +3863,7 @@ async function tagAdopted(provider, specs, lock, opts = {}) {
3857
3863
  async function writePromptsBack(manifestPath, lock, opts = {}) {
3858
3864
  const log2 = opts.onProgress ?? (() => {
3859
3865
  });
3860
- const raw = JSON.parse(await readFile7(manifestPath, "utf8"));
3866
+ const raw = JSON.parse(await readFile6(manifestPath, "utf8"));
3861
3867
  const best = /* @__PURE__ */ new Map();
3862
3868
  for (const entry of Object.values(lock.entries)) {
3863
3869
  if (!entry.objectId || !entry.prompt) continue;
@@ -4156,10 +4162,10 @@ function serveReviewPage(opts) {
4156
4162
  res.end(JSON.stringify(result));
4157
4163
  server.close(() => resolve(result));
4158
4164
  } catch (err) {
4159
- const message3 = err instanceof Error ? err.message : String(err);
4160
- log2(` apply failed: ${message3}`);
4165
+ const message5 = err instanceof Error ? err.message : String(err);
4166
+ log2(` apply failed: ${message5}`);
4161
4167
  res.writeHead(500, { "Content-Type": "text/plain" });
4162
- res.end(message3);
4168
+ res.end(message5);
4163
4169
  }
4164
4170
  return;
4165
4171
  }
@@ -4247,7 +4253,7 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
4247
4253
  }
4248
4254
 
4249
4255
  // src/pipeline/init.ts
4250
- import { readdir, readFile as readFile8, writeFile as writeFile5 } from "fs/promises";
4256
+ import { readdir, readFile as readFile7, writeFile as writeFile5 } from "fs/promises";
4251
4257
  import { existsSync as existsSync10 } from "fs";
4252
4258
  import path12 from "path";
4253
4259
  function pngSize(buf) {
@@ -4277,7 +4283,7 @@ async function scanAssets(root, opts = {}) {
4277
4283
  const skipped = [];
4278
4284
  const seen = /* @__PURE__ */ new Map();
4279
4285
  for (const file of files.sort()) {
4280
- const size = pngSize(await readFile8(file));
4286
+ const size = pngSize(await readFile7(file));
4281
4287
  if (!size) {
4282
4288
  skipped.push(`${file} (not a readable PNG)`);
4283
4289
  continue;
@@ -4346,7 +4352,7 @@ async function writeManifestFile(target, manifest) {
4346
4352
  }
4347
4353
 
4348
4354
  // src/pipeline/salvage.ts
4349
- import { readFile as readFile9 } from "fs/promises";
4355
+ import { readFile as readFile8 } from "fs/promises";
4350
4356
  import { existsSync as existsSync11 } from "fs";
4351
4357
  import path13 from "path";
4352
4358
  async function loadClaims(lockPaths) {
@@ -4355,7 +4361,7 @@ async function loadClaims(lockPaths) {
4355
4361
  if (!existsSync11(p)) throw new Error(`Claim lockfile not found: ${p}`);
4356
4362
  let parsed;
4357
4363
  try {
4358
- parsed = parseLock(JSON.parse(await readFile9(p, "utf8")));
4364
+ parsed = parseLock(JSON.parse(await readFile8(p, "utf8")));
4359
4365
  } catch {
4360
4366
  throw new Error(`Claim lockfile is malformed: ${p}`);
4361
4367
  }
@@ -4526,7 +4532,7 @@ async function applyTags(provider, decisions, existing, opts = {}) {
4526
4532
  }
4527
4533
 
4528
4534
  // src/workspace.ts
4529
- import { mkdir as mkdir4, readFile as readFile10, rename as rename4, rm as rm4, writeFile as writeFile6 } from "fs/promises";
4535
+ import { mkdir as mkdir4, readFile as readFile9, rename as rename4, rm as rm4, writeFile as writeFile6 } from "fs/promises";
4530
4536
  import { existsSync as existsSync12 } from "fs";
4531
4537
  import path14 from "path";
4532
4538
  import { z as z4 } from "zod";
@@ -4556,7 +4562,7 @@ async function loadWorkspace(workspacePath) {
4556
4562
  if (!existsSync12(workspacePath)) return { version: 1, projects: [] };
4557
4563
  let raw;
4558
4564
  try {
4559
- raw = JSON.parse(await readFile10(workspacePath, "utf8"));
4565
+ raw = JSON.parse(await readFile9(workspacePath, "utf8"));
4560
4566
  } catch (err) {
4561
4567
  throw new Error(
4562
4568
  `Workspace catalog at ${workspacePath} is malformed:
@@ -4753,7 +4759,7 @@ async function workspaceStatus(ws, dir) {
4753
4759
  }
4754
4760
 
4755
4761
  // src/pipeline/audit.ts
4756
- import { readFile as readFile11 } from "fs/promises";
4762
+ import { readFile as readFile10 } from "fs/promises";
4757
4763
  import { existsSync as existsSync13 } from "fs";
4758
4764
  import path15 from "path";
4759
4765
  function colorDistance(a, b) {
@@ -4805,7 +4811,7 @@ async function auditStyle(loaded, specs, styleId, lock) {
4805
4811
  continue;
4806
4812
  }
4807
4813
  try {
4808
- const png = decodePng(await readFile11(output.absolutePath));
4814
+ const png = decodePng(await readFile10(output.absolutePath));
4809
4815
  const palette = extractPalette(png, 12);
4810
4816
  assets.push({
4811
4817
  assetId: spec.assetId,
@@ -4832,7 +4838,7 @@ async function auditStyle(loaded, specs, styleId, lock) {
4832
4838
  const abs = path15.resolve(loaded.root, img.path);
4833
4839
  if (!existsSync13(abs)) continue;
4834
4840
  try {
4835
- refPalettes.push(extractPalette(decodePng(await readFile11(abs)), 12));
4841
+ refPalettes.push(extractPalette(decodePng(await readFile10(abs)), 12));
4836
4842
  } catch {
4837
4843
  }
4838
4844
  }
@@ -4909,7 +4915,7 @@ function hex(c) {
4909
4915
 
4910
4916
  // src/pipeline/cache-health.ts
4911
4917
  import { existsSync as existsSync14 } from "fs";
4912
- import { readFile as readFile12, readdir as readdir2, rm as rm5 } from "fs/promises";
4918
+ import { readFile as readFile11, readdir as readdir2, rm as rm5 } from "fs/promises";
4913
4919
  import path16 from "path";
4914
4920
  async function inspectCaches(lock, lockPath, options = {}) {
4915
4921
  if (options.prune && !existsSync14(lockPath)) {
@@ -4939,7 +4945,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
4939
4945
  await saveCache(remotePath, { version: 1, hashes: {} });
4940
4946
  removed.resetRemoteHashCache = true;
4941
4947
  } else if (remoteHashes.invalidIds.length) {
4942
- const cache = parseCache(JSON.parse(await readFile12(remotePath, "utf8")));
4948
+ const cache = parseCache(JSON.parse(await readFile11(remotePath, "utf8")));
4943
4949
  for (const id of remoteHashes.invalidIds) delete cache.hashes[id];
4944
4950
  removed.remoteHashEntries = remoteHashes.invalidIds.length;
4945
4951
  await saveCache(remotePath, cache);
@@ -4983,7 +4989,7 @@ async function inspectContentCache(contentDir, referenced) {
4983
4989
  const expected = mediaType ? entry.name.slice(0, -4) : "";
4984
4990
  let bytes;
4985
4991
  try {
4986
- bytes = await readFile12(file);
4992
+ bytes = await readFile11(file);
4987
4993
  report.bytes += bytes.length;
4988
4994
  } catch (err) {
4989
4995
  report.invalid.push({
@@ -5030,7 +5036,7 @@ async function inspectRemoteHashCache(remotePath) {
5030
5036
  if (!report.exists) return report;
5031
5037
  let cache;
5032
5038
  try {
5033
- cache = parseCache(JSON.parse(await readFile12(remotePath, "utf8")));
5039
+ cache = parseCache(JSON.parse(await readFile11(remotePath, "utf8")));
5034
5040
  } catch (err) {
5035
5041
  report.error = err instanceof Error ? err.message : String(err);
5036
5042
  return report;
@@ -5047,14 +5053,14 @@ async function inspectRemoteHashCache(remotePath) {
5047
5053
  // src/pipeline/refine.ts
5048
5054
  import path18 from "path";
5049
5055
  import { execFile } from "child_process";
5050
- import { mkdtemp, readFile as readFile14, rm as rm7 } from "fs/promises";
5056
+ import { mkdtemp, readFile as readFile13, rm as rm7 } from "fs/promises";
5051
5057
  import { tmpdir } from "os";
5052
5058
  import { promisify } from "util";
5053
5059
 
5054
5060
  // src/artifacts.ts
5055
5061
  import path17 from "path";
5056
5062
  import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
5057
- import { mkdir as mkdir5, readFile as readFile13, rename as rename5, rm as rm6, writeFile as writeFile7 } from "fs/promises";
5063
+ import { mkdir as mkdir5, readFile as readFile12, rename as rename5, rm as rm6, writeFile as writeFile7 } from "fs/promises";
5058
5064
  var activeTransactions = /* @__PURE__ */ new Set();
5059
5065
  function message(error) {
5060
5066
  return error instanceof Error ? error.message : String(error);
@@ -5114,11 +5120,11 @@ function parseArtifactManifest(absolute, data) {
5114
5120
  }
5115
5121
  async function readArtifactBundleManifest(manifestPath) {
5116
5122
  const absolute = path17.resolve(manifestPath);
5117
- return parseArtifactManifest(absolute, await readFile13(absolute, "utf8"));
5123
+ return parseArtifactManifest(absolute, await readFile12(absolute, "utf8"));
5118
5124
  }
5119
5125
  async function readOptional(file) {
5120
5126
  try {
5121
- return await readFile13(file);
5127
+ return await readFile12(file);
5122
5128
  } catch (error) {
5123
5129
  if (isCode(error, "ENOENT")) return null;
5124
5130
  throw error;
@@ -5307,7 +5313,7 @@ async function verifyArtifactBundle(manifestPath) {
5307
5313
  for (const source of raw.sources) {
5308
5314
  let actual = null;
5309
5315
  try {
5310
- actual = digest(await readFile13(path17.resolve(root, source.path)));
5316
+ actual = digest(await readFile12(path17.resolve(root, source.path)));
5311
5317
  } catch (error) {
5312
5318
  if (!isCode(error, "ENOENT")) throw error;
5313
5319
  }
@@ -5315,7 +5321,7 @@ async function verifyArtifactBundle(manifestPath) {
5315
5321
  }
5316
5322
  for (const output of raw.outputs) {
5317
5323
  try {
5318
- if (digest(await readFile13(path17.resolve(root, output.path))) !== output.sha256) {
5324
+ if (digest(await readFile12(path17.resolve(root, output.path))) !== output.sha256) {
5319
5325
  changedOutputs.push(output.path);
5320
5326
  }
5321
5327
  } catch (error) {
@@ -5387,7 +5393,7 @@ async function writeArtifactBundle(files, options = {}) {
5387
5393
  const unchanged = [];
5388
5394
  for (const artifact of normalized) {
5389
5395
  try {
5390
- const current = await readFile13(artifact.destination);
5396
+ const current = await readFile12(artifact.destination);
5391
5397
  if (current.equals(artifact.data)) {
5392
5398
  unchanged.push(artifact.destination);
5393
5399
  } else {
@@ -5706,7 +5712,7 @@ async function refineAsset(options) {
5706
5712
  if (options.minTransparency !== void 0 && (!Number.isFinite(options.minTransparency) || options.minTransparency < 0 || options.minTransparency > 1)) throw new Error("minTransparency must be between 0 and 1.");
5707
5713
  let sourcePng;
5708
5714
  try {
5709
- sourcePng = decodePng(await readFile14(source));
5715
+ sourcePng = decodePng(await readFile13(source));
5710
5716
  } catch (error) {
5711
5717
  throw new Error(`Cannot read refinement source ${source}: ${message2(error)}`, { cause: error });
5712
5718
  }
@@ -5733,7 +5739,7 @@ async function refineAsset(options) {
5733
5739
  );
5734
5740
  }
5735
5741
  const detection = parseDetection(firstJsonObject(stdout));
5736
- const native = decodePng(await readFile14(recovered));
5742
+ const native = decodePng(await readFile13(recovered));
5737
5743
  const quantized = quantizeToPalette(native, palette);
5738
5744
  const png = encodeRgbaPng(quantized.width, quantized.height, quantized.pixels);
5739
5745
  const audit = auditRefinement(
@@ -5832,7 +5838,7 @@ async function approveQualityRecord(recordPath, approval) {
5832
5838
  }
5833
5839
  const root = path18.dirname(record);
5834
5840
  const outputPath = path18.resolve(root, manifest.outputs[0].path);
5835
- const output = await readFile14(outputPath);
5841
+ const output = await readFile13(outputPath);
5836
5842
  const sources = manifest.sources.map((source) => ({
5837
5843
  ...source,
5838
5844
  path: path18.resolve(root, source.path)
@@ -6118,7 +6124,7 @@ function isRecord(value) {
6118
6124
  }
6119
6125
 
6120
6126
  // src/pick/salvage-server.ts
6121
- import { mkdir as mkdir6, writeFile as writeFile8, readFile as readFile15 } from "fs/promises";
6127
+ import { mkdir as mkdir6, writeFile as writeFile8, readFile as readFile14 } from "fs/promises";
6122
6128
  import path19 from "path";
6123
6129
 
6124
6130
  // src/pick/salvage-sheet.ts
@@ -6366,7 +6372,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
6366
6372
  }
6367
6373
  }
6368
6374
  await applyTags(provider, decisions, existingTags, { onProgress: log2 });
6369
- const raw = JSON.parse(await readFile15(ctx.manifestPath, "utf8"));
6375
+ const raw = JSON.parse(await readFile14(ctx.manifestPath, "utf8"));
6370
6376
  for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
6371
6377
  await writeFile8(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
6372
6378
  await saveLock(ctx.lockPath, ctx.lock);
@@ -6375,14 +6381,742 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
6375
6381
  });
6376
6382
  }
6377
6383
 
6384
+ // src/recipes.ts
6385
+ import path20 from "path";
6386
+ import { existsSync as existsSync15 } from "fs";
6387
+ import { readdir as readdir3, readFile as readFile15 } from "fs/promises";
6388
+ import { fileURLToPath } from "url";
6389
+ import { z as z5 } from "zod";
6390
+ var SHA256_RE = /^[0-9a-f]{64}$/;
6391
+ var RECIPE_ID_RE = /^[a-z0-9][a-z0-9-]*\/[a-z0-9][a-z0-9-]*$/;
6392
+ var VERSION_RE = /^\d+\.\d+\.\d+$/;
6393
+ var RECIPE_FILE = "pixelkiln.recipe.json";
6394
+ var RECIPE_DIR_TOKEN = "{{recipeDir}}";
6395
+ var PortablePathSchema = z5.string().min(1).refine((value) => {
6396
+ if (value.includes("\\") || path20.posix.isAbsolute(value)) return false;
6397
+ const parts = value.split("/");
6398
+ return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
6399
+ }, "expected a portable relative path without '.', '..', or backslashes");
6400
+ var RecipeBindingSchema = z5.object({
6401
+ nodeId: z5.string().min(1),
6402
+ input: z5.string().min(1)
6403
+ }).strict();
6404
+ var RecipeSchema = z5.object({
6405
+ $schema: z5.string().url().optional(),
6406
+ format: z5.literal("pixelkiln-recipe"),
6407
+ schemaVersion: z5.literal(1),
6408
+ id: z5.string().regex(RECIPE_ID_RE, "expected provider/name"),
6409
+ version: z5.string().regex(VERSION_RE, "expected x.y.z"),
6410
+ provider: z5.string().regex(/^[a-z0-9][a-z0-9-]*$/),
6411
+ summary: z5.string().min(1).max(240),
6412
+ files: z5.array(z5.object({
6413
+ path: PortablePathSchema,
6414
+ role: z5.enum(["workflow", "reference"]),
6415
+ sha256: z5.string().regex(SHA256_RE)
6416
+ }).strict()).default([]),
6417
+ models: z5.array(z5.object({
6418
+ path: PortablePathSchema,
6419
+ sha256: z5.string().regex(SHA256_RE),
6420
+ source: z5.string().url(),
6421
+ license: z5.string().min(1)
6422
+ }).strict()).default([]),
6423
+ styleId: z5.string().regex(/^[a-z0-9][a-z0-9-]*$/),
6424
+ style: StyleSchema,
6425
+ workflow: z5.object({
6426
+ path: PortablePathSchema,
6427
+ outputNodeId: z5.string().min(1),
6428
+ numImages: z5.number().int().min(1).max(16),
6429
+ bindings: z5.object({
6430
+ prompt: RecipeBindingSchema,
6431
+ width: RecipeBindingSchema,
6432
+ height: RecipeBindingSchema,
6433
+ batchSize: RecipeBindingSchema,
6434
+ seed: RecipeBindingSchema.optional()
6435
+ }).strict()
6436
+ }).strict().optional(),
6437
+ quality: z5.object({
6438
+ stage: z5.enum(["composition-source", "production-candidate"]),
6439
+ workingCanvas: z5.object({
6440
+ width: z5.number().int().min(16),
6441
+ height: z5.number().int().min(16)
6442
+ }).strict(),
6443
+ recommendedNativeSize: z5.object({
6444
+ min: z5.number().int().min(1),
6445
+ max: z5.number().int().min(1)
6446
+ }).strict(),
6447
+ paletteColors: z5.object({
6448
+ min: z5.number().int().min(1),
6449
+ max: z5.number().int().min(1)
6450
+ }).strict(),
6451
+ background: z5.enum(["opaque", "transparent", "full-bleed"]),
6452
+ checks: z5.array(z5.enum([
6453
+ "prompt-coverage",
6454
+ "native-grid",
6455
+ "final-palette",
6456
+ "alpha-edge",
6457
+ "human-1x"
6458
+ ])).min(1),
6459
+ notes: z5.array(z5.string().min(1)).default([])
6460
+ }).strict(),
6461
+ integrity: z5.object({
6462
+ algorithm: z5.literal("sha256"),
6463
+ digest: z5.string().regex(SHA256_RE)
6464
+ }).strict()
6465
+ }).strict().superRefine((recipe, context) => {
6466
+ const filePaths = recipe.files.map((file) => file.path);
6467
+ if (new Set(filePaths).size !== filePaths.length) {
6468
+ context.addIssue({ code: z5.ZodIssueCode.custom, path: ["files"], message: "duplicate file path" });
6469
+ }
6470
+ const modelPaths = recipe.models.map((model) => model.path);
6471
+ if (new Set(modelPaths).size !== modelPaths.length) {
6472
+ context.addIssue({ code: z5.ZodIssueCode.custom, path: ["models"], message: "duplicate model path" });
6473
+ }
6474
+ if (filePaths.includes(RECIPE_FILE)) {
6475
+ context.addIssue({
6476
+ code: z5.ZodIssueCode.custom,
6477
+ path: ["files"],
6478
+ message: `${RECIPE_FILE} cannot hash itself`
6479
+ });
6480
+ }
6481
+ if (recipe.quality.recommendedNativeSize.min > recipe.quality.recommendedNativeSize.max) {
6482
+ context.addIssue({
6483
+ code: z5.ZodIssueCode.custom,
6484
+ path: ["quality", "recommendedNativeSize"],
6485
+ message: "min cannot exceed max"
6486
+ });
6487
+ }
6488
+ if (recipe.quality.paletteColors.min > recipe.quality.paletteColors.max) {
6489
+ context.addIssue({
6490
+ code: z5.ZodIssueCode.custom,
6491
+ path: ["quality", "paletteColors"],
6492
+ message: "min cannot exceed max"
6493
+ });
6494
+ }
6495
+ if (recipe.workflow && !recipe.files.some(
6496
+ (file) => file.path === recipe.workflow.path && file.role === "workflow"
6497
+ )) {
6498
+ context.addIssue({
6499
+ code: z5.ZodIssueCode.custom,
6500
+ path: ["workflow", "path"],
6501
+ message: "must name a file whose role is workflow"
6502
+ });
6503
+ }
6504
+ if (recipe.provider === "comfyui") {
6505
+ if (!recipe.workflow) {
6506
+ context.addIssue({
6507
+ code: z5.ZodIssueCode.custom,
6508
+ path: ["workflow"],
6509
+ message: "ComfyUI recipes require workflow metadata"
6510
+ });
6511
+ return;
6512
+ }
6513
+ const options = recipe.style.providerOptions.comfyui;
6514
+ if (!options || typeof options !== "object" || Array.isArray(options)) {
6515
+ context.addIssue({
6516
+ code: z5.ZodIssueCode.custom,
6517
+ path: ["style", "providerOptions", "comfyui"],
6518
+ message: "ComfyUI recipes require ComfyUI provider options"
6519
+ });
6520
+ return;
6521
+ }
6522
+ const expected = {
6523
+ workflowFile: `${RECIPE_DIR_TOKEN}/${recipe.workflow.path}`,
6524
+ outputNodeId: recipe.workflow.outputNodeId,
6525
+ numImages: recipe.workflow.numImages,
6526
+ bindings: recipe.workflow.bindings
6527
+ };
6528
+ if (JSON.stringify(canonical3(options)) !== JSON.stringify(canonical3(expected))) {
6529
+ context.addIssue({
6530
+ code: z5.ZodIssueCode.custom,
6531
+ path: ["style", "providerOptions", "comfyui"],
6532
+ message: "must match workflow metadata and use {{recipeDir}} for workflowFile"
6533
+ });
6534
+ }
6535
+ }
6536
+ });
6537
+ function canonical3(value) {
6538
+ if (value === null || typeof value === "string" || typeof value === "boolean") return value;
6539
+ if (typeof value === "number") {
6540
+ if (!Number.isFinite(value)) throw new Error("Recipe numbers must be finite.");
6541
+ return value;
6542
+ }
6543
+ if (Array.isArray(value)) return value.map(canonical3);
6544
+ if (typeof value === "object") {
6545
+ const sorted = {};
6546
+ for (const key of Object.keys(value).sort()) {
6547
+ const item = value[key];
6548
+ if (item !== void 0) sorted[key] = canonical3(item);
6549
+ }
6550
+ return sorted;
6551
+ }
6552
+ throw new Error(`Recipe metadata cannot contain ${typeof value} values.`);
6553
+ }
6554
+ function recipeDigest(recipe) {
6555
+ const { integrity: _integrity, ...payload } = recipe;
6556
+ return sha256(JSON.stringify(canonical3(payload)));
6557
+ }
6558
+ function message3(error) {
6559
+ return error instanceof Error ? error.message : String(error);
6560
+ }
6561
+ async function readRecipeFile(recipePath, bundled) {
6562
+ const absolute = path20.resolve(recipePath);
6563
+ let raw;
6564
+ try {
6565
+ raw = JSON.parse(await readFile15(absolute, "utf8"));
6566
+ } catch (error) {
6567
+ throw new Error(`Could not read recipe ${absolute}: ${message3(error)}`, { cause: error });
6568
+ }
6569
+ const parsed = RecipeSchema.safeParse(raw);
6570
+ if (!parsed.success) {
6571
+ throw new Error(
6572
+ `Invalid recipe ${absolute}:
6573
+ ` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
6574
+ );
6575
+ }
6576
+ return { recipe: parsed.data, path: absolute, dir: path20.dirname(absolute), bundled };
6577
+ }
6578
+ async function walkRecipeFiles(root) {
6579
+ if (!existsSync15(root)) return [];
6580
+ const out = [];
6581
+ for (const entry of await readdir3(root, { withFileTypes: true })) {
6582
+ const resolved = path20.join(root, entry.name);
6583
+ if (entry.isDirectory()) out.push(...await walkRecipeFiles(resolved));
6584
+ else if (entry.isFile() && entry.name === RECIPE_FILE) out.push(resolved);
6585
+ }
6586
+ return out;
6587
+ }
6588
+ function bundledRecipeRoot() {
6589
+ return fileURLToPath(new URL("../recipes/", import.meta.url));
6590
+ }
6591
+ async function listBundledRecipes(root = bundledRecipeRoot()) {
6592
+ const recipes = await Promise.all(
6593
+ (await walkRecipeFiles(path20.resolve(root))).map((file) => readRecipeFile(file, true))
6594
+ );
6595
+ return recipes.sort(
6596
+ (a, b) => a.recipe.id.localeCompare(b.recipe.id) || compareVersions(b.recipe.version, a.recipe.version)
6597
+ );
6598
+ }
6599
+ function compareVersions(a, b) {
6600
+ const av = a.split(".").map(Number);
6601
+ const bv = b.split(".").map(Number);
6602
+ for (let i = 0; i < 3; i++) {
6603
+ const difference = av[i] - bv[i];
6604
+ if (difference) return difference;
6605
+ }
6606
+ return 0;
6607
+ }
6608
+ function parseSelector(selector) {
6609
+ const match = selector.match(/^([^@]+?)(?:@(\d+\.\d+\.\d+))?$/);
6610
+ if (!match || !RECIPE_ID_RE.test(match[1])) {
6611
+ throw new Error(`Invalid recipe selector "${selector}". Expected provider/name or provider/name@x.y.z.`);
6612
+ }
6613
+ return { id: match[1], version: match[2] };
6614
+ }
6615
+ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
6616
+ const local = path20.resolve(target);
6617
+ if (existsSync15(local)) {
6618
+ const recipePath = path20.basename(local) === RECIPE_FILE ? local : path20.join(local, RECIPE_FILE);
6619
+ return readRecipeFile(recipePath, false);
6620
+ }
6621
+ if (target.startsWith(".") || target.startsWith("/") || target.includes("\\")) {
6622
+ throw new Error(`Recipe path not found: ${local}`);
6623
+ }
6624
+ const selector = parseSelector(target);
6625
+ const matches = (await listBundledRecipes(bundledRoot)).filter(
6626
+ (entry) => entry.recipe.id === selector.id && (selector.version === void 0 || entry.recipe.version === selector.version)
6627
+ );
6628
+ if (!matches.length) throw new Error(`Bundled recipe not found: ${target}`);
6629
+ return matches[0];
6630
+ }
6631
+ async function verifyFile(root, file) {
6632
+ const absolute = path20.resolve(root, ...file.path.split("/"));
6633
+ if (!existsSync15(absolute)) {
6634
+ return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
6635
+ }
6636
+ const actualSha256 = await sha256File(absolute);
6637
+ return {
6638
+ path: file.path,
6639
+ expectedSha256: file.sha256,
6640
+ actualSha256,
6641
+ status: actualSha256 === file.sha256 ? "ok" : "mismatch"
6642
+ };
6643
+ }
6644
+ async function verifyRecipe(target, options = {}) {
6645
+ const loaded = await resolveRecipe(target, options.bundledRoot);
6646
+ const actualIntegrity = recipeDigest(loaded.recipe);
6647
+ const files = await Promise.all(loaded.recipe.files.map((file) => verifyFile(loaded.dir, file)));
6648
+ const modelRoot = options.modelRoot ? path20.resolve(options.modelRoot) : null;
6649
+ const models = modelRoot ? await Promise.all(loaded.recipe.models.map(async (model) => ({
6650
+ ...await verifyFile(modelRoot, model),
6651
+ source: model.source,
6652
+ license: model.license
6653
+ }))) : loaded.recipe.models.map((model) => ({
6654
+ path: model.path,
6655
+ expectedSha256: model.sha256,
6656
+ actualSha256: null,
6657
+ source: model.source,
6658
+ license: model.license,
6659
+ status: "unchecked"
6660
+ }));
6661
+ const integrityStatus = actualIntegrity === loaded.recipe.integrity.digest ? "ok" : "mismatch";
6662
+ return {
6663
+ version: 1,
6664
+ ok: integrityStatus === "ok" && files.every((file) => file.status === "ok") && (!modelRoot || models.every((model) => model.status === "ok")),
6665
+ recipe: {
6666
+ id: loaded.recipe.id,
6667
+ version: loaded.recipe.version,
6668
+ path: loaded.path,
6669
+ bundled: loaded.bundled
6670
+ },
6671
+ integrity: {
6672
+ expectedSha256: loaded.recipe.integrity.digest,
6673
+ actualSha256: actualIntegrity,
6674
+ status: integrityStatus
6675
+ },
6676
+ files,
6677
+ models,
6678
+ modelRoot
6679
+ };
6680
+ }
6681
+ function renderStyle(style, destination, cwd) {
6682
+ const portableDir = path20.relative(cwd, destination).split(path20.sep).join("/") || ".";
6683
+ const replace = (value) => {
6684
+ if (typeof value === "string") return value.replaceAll(RECIPE_DIR_TOKEN, portableDir);
6685
+ if (Array.isArray(value)) return value.map(replace);
6686
+ if (value && typeof value === "object") {
6687
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, replace(item)]));
6688
+ }
6689
+ return value;
6690
+ };
6691
+ return replace(style);
6692
+ }
6693
+ async function installRecipe(target, options = {}) {
6694
+ const cwd = path20.resolve(options.cwd ?? process.cwd());
6695
+ const loaded = await resolveRecipe(target, options.bundledRoot);
6696
+ const verification = await verifyRecipe(loaded.path);
6697
+ if (!verification.ok) {
6698
+ throw new Error(`Recipe ${loaded.recipe.id}@${loaded.recipe.version} failed integrity verification.`);
6699
+ }
6700
+ const destination = options.out ? path20.resolve(cwd, options.out) : path20.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
6701
+ const sources = [
6702
+ { path: RECIPE_FILE, source: loaded.path },
6703
+ ...loaded.recipe.files.map((file) => ({ path: file.path, source: path20.join(loaded.dir, ...file.path.split("/")) }))
6704
+ ];
6705
+ const files = await Promise.all(sources.map(async (file) => ({
6706
+ path: path20.join(destination, ...file.path.split("/")),
6707
+ data: await readFile15(file.source)
6708
+ })));
6709
+ if (!options.force) {
6710
+ for (const file of files) {
6711
+ if (!existsSync15(file.path)) continue;
6712
+ const current = await readFile15(file.path);
6713
+ if (!current.equals(file.data)) {
6714
+ throw new Error(
6715
+ `Recipe destination has local changes: ${file.path}. Choose another --out or pass --force to replace declared recipe files.`
6716
+ );
6717
+ }
6718
+ }
6719
+ }
6720
+ const written = await writeArtifactBundle(files);
6721
+ return {
6722
+ recipe: loaded.recipe,
6723
+ destination,
6724
+ changed: written.changed,
6725
+ unchanged: written.unchanged,
6726
+ styleId: loaded.recipe.styleId,
6727
+ style: renderStyle(loaded.recipe.style, destination, cwd)
6728
+ };
6729
+ }
6730
+
6731
+ // src/pipeline/quality-regression.ts
6732
+ import path21 from "path";
6733
+ import { existsSync as existsSync16 } from "fs";
6734
+ import { readFile as readFile16 } from "fs/promises";
6735
+ import { z as z6 } from "zod";
6736
+ var SHA256_RE2 = /^[0-9a-f]{64}$/;
6737
+ var HEX_RE = /^#[0-9a-f]{6}$/;
6738
+ var MAX_REDMEAN_DISTANCE = 765;
6739
+ var QualityPathSchema = z6.string().min(1).refine(
6740
+ (value) => !path21.posix.isAbsolute(value) && !value.includes("\\"),
6741
+ "expected a portable relative path"
6742
+ );
6743
+ var DEFAULT_QUALITY_TOLERANCES = {
6744
+ requireExactHash: false,
6745
+ maxColorCountIncrease: 0,
6746
+ maxNewColors: 0,
6747
+ maxTransparencyDelta: 0.01,
6748
+ maxPartialAlphaIncrease: 0,
6749
+ maxEdgeDensityDelta: 0.03,
6750
+ maxEdgeContrastDrop: 0.03,
6751
+ maxIsolatedPixelIncrease: 5e-3
6752
+ };
6753
+ var QualityToleranceSchema = z6.object({
6754
+ requireExactHash: z6.boolean(),
6755
+ maxColorCountIncrease: z6.number().int().min(0),
6756
+ maxNewColors: z6.number().int().min(0),
6757
+ maxTransparencyDelta: z6.number().min(0).max(1),
6758
+ maxPartialAlphaIncrease: z6.number().min(0).max(1),
6759
+ maxEdgeDensityDelta: z6.number().min(0).max(1),
6760
+ maxEdgeContrastDrop: z6.number().min(0).max(1),
6761
+ maxIsolatedPixelIncrease: z6.number().min(0).max(1)
6762
+ }).strict();
6763
+ var ImageQualityMetricsSchema = z6.object({
6764
+ sha256: z6.string().regex(SHA256_RE2),
6765
+ width: z6.number().int().min(1),
6766
+ height: z6.number().int().min(1),
6767
+ colorCount: z6.number().int().min(0),
6768
+ colors: z6.array(z6.string().regex(HEX_RE)),
6769
+ transparency: z6.number().min(0).max(1),
6770
+ partialAlpha: z6.number().min(0).max(1),
6771
+ edgeDensity: z6.number().min(0).max(1),
6772
+ meanEdgeContrast: z6.number().min(0).max(1),
6773
+ isolatedPixelRatio: z6.number().min(0).max(1)
6774
+ }).strict();
6775
+ var QualityBaselineCaseSchema = z6.object({
6776
+ id: z6.string().regex(/^[a-z0-9][a-z0-9/_-]*$/),
6777
+ file: QualityPathSchema,
6778
+ record: z6.object({
6779
+ path: QualityPathSchema,
6780
+ sha256: z6.string().regex(SHA256_RE2)
6781
+ }).strict().optional(),
6782
+ expected: ImageQualityMetricsSchema,
6783
+ tolerances: QualityToleranceSchema
6784
+ }).strict();
6785
+ var QualityBaselineSchema = z6.object({
6786
+ $schema: z6.string().url().optional(),
6787
+ format: z6.literal("pixelkiln-quality-baseline"),
6788
+ schemaVersion: z6.literal(1),
6789
+ cases: z6.array(QualityBaselineCaseSchema).min(1)
6790
+ }).strict().superRefine((baseline, context) => {
6791
+ const ids = baseline.cases.map((entry) => entry.id);
6792
+ if (new Set(ids).size !== ids.length) {
6793
+ context.addIssue({ code: z6.ZodIssueCode.custom, path: ["cases"], message: "duplicate case id" });
6794
+ }
6795
+ });
6796
+ var QualityInputSchema = z6.object({
6797
+ id: z6.string().regex(/^[a-z0-9][a-z0-9/_-]*$/),
6798
+ path: z6.string().min(1),
6799
+ record: z6.string().min(1).optional(),
6800
+ tolerances: QualityToleranceSchema.partial().optional()
6801
+ }).strict();
6802
+ function message4(error) {
6803
+ return error instanceof Error ? error.message : String(error);
6804
+ }
6805
+ function round(value) {
6806
+ return Number(value.toFixed(6));
6807
+ }
6808
+ function pixelKey(pixels, offset) {
6809
+ const alpha = pixels[offset + 3];
6810
+ if (alpha === 0) return 0;
6811
+ return (pixels[offset] << 24 >>> 0 | pixels[offset + 1] << 16 | pixels[offset + 2] << 8 | alpha) >>> 0;
6812
+ }
6813
+ function edgeContrast(pixels, a, b) {
6814
+ const alphaA = pixels[a + 3];
6815
+ const alphaB = pixels[b + 3];
6816
+ const alphaDistance = Math.abs(alphaA - alphaB) / 255;
6817
+ if (alphaA === 0 || alphaB === 0) return alphaDistance;
6818
+ const rgbDistance = colorDistance(
6819
+ { r: pixels[a], g: pixels[a + 1], b: pixels[a + 2] },
6820
+ { r: pixels[b], g: pixels[b + 1], b: pixels[b + 2] }
6821
+ ) / MAX_REDMEAN_DISTANCE;
6822
+ return Math.min(1, Math.max(alphaDistance, rgbDistance));
6823
+ }
6824
+ function measureDecodedImageQuality(png, digest2) {
6825
+ const colors = /* @__PURE__ */ new Set();
6826
+ let transparent = 0;
6827
+ let partialAlpha = 0;
6828
+ let visible = 0;
6829
+ let isolated = 0;
6830
+ for (let offset = 0; offset < png.pixels.length; offset += 4) {
6831
+ const alpha = png.pixels[offset + 3];
6832
+ if (alpha === 0) {
6833
+ transparent++;
6834
+ continue;
6835
+ }
6836
+ visible++;
6837
+ if (alpha < 255) partialAlpha++;
6838
+ colors.add(png.pixels[offset] << 16 | png.pixels[offset + 1] << 8 | png.pixels[offset + 2]);
6839
+ const index = offset / 4;
6840
+ const x = index % png.width;
6841
+ const y = Math.floor(index / png.width);
6842
+ const key = pixelKey(png.pixels, offset);
6843
+ const hasSameNeighbor = x > 0 && pixelKey(png.pixels, offset - 4) === key || x + 1 < png.width && pixelKey(png.pixels, offset + 4) === key || y > 0 && pixelKey(png.pixels, offset - png.width * 4) === key || y + 1 < png.height && pixelKey(png.pixels, offset + png.width * 4) === key;
6844
+ if (!hasSameNeighbor) {
6845
+ isolated++;
6846
+ }
6847
+ }
6848
+ let neighborPairs = 0;
6849
+ let changedPairs = 0;
6850
+ let contrastTotal = 0;
6851
+ for (let y = 0; y < png.height; y++) {
6852
+ for (let x = 0; x < png.width; x++) {
6853
+ const offset = (y * png.width + x) * 4;
6854
+ if (x + 1 < png.width) {
6855
+ const neighbor = offset + 4;
6856
+ neighborPairs++;
6857
+ if (pixelKey(png.pixels, offset) !== pixelKey(png.pixels, neighbor)) {
6858
+ changedPairs++;
6859
+ contrastTotal += edgeContrast(png.pixels, offset, neighbor);
6860
+ }
6861
+ }
6862
+ if (y + 1 < png.height) {
6863
+ const neighbor = offset + png.width * 4;
6864
+ neighborPairs++;
6865
+ if (pixelKey(png.pixels, offset) !== pixelKey(png.pixels, neighbor)) {
6866
+ changedPairs++;
6867
+ contrastTotal += edgeContrast(png.pixels, offset, neighbor);
6868
+ }
6869
+ }
6870
+ }
6871
+ }
6872
+ const total = png.width * png.height;
6873
+ return {
6874
+ sha256: digest2,
6875
+ width: png.width,
6876
+ height: png.height,
6877
+ colorCount: colors.size,
6878
+ colors: [...colors].sort((a, b) => a - b).map((color) => `#${color.toString(16).padStart(6, "0")}`),
6879
+ transparency: round(total ? transparent / total : 0),
6880
+ partialAlpha: round(total ? partialAlpha / total : 0),
6881
+ edgeDensity: round(neighborPairs ? changedPairs / neighborPairs : 0),
6882
+ meanEdgeContrast: round(changedPairs ? contrastTotal / changedPairs : 0),
6883
+ isolatedPixelRatio: round(visible ? isolated / visible : 0)
6884
+ };
6885
+ }
6886
+ async function measureImageQuality(file) {
6887
+ const absolute = path21.resolve(file);
6888
+ let bytes;
6889
+ try {
6890
+ bytes = await readFile16(absolute);
6891
+ } catch (error) {
6892
+ throw new Error(`Cannot read quality image ${absolute}: ${message4(error)}`, { cause: error });
6893
+ }
6894
+ try {
6895
+ return measureDecodedImageQuality(decodePng(bytes), sha256(bytes));
6896
+ } catch (error) {
6897
+ throw new Error(`Cannot measure quality image ${absolute}: ${message4(error)}`, { cause: error });
6898
+ }
6899
+ }
6900
+ function resolveQualityInputs(raw, inputsFilePath) {
6901
+ const parsed = z6.array(QualityInputSchema).min(1).safeParse(raw);
6902
+ if (!parsed.success) {
6903
+ throw new Error(
6904
+ "--inputs must be a non-empty JSON array of quality cases:\n" + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
6905
+ );
6906
+ }
6907
+ const ids = parsed.data.map((entry) => entry.id);
6908
+ if (new Set(ids).size !== ids.length) throw new Error("--inputs contains a duplicate quality case id.");
6909
+ const root = path21.dirname(path21.resolve(inputsFilePath));
6910
+ return parsed.data.map((entry) => ({
6911
+ id: entry.id,
6912
+ file: path21.resolve(root, entry.path),
6913
+ ...entry.record ? { record: path21.resolve(root, entry.record) } : {},
6914
+ tolerances: QualityToleranceSchema.parse({
6915
+ ...DEFAULT_QUALITY_TOLERANCES,
6916
+ ...entry.tolerances
6917
+ })
6918
+ }));
6919
+ }
6920
+ function portableRelative2(from, to) {
6921
+ return path21.relative(from, path21.resolve(to)).split(path21.sep).join("/") || ".";
6922
+ }
6923
+ async function verifyRecordForImage(recordPath, imagePath) {
6924
+ const verification = await verifyArtifactBundle(recordPath);
6925
+ if (!verification.current) {
6926
+ const changed = [...verification.changedSources, ...verification.changedOutputs];
6927
+ throw new Error(
6928
+ `Quality record is not current: ${recordPath}` + (changed.length ? ` (${changed.join(", ")})` : "")
6929
+ );
6930
+ }
6931
+ const record = await readArtifactBundleManifest(recordPath);
6932
+ const options = record.options;
6933
+ if (record.kind !== "refine" || options === null || typeof options !== "object" || options.schema !== "pixelkiln-quality") {
6934
+ throw new Error(`Quality record ${recordPath} is not a PixelKiln refinement record.`);
6935
+ }
6936
+ const outputs = record.outputs.map((output) => path21.resolve(path21.dirname(recordPath), output.path));
6937
+ if (!outputs.includes(path21.resolve(imagePath))) {
6938
+ throw new Error(`Quality record ${recordPath} does not own ${imagePath}.`);
6939
+ }
6940
+ }
6941
+ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
6942
+ if (!inputs.length) throw new Error("Cannot snapshot an empty quality baseline.");
6943
+ const absolute = path21.resolve(baselinePath);
6944
+ for (const input of inputs) {
6945
+ if (path21.resolve(input.file) === absolute || input.record && path21.resolve(input.record) === absolute) {
6946
+ throw new Error(`Quality baseline output would overwrite an input: ${absolute}.`);
6947
+ }
6948
+ }
6949
+ const root = path21.dirname(absolute);
6950
+ const cases = [];
6951
+ for (const input of [...inputs].sort((a, b) => a.id.localeCompare(b.id))) {
6952
+ const expected = await measureImageQuality(input.file);
6953
+ let record;
6954
+ if (input.record) {
6955
+ await verifyRecordForImage(input.record, input.file);
6956
+ record = {
6957
+ path: portableRelative2(root, input.record),
6958
+ sha256: await sha256File(input.record)
6959
+ };
6960
+ }
6961
+ cases.push({
6962
+ id: input.id,
6963
+ file: portableRelative2(root, input.file),
6964
+ ...record ? { record } : {},
6965
+ expected,
6966
+ tolerances: input.tolerances
6967
+ });
6968
+ }
6969
+ const baseline = QualityBaselineSchema.parse({
6970
+ $schema: "https://unpkg.com/pixelkiln/schema/quality-baseline.schema.json",
6971
+ format: "pixelkiln-quality-baseline",
6972
+ schemaVersion: 1,
6973
+ cases
6974
+ });
6975
+ const data = Buffer.from(JSON.stringify(baseline, null, 2) + "\n");
6976
+ if (existsSync16(absolute) && !options.force) {
6977
+ const current = await readFile16(absolute);
6978
+ if (!current.equals(data)) {
6979
+ throw new Error(`Quality baseline already exists with different content: ${absolute}. Pass --force to replace it.`);
6980
+ }
6981
+ }
6982
+ const result = await writeArtifactBundle([{ path: absolute, data }]);
6983
+ return { baseline, path: absolute, changed: result.changed.length > 0 };
6984
+ }
6985
+ async function readQualityBaseline(baselinePath) {
6986
+ const absolute = path21.resolve(baselinePath);
6987
+ let raw;
6988
+ try {
6989
+ raw = JSON.parse(await readFile16(absolute, "utf8"));
6990
+ } catch (error) {
6991
+ throw new Error(`Could not read quality baseline ${absolute}: ${message4(error)}`, { cause: error });
6992
+ }
6993
+ const parsed = QualityBaselineSchema.safeParse(raw);
6994
+ if (!parsed.success) {
6995
+ throw new Error(
6996
+ `Invalid quality baseline ${absolute}:
6997
+ ` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
6998
+ );
6999
+ }
7000
+ return parsed.data;
7001
+ }
7002
+ function regressionViolations(expected, actual, tolerances) {
7003
+ const violations = [];
7004
+ const warnings = [];
7005
+ const changed = actual.sha256 !== expected.sha256;
7006
+ if (expected.width !== actual.width || expected.height !== actual.height) {
7007
+ violations.push(
7008
+ `dimensions changed from ${expected.width}x${expected.height} to ${actual.width}x${actual.height}`
7009
+ );
7010
+ }
7011
+ if (changed && tolerances.requireExactHash) violations.push("image hash changed");
7012
+ else if (changed) warnings.push("image hash changed; metric tolerances decide this case");
7013
+ const colorIncrease = actual.colorCount - expected.colorCount;
7014
+ if (colorIncrease > tolerances.maxColorCountIncrease) {
7015
+ violations.push(
7016
+ `color count increased by ${colorIncrease} (allowed ${tolerances.maxColorCountIncrease})`
7017
+ );
7018
+ }
7019
+ const expectedColors = new Set(expected.colors);
7020
+ const newColors = actual.colors.filter((color) => !expectedColors.has(color));
7021
+ if (newColors.length > tolerances.maxNewColors) {
7022
+ violations.push(
7023
+ `${newColors.length} new color(s) exceed ${tolerances.maxNewColors}: ${newColors.slice(0, 8).join(", ")}`
7024
+ );
7025
+ }
7026
+ const transparencyDelta = Math.abs(actual.transparency - expected.transparency);
7027
+ if (transparencyDelta > tolerances.maxTransparencyDelta) {
7028
+ violations.push(
7029
+ `transparency changed by ${(transparencyDelta * 100).toFixed(2)} points (allowed ${(tolerances.maxTransparencyDelta * 100).toFixed(2)})`
7030
+ );
7031
+ }
7032
+ const partialAlphaIncrease = actual.partialAlpha - expected.partialAlpha;
7033
+ if (partialAlphaIncrease > tolerances.maxPartialAlphaIncrease) {
7034
+ violations.push(
7035
+ `partial-alpha pixels increased by ${(partialAlphaIncrease * 100).toFixed(2)} points (allowed ${(tolerances.maxPartialAlphaIncrease * 100).toFixed(2)})`
7036
+ );
7037
+ }
7038
+ const edgeDensityDelta = Math.abs(actual.edgeDensity - expected.edgeDensity);
7039
+ if (edgeDensityDelta > tolerances.maxEdgeDensityDelta) {
7040
+ violations.push(
7041
+ `edge density changed by ${(edgeDensityDelta * 100).toFixed(2)} points (allowed ${(tolerances.maxEdgeDensityDelta * 100).toFixed(2)})`
7042
+ );
7043
+ }
7044
+ const edgeContrastDrop = expected.meanEdgeContrast - actual.meanEdgeContrast;
7045
+ if (edgeContrastDrop > tolerances.maxEdgeContrastDrop) {
7046
+ violations.push(
7047
+ `mean edge contrast dropped by ${(edgeContrastDrop * 100).toFixed(2)} points (allowed ${(tolerances.maxEdgeContrastDrop * 100).toFixed(2)})`
7048
+ );
7049
+ }
7050
+ const isolatedIncrease = actual.isolatedPixelRatio - expected.isolatedPixelRatio;
7051
+ if (isolatedIncrease > tolerances.maxIsolatedPixelIncrease) {
7052
+ violations.push(
7053
+ `isolated-pixel ratio increased by ${(isolatedIncrease * 100).toFixed(2)} points (allowed ${(tolerances.maxIsolatedPixelIncrease * 100).toFixed(2)})`
7054
+ );
7055
+ }
7056
+ return { violations, warnings };
7057
+ }
7058
+ async function checkQualityBaseline(baselinePath) {
7059
+ const absolute = path21.resolve(baselinePath);
7060
+ const baseline = await readQualityBaseline(absolute);
7061
+ const root = path21.dirname(absolute);
7062
+ const cases = [];
7063
+ for (const entry of baseline.cases) {
7064
+ const file = path21.resolve(root, entry.file);
7065
+ let actual = null;
7066
+ const violations = [];
7067
+ const warnings = [];
7068
+ try {
7069
+ actual = await measureImageQuality(file);
7070
+ const regression = regressionViolations(entry.expected, actual, entry.tolerances);
7071
+ violations.push(...regression.violations);
7072
+ warnings.push(...regression.warnings);
7073
+ } catch (error) {
7074
+ violations.push(message4(error));
7075
+ }
7076
+ if (entry.record) {
7077
+ const recordPath = path21.resolve(root, entry.record.path);
7078
+ try {
7079
+ const recordHash = await sha256File(recordPath);
7080
+ if (recordHash !== entry.record.sha256) violations.push("quality record hash changed");
7081
+ await verifyRecordForImage(recordPath, file);
7082
+ } catch (error) {
7083
+ violations.push(message4(error));
7084
+ }
7085
+ }
7086
+ cases.push({
7087
+ id: entry.id,
7088
+ file,
7089
+ status: violations.length ? "fail" : "pass",
7090
+ hashChanged: actual !== null && actual.sha256 !== entry.expected.sha256,
7091
+ expected: entry.expected,
7092
+ actual,
7093
+ violations,
7094
+ warnings
7095
+ });
7096
+ }
7097
+ const failed = cases.filter((entry) => entry.status === "fail").length;
7098
+ return {
7099
+ version: 1,
7100
+ safe: failed === 0,
7101
+ baseline: absolute,
7102
+ summary: {
7103
+ total: cases.length,
7104
+ passed: cases.length - failed,
7105
+ failed,
7106
+ changed: cases.filter((entry) => entry.hashChanged).length
7107
+ },
7108
+ cases
7109
+ };
7110
+ }
7111
+
6378
7112
  // src/cli.ts
6379
7113
  var log = (msg = "") => console.log(msg);
6380
7114
  async function provenanceFile(id, file) {
6381
- const absolute = path20.resolve(file);
7115
+ const absolute = path22.resolve(file);
6382
7116
  return {
6383
7117
  id,
6384
7118
  path: absolute,
6385
- sha256: existsSync15(absolute) ? await sha256File(absolute) : null,
7119
+ sha256: existsSync17(absolute) ? await sha256File(absolute) : null,
6386
7120
  included: true
6387
7121
  };
6388
7122
  }
@@ -6415,7 +7149,8 @@ var VALUE_FLAGS = [
6415
7149
  "--fixer-revision",
6416
7150
  "--min-grid-confidence",
6417
7151
  "--reviewer",
6418
- "--note"
7152
+ "--note",
7153
+ "--model-root"
6419
7154
  ];
6420
7155
  var BOOL_FLAGS = [
6421
7156
  "--force",
@@ -6454,7 +7189,9 @@ var COMMANDS = [
6454
7189
  "tag",
6455
7190
  "balance",
6456
7191
  "status",
7192
+ "quality",
6457
7193
  "refine",
7194
+ "recipe",
6458
7195
  "workspace",
6459
7196
  "help",
6460
7197
  "--help",
@@ -6464,6 +7201,8 @@ var COMMANDS = [
6464
7201
  ];
6465
7202
  var WORKSPACE_SUBCOMMANDS = ["add", "remove", "list", "status", "claims"];
6466
7203
  var REFINE_SUBCOMMANDS = ["run", "approve", "check"];
7204
+ var RECIPE_SUBCOMMANDS = ["list", "inspect", "install", "verify"];
7205
+ var QUALITY_SUBCOMMANDS = ["snapshot", "check"];
6467
7206
  function parseArgs(argv) {
6468
7207
  const [command = "help"] = argv;
6469
7208
  if (!COMMANDS.includes(command)) {
@@ -6472,7 +7211,36 @@ function parseArgs(argv) {
6472
7211
  let rest = argv.slice(1);
6473
7212
  let subcommand;
6474
7213
  let target;
6475
- if (command === "workspace") {
7214
+ if (command === "quality") {
7215
+ subcommand = rest[0];
7216
+ if (subcommand === void 0 || subcommand.startsWith("-")) {
7217
+ throw new Error(`quality needs a subcommand: ${QUALITY_SUBCOMMANDS.join(", ")}`);
7218
+ }
7219
+ if (!QUALITY_SUBCOMMANDS.includes(subcommand)) {
7220
+ throw new Error(
7221
+ `Unknown quality subcommand "${subcommand}". Known: ${QUALITY_SUBCOMMANDS.join(", ")}`
7222
+ );
7223
+ }
7224
+ rest = rest.slice(1);
7225
+ } else if (command === "recipe") {
7226
+ subcommand = rest[0];
7227
+ if (subcommand === void 0 || subcommand.startsWith("-")) {
7228
+ throw new Error(`recipe needs a subcommand: ${RECIPE_SUBCOMMANDS.join(", ")}`);
7229
+ }
7230
+ if (!RECIPE_SUBCOMMANDS.includes(subcommand)) {
7231
+ throw new Error(
7232
+ `Unknown recipe subcommand "${subcommand}". Known: ${RECIPE_SUBCOMMANDS.join(", ")}`
7233
+ );
7234
+ }
7235
+ rest = rest.slice(1);
7236
+ if (subcommand !== "list") {
7237
+ target = rest[0];
7238
+ if (target === void 0 || target.startsWith("-")) {
7239
+ throw new Error(`recipe ${subcommand} needs a recipe id, selector, or path.`);
7240
+ }
7241
+ rest = rest.slice(1);
7242
+ }
7243
+ } else if (command === "workspace") {
6476
7244
  subcommand = rest[0];
6477
7245
  if (subcommand === void 0 || subcommand.startsWith("-")) {
6478
7246
  throw new Error(`workspace needs a subcommand: ${WORKSPACE_SUBCOMMANDS.join(", ")}`);
@@ -6583,7 +7351,7 @@ function parseArgs(argv) {
6583
7351
  return {
6584
7352
  command,
6585
7353
  manifest,
6586
- lock: get("--lock") ?? path20.join(path20.dirname(path20.resolve(manifest)), "pixelkiln.lock.json"),
7354
+ lock: get("--lock") ?? path22.join(path22.dirname(path22.resolve(manifest)), "pixelkiln.lock.json"),
6587
7355
  explicitLock: get("--lock"),
6588
7356
  styles: list("--style"),
6589
7357
  assets: list("--only"),
@@ -6620,6 +7388,7 @@ function parseArgs(argv) {
6620
7388
  minGridConfidence: rawGridConfidence,
6621
7389
  reviewer: get("--reviewer"),
6622
7390
  note: get("--note"),
7391
+ modelRoot: get("--model-root"),
6623
7392
  subcommand,
6624
7393
  workspace: get("--workspace"),
6625
7394
  target,
@@ -6648,6 +7417,8 @@ Commands
6648
7417
  audit Measure how consistently a style's assets hold together. Offline.
6649
7418
  refine Recover a native pixel grid, enforce a final palette, audit it,
6650
7419
  and attach a verifiable human approval. run/approve/check. Offline.
7420
+ recipe List, inspect, install, or verify versioned workflow packs. Offline.
7421
+ quality Snapshot or check measurable image regressions. Offline.
6651
7422
  cache Inspect local recovery/object-hash caches; optionally verify or prune.
6652
7423
  pack Composite sprites into a PNG/atlas/provenance bundle. Offline.
6653
7424
  mount Write a style's sprites into their declared cells of an existing
@@ -6665,7 +7436,7 @@ Commands
6665
7436
  Options
6666
7437
  --columns <n> pack/export: sprites or tiles per row (default: near-square)
6667
7438
  --port <n> Local review-server port (default: choose a free port)
6668
- --inputs <path> pack: JSON [{id,path}] instead of the lockfile; needs --out
7439
+ --inputs <path> pack/quality snapshot: JSON input list; needs --out
6669
7440
  --format <format> export: generic (default), tiled, or godot
6670
7441
  --output-role <r> pack: include only this output role (repeatable)
6671
7442
  --primary-only pack: include only unambiguous primary/single outputs
@@ -6679,16 +7450,17 @@ Options
6679
7450
  --min-grid-confidence <level> refine: high (default), medium, or low
6680
7451
  --reviewer <name> refine approve: human reviewer recorded in provenance
6681
7452
  --note <text> refine approve: optional review note
7453
+ --model-root <dir> recipe verify: also hash required local model files
6682
7454
  --prune cache: remove invalid/unreferenced local cache data
6683
7455
  --manifest <path> Default: pixelkiln.manifest.json
6684
7456
  --lock <path> Default: pixelkiln.lock.json beside the manifest
6685
7457
  --style a,b Restrict to these styles
6686
7458
  --only id1,id2 Restrict to these asset ids
6687
7459
  --budget <n> Refuse to spend more than n provider cost units
6688
- --force Regenerate; derived commands also take over modified/unowned output
7460
+ --force Regenerate; also replace changed recipe files or quality baselines
6689
7461
  --dry-run Never spend; doctor also skips provider connectivity
6690
7462
  --all salvage --dry-run: list every unclaimed object, not just the first 30
6691
- --json Machine-readable output where supported, including plan/audit/doctor/cache
7463
+ --json Machine-readable output where supported, including quality checks
6692
7464
  --check plan/audit/cache: exit nonzero when the selected state is unsafe
6693
7465
  --yes, -y Skip the confirmation prompt
6694
7466
  --no-open Do not auto-open the browser during pick
@@ -6696,7 +7468,7 @@ Options
6696
7468
  --claims a.json,b Other projects' lockfiles (salvage; required if account is shared)
6697
7469
  --workspace <path> Workspace catalog (default: pixelkiln.workspace.json). Also
6698
7470
  derives salvage's claim set instead of repeated --claims.
6699
- --from <dir> Source tree for init
7471
+ --from <path> Source for init/refine, or baseline for quality check
6700
7472
  --write-prompts adopt: recover prompts into the manifest
6701
7473
 
6702
7474
  Examples
@@ -6711,6 +7483,11 @@ Examples
6711
7483
  pixelkiln refine --from source.png --out final.png --palette "#101820,#f2aa4c"
6712
7484
  pixelkiln refine approve --from final.pixelkiln.json --reviewer "Your Name"
6713
7485
  pixelkiln refine check --from final.pixelkiln.json
7486
+ pixelkiln recipe list
7487
+ pixelkiln recipe install comfyui/pixel-art-xl-environment
7488
+ pixelkiln recipe verify pixelkiln-recipes/comfyui/pixel-art-xl-environment/1.0.0 --model-root /path/to/ComfyUI/models
7489
+ pixelkiln quality snapshot --inputs quality-inputs.json --out pixelkiln.quality.json
7490
+ pixelkiln quality check --from pixelkiln.quality.json
6714
7491
  pixelkiln workspace add ../other-game/pixelkiln.manifest.json
6715
7492
  pixelkiln workspace status --json
6716
7493
  pixelkiln salvage --workspace pixelkiln.workspace.json
@@ -6755,10 +7532,10 @@ async function confirm(question, auto) {
6755
7532
  return answer === "y" || answer === "yes";
6756
7533
  }
6757
7534
  async function requireCompleteWorkspaceClaims(workspacePath) {
6758
- if (!existsSync15(workspacePath)) {
7535
+ if (!existsSync17(workspacePath)) {
6759
7536
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
6760
7537
  }
6761
- const dir = path20.dirname(path20.resolve(workspacePath));
7538
+ const dir = path22.dirname(path22.resolve(workspacePath));
6762
7539
  const ws = await loadWorkspace(workspacePath);
6763
7540
  const diagnostics = validateWorkspace(ws, dir);
6764
7541
  const errors = diagnostics.filter((d) => d.level === "error");
@@ -6779,11 +7556,146 @@ async function main() {
6779
7556
  }
6780
7557
  if (args.command === "--version" || args.command === "-v") {
6781
7558
  const pkg = JSON.parse(
6782
- await readFile16(new URL("../package.json", import.meta.url), "utf8")
7559
+ await readFile17(new URL("../package.json", import.meta.url), "utf8")
6783
7560
  );
6784
7561
  log(`${pkg.name} ${pkg.version}`);
6785
7562
  return;
6786
7563
  }
7564
+ if (args.command === "quality") {
7565
+ if (args.subcommand === "snapshot") {
7566
+ if (!args.inputs) throw new Error("quality snapshot needs --inputs <quality-inputs.json>.");
7567
+ if (!args.out) throw new Error("quality snapshot needs --out <pixelkiln.quality.json>.");
7568
+ let raw;
7569
+ try {
7570
+ raw = JSON.parse(await readFile17(path22.resolve(args.inputs), "utf8"));
7571
+ } catch (error) {
7572
+ throw new Error(
7573
+ `Could not read quality inputs ${path22.resolve(args.inputs)}: ${error instanceof Error ? error.message : String(error)}`,
7574
+ { cause: error }
7575
+ );
7576
+ }
7577
+ const inputs = resolveQualityInputs(raw, args.inputs);
7578
+ if (path22.resolve(args.inputs) === path22.resolve(args.out)) {
7579
+ throw new Error("quality snapshot --out must not overwrite its --inputs file.");
7580
+ }
7581
+ const result = await snapshotQualityBaseline(inputs, args.out, { force: args.force });
7582
+ if (args.json) {
7583
+ log(JSON.stringify({
7584
+ version: 1,
7585
+ path: result.path,
7586
+ changed: result.changed,
7587
+ baseline: result.baseline
7588
+ }, null, 2));
7589
+ } else {
7590
+ log(
7591
+ ` ${result.changed ? "wrote" : "unchanged"} ${path22.relative(process.cwd(), result.path)} (${result.baseline.cases.length} case(s))`
7592
+ );
7593
+ log(` Review the tolerances, commit the baseline, then run quality check in CI.`);
7594
+ }
7595
+ return;
7596
+ }
7597
+ if (!args.from) throw new Error("quality check needs --from <pixelkiln.quality.json>.");
7598
+ const report = await checkQualityBaseline(args.from);
7599
+ if (args.json) {
7600
+ log(JSON.stringify(report, null, 2));
7601
+ } else {
7602
+ for (const entry of report.cases) {
7603
+ log(` ${entry.status === "pass" ? "ok" : "ERROR"} ${entry.id}`);
7604
+ for (const violation of entry.violations) log(` ${violation}`);
7605
+ for (const warning of entry.warnings) log(` WARN ${warning}`);
7606
+ }
7607
+ log(
7608
+ `
7609
+ ${report.summary.passed}/${report.summary.total} passed` + (report.summary.changed ? ` \xB7 ${report.summary.changed} image hash(es) changed` : "")
7610
+ );
7611
+ log(` Metrics catch structural regressions; they do not replace art review.`);
7612
+ }
7613
+ if (!report.safe) process.exitCode = 1;
7614
+ return;
7615
+ }
7616
+ if (args.command === "recipe") {
7617
+ if (args.subcommand === "list") {
7618
+ const recipes = await listBundledRecipes();
7619
+ if (args.json) {
7620
+ log(JSON.stringify({
7621
+ version: 1,
7622
+ recipes: recipes.map(({ recipe }) => ({
7623
+ id: recipe.id,
7624
+ version: recipe.version,
7625
+ provider: recipe.provider,
7626
+ summary: recipe.summary,
7627
+ selector: `${recipe.id}@${recipe.version}`
7628
+ }))
7629
+ }, null, 2));
7630
+ } else {
7631
+ log(` ${recipes.length} bundled recipe(s):`);
7632
+ for (const { recipe } of recipes) {
7633
+ log(` ${(recipe.id + "@" + recipe.version).padEnd(52)} ${recipe.summary}`);
7634
+ }
7635
+ }
7636
+ return;
7637
+ }
7638
+ const target = args.target;
7639
+ if (args.subcommand === "inspect") {
7640
+ const { recipe, path: recipePath, bundled } = await resolveRecipe(target);
7641
+ if (args.json) {
7642
+ log(JSON.stringify({ ...recipe, path: recipePath, bundled }, null, 2));
7643
+ } else {
7644
+ log(` ${recipe.id}@${recipe.version}`);
7645
+ log(` ${recipe.summary}`);
7646
+ log(` provider: ${recipe.provider} \xB7 style: ${recipe.styleId} \xB7 stage: ${recipe.quality.stage}`);
7647
+ log(
7648
+ ` native target: ${recipe.quality.recommendedNativeSize.min}\u2013${recipe.quality.recommendedNativeSize.max}px \xB7 palette: ${recipe.quality.paletteColors.min}\u2013${recipe.quality.paletteColors.max} colors`
7649
+ );
7650
+ if (recipe.workflow) log(` workflow: ${recipe.workflow.path} \xB7 ${recipe.workflow.numImages} candidate(s)`);
7651
+ for (const model of recipe.models) log(` model: ${model.path} \xB7 ${model.license}`);
7652
+ log(` source: ${recipePath}${bundled ? " (bundled)" : ""}`);
7653
+ }
7654
+ return;
7655
+ }
7656
+ if (args.subcommand === "verify") {
7657
+ const report = await verifyRecipe(target, { modelRoot: args.modelRoot });
7658
+ if (args.json) {
7659
+ log(JSON.stringify(report, null, 2));
7660
+ } else {
7661
+ log(` ${report.ok ? "ok" : "ERROR"} ${report.recipe.id}@${report.recipe.version}`);
7662
+ log(` ${report.integrity.status.padEnd(9)} recipe metadata`);
7663
+ for (const file of report.files) log(` ${file.status.padEnd(9)} ${file.path}`);
7664
+ for (const model of report.models) log(` ${model.status.padEnd(9)} ${model.path}`);
7665
+ if (!report.modelRoot && report.models.length) {
7666
+ log(` models were not checked; pass --model-root <ComfyUI/models> to verify this workstation`);
7667
+ }
7668
+ }
7669
+ if (!report.ok) process.exitCode = 1;
7670
+ return;
7671
+ }
7672
+ if (args.subcommand === "install") {
7673
+ const result = await installRecipe(target, { out: args.out, force: args.force });
7674
+ if (args.json) {
7675
+ log(JSON.stringify({
7676
+ version: 1,
7677
+ recipe: `${result.recipe.id}@${result.recipe.version}`,
7678
+ destination: result.destination,
7679
+ changed: result.changed,
7680
+ unchanged: result.unchanged,
7681
+ styleId: result.styleId,
7682
+ style: result.style
7683
+ }, null, 2));
7684
+ } else {
7685
+ log(` installed ${result.recipe.id}@${result.recipe.version}`);
7686
+ log(` ${path22.relative(process.cwd(), result.destination) || "."}`);
7687
+ log(`
7688
+ Add this entry under your manifest's styles object:`);
7689
+ log(JSON.stringify({ [result.styleId]: result.style }, null, 2));
7690
+ if (result.recipe.models.length) {
7691
+ log(`
7692
+ Models are not downloaded automatically. Verify them with:`);
7693
+ log(` pixelkiln recipe verify ${path22.relative(process.cwd(), result.destination)} --model-root <ComfyUI/models>`);
7694
+ }
7695
+ }
7696
+ return;
7697
+ }
7698
+ }
6787
7699
  if (args.command === "refine") {
6788
7700
  if (!args.from) {
6789
7701
  throw new Error(
@@ -6812,12 +7724,12 @@ async function main() {
6812
7724
  ` recovered ${result2.detection.columns}x${result2.detection.rows} native grid (${result2.detection.confidence}: ${result2.detection.consensus})`
6813
7725
  );
6814
7726
  log(` enforced ${result2.palette.length}-color palette without dithering`);
6815
- log(` wrote ${path20.relative(process.cwd(), result2.output)}`);
6816
- log(` quality record: ${path20.relative(process.cwd(), result2.record)}`);
7727
+ log(` wrote ${path22.relative(process.cwd(), result2.output)}`);
7728
+ log(` quality record: ${path22.relative(process.cwd(), result2.record)}`);
6817
7729
  log(`
6818
7730
  Automated checks passed. Human 1\xD7 review is still required:`);
6819
7731
  log(
6820
- ` pixelkiln refine approve --from ${path20.relative(process.cwd(), result2.record)} --reviewer "Your Name"`
7732
+ ` pixelkiln refine approve --from ${path22.relative(process.cwd(), result2.record)} --reviewer "Your Name"`
6821
7733
  );
6822
7734
  }
6823
7735
  return;
@@ -6837,21 +7749,21 @@ async function main() {
6837
7749
  note: args.note
6838
7750
  });
6839
7751
  if (args.json) log(JSON.stringify(result2, null, 2));
6840
- else log(` approved ${path20.relative(process.cwd(), result2.output)} by ${args.reviewer.trim()}`);
7752
+ else log(` approved ${path22.relative(process.cwd(), result2.output)} by ${args.reviewer.trim()}`);
6841
7753
  return;
6842
7754
  }
6843
7755
  const result = await checkQualityRecord(args.from);
6844
7756
  if (args.json) {
6845
7757
  log(JSON.stringify(result, null, 2));
6846
7758
  } else {
6847
- log(` ${result.safe ? "release-ready" : "not release-ready"}: ${path20.relative(process.cwd(), result.output || result.record)}`);
7759
+ log(` ${result.safe ? "release-ready" : "not release-ready"}: ${path22.relative(process.cwd(), result.output || result.record)}`);
6848
7760
  for (const reason of result.reasons) log(` ${reason}`);
6849
7761
  }
6850
7762
  if (!result.safe) process.exitCode = 1;
6851
7763
  return;
6852
7764
  }
6853
7765
  if (args.command === "balance") {
6854
- loadEnvFiles(path20.dirname(path20.resolve(args.manifest)));
7766
+ loadEnvFiles(path22.dirname(path22.resolve(args.manifest)));
6855
7767
  loadEnvFiles(process.cwd());
6856
7768
  const loaded2 = await loadManifest(args.manifest);
6857
7769
  const p = createProvider(loaded2.manifest.provider, "online");
@@ -6863,31 +7775,31 @@ async function main() {
6863
7775
  }
6864
7776
  if (args.command === "init") {
6865
7777
  if (!args.from) throw new Error("init needs --from <dir> pointing at your existing PNGs.");
6866
- const root = path20.resolve(args.from);
6867
- if (!existsSync15(root)) throw new Error(`No directory at ${root}`);
7778
+ const root = path22.resolve(args.from);
7779
+ if (!existsSync17(root)) throw new Error(`No directory at ${root}`);
6868
7780
  const generator = args.generator ?? "map";
6869
7781
  if (generator !== "1dir" && generator !== "map") {
6870
7782
  throw new Error(`--generator must be "1dir" or "map", got "${args.generator}".`);
6871
7783
  }
6872
- const target = path20.resolve(args.out ?? "pixelkiln.manifest.json");
7784
+ const target = path22.resolve(args.out ?? "pixelkiln.manifest.json");
6873
7785
  const { assets, skipped } = await scanAssets(root, { exclude: args.exclude });
6874
7786
  if (!assets.length) throw new Error(`No PNGs found under ${root}`);
6875
7787
  const manifest = buildManifest(
6876
- args.name ?? path20.basename(path20.dirname(target)),
7788
+ args.name ?? path22.basename(path22.dirname(target)),
6877
7789
  args.styles[0] ?? "base",
6878
7790
  generator,
6879
- path20.relative(path20.dirname(target), root) || ".",
7791
+ path22.relative(path22.dirname(target), root) || ".",
6880
7792
  assets
6881
7793
  );
6882
7794
  await writeManifestFile(target, manifest);
6883
- log(` scanned ${assets.length} PNG(s) under ${path20.relative(process.cwd(), root)}`);
7795
+ log(` scanned ${assets.length} PNG(s) under ${path22.relative(process.cwd(), root)}`);
6884
7796
  if (skipped.length) log(` skipped ${skipped.length} unreadable file(s)`);
6885
- log(` wrote ${path20.relative(process.cwd(), target)}`);
7797
+ log(` wrote ${path22.relative(process.cwd(), target)}`);
6886
7798
  log(`
6887
7799
  Prompts are intentionally empty. To recover the real ones from your`);
6888
7800
  log(` PixelLab account instead of inventing them:`);
6889
7801
  log(`
6890
- pixelkiln adopt --manifest ${path20.relative(process.cwd(), target)} --write-prompts
7802
+ pixelkiln adopt --manifest ${path22.relative(process.cwd(), target)} --write-prompts
6891
7803
  `);
6892
7804
  return;
6893
7805
  }
@@ -6935,10 +7847,10 @@ async function main() {
6935
7847
  if (args.primaryOnly || args.outputRoles.length) {
6936
7848
  throw new Error("--primary-only and --output-role require manifest-driven pack");
6937
7849
  }
6938
- const raw = JSON.parse(await readFile16(path20.resolve(args.inputs), "utf8"));
7850
+ const raw = JSON.parse(await readFile17(path22.resolve(args.inputs), "utf8"));
6939
7851
  const inputs = resolvePackInputs(raw, args.inputs);
6940
7852
  const { png, atlas, skipped, sources } = packSprites(inputs, { columns: args.columns });
6941
- const base = path20.resolve(args.out.replace(/\.png$/, ""));
7853
+ const base = path22.resolve(args.out.replace(/\.png$/, ""));
6942
7854
  const outputs = [
6943
7855
  { path: `${base}.png`, data: png },
6944
7856
  { path: `${base}.json`, data: JSON.stringify(atlas, null, 2) + "\n" }
@@ -6951,17 +7863,17 @@ async function main() {
6951
7863
  log(
6952
7864
  ` ${atlas.frames.length} sprite(s), ${atlas.sheet.width}x${atlas.sheet.height} in ${atlas.columns} column(s) \u2014 ${(png.length / 1024).toFixed(1)} KB`
6953
7865
  );
6954
- log(` ${path20.relative(process.cwd(), base)}.png + .json + .pixelkiln.json`);
7866
+ log(` ${path22.relative(process.cwd(), base)}.png + .json + .pixelkiln.json`);
6955
7867
  for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
6956
7868
  return;
6957
7869
  }
6958
7870
  if (args.command === "workspace") {
6959
- const workspacePath = path20.resolve(args.workspace ?? "pixelkiln.workspace.json");
6960
- const dir = path20.dirname(workspacePath);
7871
+ const workspacePath = path22.resolve(args.workspace ?? "pixelkiln.workspace.json");
7872
+ const dir = path22.dirname(workspacePath);
6961
7873
  if (args.subcommand === "add") {
6962
- const manifestPath = path20.resolve(args.target);
7874
+ const manifestPath = path22.resolve(args.target);
6963
7875
  const loadedTarget = await loadManifest(manifestPath);
6964
- const lockPath = args.explicitLock ? path20.resolve(args.explicitLock) : path20.join(path20.dirname(manifestPath), "pixelkiln.lock.json");
7876
+ const lockPath = args.explicitLock ? path22.resolve(args.explicitLock) : path22.join(path22.dirname(manifestPath), "pixelkiln.lock.json");
6965
7877
  const ws = await loadWorkspace(workspacePath);
6966
7878
  const id = args.name ?? loadedTarget.manifest.name;
6967
7879
  if (ws.projects.some((p) => p.id === id)) {
@@ -6981,10 +7893,10 @@ async function main() {
6981
7893
  ...args.account ? { account: args.account } : {}
6982
7894
  };
6983
7895
  await saveWorkspace(workspacePath, { version: 1, projects: [...ws.projects, project] });
6984
- log(` registered "${id}" in ${path20.relative(process.cwd(), workspacePath)}`);
7896
+ log(` registered "${id}" in ${path22.relative(process.cwd(), workspacePath)}`);
6985
7897
  log(` manifest: ${project.manifest}`);
6986
7898
  log(` lock: ${project.lock}`);
6987
- if (!existsSync15(lockPath)) {
7899
+ if (!existsSync17(lockPath)) {
6988
7900
  log(
6989
7901
  ` warning: no lockfile there yet \u2014 this project contributes no claims until one is generated`
6990
7902
  );
@@ -6993,7 +7905,7 @@ async function main() {
6993
7905
  }
6994
7906
  if (args.subcommand === "remove") {
6995
7907
  const ws = await loadWorkspace(workspacePath);
6996
- const resolvedTarget = path20.resolve(args.target);
7908
+ const resolvedTarget = path22.resolve(args.target);
6997
7909
  const match = ws.projects.find(
6998
7910
  (p) => p.id === args.target || resolveProject(dir, p).manifestPath === resolvedTarget
6999
7911
  );
@@ -7004,11 +7916,11 @@ async function main() {
7004
7916
  version: 1,
7005
7917
  projects: ws.projects.filter((p) => p !== match)
7006
7918
  });
7007
- log(` removed "${match.id}" from ${path20.relative(process.cwd(), workspacePath)}`);
7919
+ log(` removed "${match.id}" from ${path22.relative(process.cwd(), workspacePath)}`);
7008
7920
  return;
7009
7921
  }
7010
7922
  if (args.subcommand === "list") {
7011
- if (!existsSync15(workspacePath)) {
7923
+ if (!existsSync17(workspacePath)) {
7012
7924
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
7013
7925
  }
7014
7926
  const ws = await loadWorkspace(workspacePath);
@@ -7016,9 +7928,9 @@ async function main() {
7016
7928
  if (args.json) {
7017
7929
  log(JSON.stringify({ version: 1, workspace: workspacePath, projects: ws.projects, diagnostics }, null, 2));
7018
7930
  } else if (!ws.projects.length) {
7019
- log(` no projects registered in ${path20.relative(process.cwd(), workspacePath)}`);
7931
+ log(` no projects registered in ${path22.relative(process.cwd(), workspacePath)}`);
7020
7932
  } else {
7021
- log(` ${ws.projects.length} project(s) in ${path20.relative(process.cwd(), workspacePath)}:`);
7933
+ log(` ${ws.projects.length} project(s) in ${path22.relative(process.cwd(), workspacePath)}:`);
7022
7934
  for (const p of ws.projects) {
7023
7935
  log(` ${p.id.padEnd(24)} ${p.manifest.padEnd(40)} (${p.provider}${p.account ? `, ${p.account}` : ""})`);
7024
7936
  }
@@ -7028,7 +7940,7 @@ async function main() {
7028
7940
  return;
7029
7941
  }
7030
7942
  if (args.subcommand === "status") {
7031
- if (!existsSync15(workspacePath)) {
7943
+ if (!existsSync17(workspacePath)) {
7032
7944
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
7033
7945
  }
7034
7946
  const ws = await loadWorkspace(workspacePath);
@@ -7036,7 +7948,7 @@ async function main() {
7036
7948
  if (args.json) {
7037
7949
  log(JSON.stringify({ ...report, workspace: workspacePath }, null, 2));
7038
7950
  } else {
7039
- log(` workspace: ${path20.relative(process.cwd(), workspacePath)}`);
7951
+ log(` workspace: ${path22.relative(process.cwd(), workspacePath)}`);
7040
7952
  for (const p of report.projects) {
7041
7953
  if (p.error) {
7042
7954
  log(`
@@ -7080,14 +7992,14 @@ async function main() {
7080
7992
  return;
7081
7993
  }
7082
7994
  }
7083
- if (!existsSync15(path20.resolve(args.manifest))) {
7995
+ if (!existsSync17(path22.resolve(args.manifest))) {
7084
7996
  throw new Error(
7085
- `No manifest at ${path20.resolve(args.manifest)}. Pass --manifest, or run \`pixelkiln init --from <dir>\`.`
7997
+ `No manifest at ${path22.resolve(args.manifest)}. Pass --manifest, or run \`pixelkiln init --from <dir>\`.`
7086
7998
  );
7087
7999
  }
7088
- const manifestDir = path20.dirname(path20.resolve(args.manifest));
8000
+ const manifestDir = path22.dirname(path22.resolve(args.manifest));
7089
8001
  const envFiles = [...loadEnvFiles(manifestDir)];
7090
- if (path20.resolve(process.cwd()) !== manifestDir) envFiles.push(...loadEnvFiles(process.cwd()));
8002
+ if (path22.resolve(process.cwd()) !== manifestDir) envFiles.push(...loadEnvFiles(process.cwd()));
7091
8003
  const loaded = await loadManifest(args.manifest);
7092
8004
  const estimator = createProvider(loaded.manifest.provider, "offline");
7093
8005
  const specs = await resolveSpecs(loaded, {
@@ -7164,7 +8076,7 @@ async function main() {
7164
8076
  let intact = true;
7165
8077
  for (const output of entry.outputs) {
7166
8078
  const file = resolveOutputPath(output.path, item.spec.root);
7167
- if (!existsSync15(file) || await sha256File(file) !== output.sha256) {
8079
+ if (!existsSync17(file) || await sha256File(file) !== output.sha256) {
7168
8080
  intact = false;
7169
8081
  break;
7170
8082
  }
@@ -7222,7 +8134,7 @@ async function main() {
7222
8134
  if (args.primaryOnly && args.outputRoles.length) {
7223
8135
  throw new Error("pack accepts either --primary-only or --output-role, not both");
7224
8136
  }
7225
- const manifestDir2 = path20.dirname(path20.resolve(args.manifest));
8137
+ const manifestDir2 = path22.dirname(path22.resolve(args.manifest));
7226
8138
  const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
7227
8139
  for (const styleId of styleIds) {
7228
8140
  const { png, atlas, skipped, sources } = packStyle(lock, styleId, manifestDir2, {
@@ -7231,7 +8143,7 @@ async function main() {
7231
8143
  primaryOnly: args.primaryOnly
7232
8144
  });
7233
8145
  const style = loaded.manifest.styles[styleId];
7234
- const base = args.out ? path20.resolve(args.out.replace(/\.png$/, "")) : path20.resolve(manifestDir2, style.outDir, `${styleId}-sheet`);
8146
+ const base = args.out ? path22.resolve(args.out.replace(/\.png$/, "")) : path22.resolve(manifestDir2, style.outDir, `${styleId}-sheet`);
7235
8147
  const outputs = [
7236
8148
  { path: `${base}.png`, data: png },
7237
8149
  { path: `${base}.json`, data: JSON.stringify(atlas, null, 2) + "\n" }
@@ -7254,13 +8166,13 @@ async function main() {
7254
8166
  log(
7255
8167
  ` ${styleId} \u2014 ${atlas.frames.length} sprite(s), ${atlas.sheet.width}x${atlas.sheet.height} in ${atlas.columns} column(s)`
7256
8168
  );
7257
- log(` ${path20.relative(process.cwd(), base)}.png + .json + .pixelkiln.json`);
8169
+ log(` ${path22.relative(process.cwd(), base)}.png + .json + .pixelkiln.json`);
7258
8170
  for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
7259
8171
  }
7260
8172
  return;
7261
8173
  }
7262
8174
  if (args.command === "mount") {
7263
- const manifestDir2 = path20.dirname(path20.resolve(args.manifest));
8175
+ const manifestDir2 = path22.dirname(path22.resolve(args.manifest));
7264
8176
  const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
7265
8177
  for (const styleId of styleIds) {
7266
8178
  const style = loaded.manifest.styles[styleId];
@@ -7290,7 +8202,7 @@ async function main() {
7290
8202
  sources,
7291
8203
  outputRoles
7292
8204
  );
7293
- const out = path20.resolve(manifestDir2, style.mount.out);
8205
+ const out = path22.resolve(manifestDir2, style.mount.out);
7294
8206
  const metadata = out.replace(/\.png$/, "") + ".json";
7295
8207
  const companion = out.replace(/\.png$/, "") + ".pixelkiln.json";
7296
8208
  const outputs = [
@@ -7303,7 +8215,7 @@ async function main() {
7303
8215
  await provenanceFile("$manifest", args.manifest),
7304
8216
  await provenanceFile("$lock", args.lock),
7305
8217
  ...artifactSources.filter(
7306
- (source) => source.id !== "$base" || path20.resolve(source.path) !== out
8218
+ (source) => source.id !== "$base" || path22.resolve(source.path) !== out
7307
8219
  )
7308
8220
  ],
7309
8221
  options: {
@@ -7316,7 +8228,7 @@ async function main() {
7316
8228
  log(
7317
8229
  ` ${styleId} \u2014 ${atlas.frames.length} cell(s) into ${atlas.sheet.width}x${atlas.sheet.height}` + (overBase ? ` over ${style.mount.base}` : " (new sheet)")
7318
8230
  );
7319
- log(` ${path20.relative(process.cwd(), out)} + atlas/provenance JSON`);
8231
+ log(` ${path22.relative(process.cwd(), out)} + atlas/provenance JSON`);
7320
8232
  for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
7321
8233
  }
7322
8234
  return;
@@ -7372,7 +8284,7 @@ async function main() {
7372
8284
  }
7373
8285
  if (args.command === "export") {
7374
8286
  const format = args.format ?? "generic";
7375
- const manifestDir2 = path20.dirname(path20.resolve(args.manifest));
8287
+ const manifestDir2 = path22.dirname(path22.resolve(args.manifest));
7376
8288
  const selected = specs.filter((spec) => {
7377
8289
  if (spec.generator !== "tiles") return false;
7378
8290
  if (args.styles.length && !args.styles.includes(spec.styleId)) return false;
@@ -7388,12 +8300,12 @@ async function main() {
7388
8300
  for (const spec of selected) {
7389
8301
  const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
7390
8302
  const style = loaded.manifest.styles[spec.styleId];
7391
- const defaultBase = path20.resolve(manifestDir2, style.outDir, `${spec.assetId}-tileset`);
7392
- const base = args.out ? path20.resolve(args.out.replace(/\.(?:png|json|tsj|tres)$/i, "")) : defaultBase;
8303
+ const defaultBase = path22.resolve(manifestDir2, style.outDir, `${spec.assetId}-tileset`);
8304
+ const base = args.out ? path22.resolve(args.out.replace(/\.(?:png|json|tsj|tres)$/i, "")) : defaultBase;
7393
8305
  const result = exportTileset(entry, spec, {
7394
8306
  format,
7395
8307
  manifestDir: manifestDir2,
7396
- imageName: path20.basename(`${base}.png`),
8308
+ imageName: path22.basename(`${base}.png`),
7397
8309
  columns: args.columns
7398
8310
  });
7399
8311
  const outputs = [
@@ -7411,7 +8323,7 @@ async function main() {
7411
8323
  asset: spec.assetId,
7412
8324
  columns: args.columns ?? null,
7413
8325
  format,
7414
- image: path20.basename(`${base}.png`),
8326
+ image: path22.basename(`${base}.png`),
7415
8327
  providerRules: result.generic.providerRules,
7416
8328
  style: spec.styleId,
7417
8329
  tileType: spec.tileType ?? null
@@ -7421,7 +8333,7 @@ async function main() {
7421
8333
  ` ${spec.styleId}/${spec.assetId} \u2014 ${result.generic.tiles.length} tile(s), ${result.generic.sheet.width}x${result.generic.sheet.height} (${format})`
7422
8334
  );
7423
8335
  log(
7424
- ` ${path20.relative(process.cwd(), base)}.png + ${path20.basename(base)}${result.extension} + .pixelkiln.json`
8336
+ ` ${path22.relative(process.cwd(), base)}.png + ${path22.basename(base)}${result.extension} + .pixelkiln.json`
7425
8337
  );
7426
8338
  }
7427
8339
  return;
@@ -7461,10 +8373,10 @@ async function main() {
7461
8373
  tagged ${n} object(s) upstream`);
7462
8374
  }
7463
8375
  if (args.writePrompts) {
7464
- const { filled, stillEmpty } = await writePromptsBack(path20.resolve(args.manifest), lock, {
8376
+ const { filled, stillEmpty } = await writePromptsBack(path22.resolve(args.manifest), lock, {
7465
8377
  onProgress: log
7466
8378
  });
7467
- log(` recovered ${filled} prompt(s) into ${path20.relative(process.cwd(), args.manifest)}`);
8379
+ log(` recovered ${filled} prompt(s) into ${path22.relative(process.cwd(), args.manifest)}`);
7468
8380
  const reloaded = await loadManifest(args.manifest);
7469
8381
  const rebased = await resolveSpecs(reloaded, {
7470
8382
  styles: args.styles,
@@ -7493,12 +8405,12 @@ async function main() {
7493
8405
  if (args.command === "salvage") {
7494
8406
  const jsonMode = args.dryRun && args.json;
7495
8407
  const diag = jsonMode ? (msg = "") => console.error(msg) : log;
7496
- const ownLock = path20.resolve(args.lock);
8408
+ const ownLock = path22.resolve(args.lock);
7497
8409
  let workspaceProjects = [];
7498
8410
  let workspaceDir = "";
7499
8411
  if (args.workspace) {
7500
- const workspacePath = path20.resolve(args.workspace);
7501
- workspaceDir = path20.dirname(workspacePath);
8412
+ const workspacePath = path22.resolve(args.workspace);
8413
+ workspaceDir = path22.dirname(workspacePath);
7502
8414
  const complete = await requireCompleteWorkspaceClaims(workspacePath);
7503
8415
  workspaceProjects = complete.ws.projects;
7504
8416
  for (const d of complete.diagnostics) diag(` WARN ${d.id}: ${d.message}`);
@@ -7507,12 +8419,12 @@ async function main() {
7507
8419
  const lockPaths = [
7508
8420
  .../* @__PURE__ */ new Set([
7509
8421
  ...workspaceLockPaths,
7510
- ...existsSync15(ownLock) ? [ownLock] : [],
7511
- ...args.claims.map((c) => path20.resolve(c))
8422
+ ...existsSync17(ownLock) ? [ownLock] : [],
8423
+ ...args.claims.map((c) => path22.resolve(c))
7512
8424
  ])
7513
8425
  ];
7514
8426
  diag(` claim set (${lockPaths.length} lockfile(s)):`);
7515
- for (const p of lockPaths) diag(` ${path20.relative(process.cwd(), p)}`);
8427
+ for (const p of lockPaths) diag(` ${path22.relative(process.cwd(), p)}`);
7516
8428
  if (!args.claims.length && !args.workspace) {
7517
8429
  diag(
7518
8430
  `
@@ -7520,7 +8432,7 @@ async function main() {
7520
8432
  pass every other project's lockfile via --claims a.json,b.json, or
7521
8433
  register every project in a workspace catalog and pass --workspace.`
7522
8434
  );
7523
- } else if (args.workspace && !workspaceProjects.some((p) => resolveProject(workspaceDir, p).manifestPath === path20.resolve(args.manifest))) {
8435
+ } else if (args.workspace && !workspaceProjects.some((p) => resolveProject(workspaceDir, p).manifestPath === path22.resolve(args.manifest))) {
7524
8436
  diag(
7525
8437
  `
7526
8438
  This project's manifest is not registered in the workspace catalog. Its own
@@ -7604,7 +8516,7 @@ async function main() {
7604
8516
  manifestPath: loaded.path,
7605
8517
  manifest: loaded.manifest,
7606
8518
  styleId,
7607
- importDir: path20.resolve(loaded.root, style.outDir),
8519
+ importDir: path22.resolve(loaded.root, style.outDir),
7608
8520
  lock,
7609
8521
  lockPath: args.lock
7610
8522
  },