pixelkiln 0.15.0 → 0.16.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 path23 from "path";
5
- import { existsSync as existsSync18 } from "fs";
6
- import { readFile as readFile17 } from "fs/promises";
4
+ import path24 from "path";
5
+ import { existsSync as existsSync19 } from "fs";
6
+ import { readFile as readFile18 } from "fs/promises";
7
7
 
8
8
  // src/env.ts
9
9
  import { readFileSync, existsSync } from "fs";
@@ -114,10 +114,10 @@ import { createReadStream } from "fs";
114
114
  function sha256(data) {
115
115
  return createHash("sha256").update(data).digest("hex");
116
116
  }
117
- async function sha256File(path24) {
117
+ async function sha256File(path25) {
118
118
  return new Promise((resolve, reject) => {
119
119
  const hash = createHash("sha256");
120
- const stream = createReadStream(path24);
120
+ const stream = createReadStream(path25);
121
121
  stream.on("data", (chunk2) => hash.update(chunk2));
122
122
  stream.on("error", reject);
123
123
  stream.on("end", () => resolve(hash.digest("hex")));
@@ -148,7 +148,14 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
148
148
  tileView: spec.tileView,
149
149
  tileFeature: spec.tileFeature,
150
150
  outlineMode: spec.outlineMode,
151
- styleImages: styleImageHashes
151
+ styleImages: styleImageHashes,
152
+ revision: spec.revision ? {
153
+ mode: spec.revision.mode,
154
+ from: spec.revision.sourceAssetId,
155
+ sourceSha256: spec.revision.sourceSha256,
156
+ maskSha256: spec.revision.maskSha256,
157
+ strength: spec.revision.strength
158
+ } : void 0
152
159
  })
153
160
  );
154
161
  }
@@ -623,6 +630,37 @@ var ComfyUIClient = class {
623
630
  }
624
631
  return promptId;
625
632
  }
633
+ async uploadImage(bytes, sha2562, format) {
634
+ const extension = format === "jpeg" ? "jpg" : "png";
635
+ const form = new FormData();
636
+ form.append(
637
+ "image",
638
+ new Blob([new Uint8Array(bytes)], { type: format === "jpeg" ? "image/jpeg" : "image/png" }),
639
+ `${sha2562}.${extension}`
640
+ );
641
+ form.append("type", "input");
642
+ form.append("subfolder", "pixelkiln");
643
+ form.append("overwrite", "true");
644
+ const url = this.endpoint("upload/image");
645
+ const response = await this.request(url, { method: "POST", body: form });
646
+ const text = await response.text();
647
+ let value = null;
648
+ try {
649
+ value = text ? JSON.parse(text) : null;
650
+ } catch {
651
+ value = null;
652
+ }
653
+ if (!response.ok) {
654
+ throw new Error(
655
+ `ComfyUI image upload failed (${response.status})` + (apiError(value) ? `: ${apiError(value)}` : "")
656
+ );
657
+ }
658
+ if (!isObject(value) || typeof value.name !== "string" || !value.name) {
659
+ throw new Error("ComfyUI image upload returned an invalid response");
660
+ }
661
+ const subfolder = typeof value.subfolder === "string" ? value.subfolder : "";
662
+ return subfolder ? `${subfolder.replace(/\/+$/, "")}/${value.name}` : value.name;
663
+ }
626
664
  async history(promptId) {
627
665
  return this.json(`history/${encodeURIComponent(promptId)}`);
628
666
  }
@@ -718,11 +756,15 @@ var ComfyUIProvider = class _ComfyUIProvider {
718
756
  supports(generator) {
719
757
  return generator === "map";
720
758
  }
759
+ supportsRevision(_mode) {
760
+ return true;
761
+ }
721
762
  estimate(spec) {
722
763
  return { unit: "free", amount: 0, candidates: resolvedOptions(spec).numImages };
723
764
  }
724
765
  validate(spec, styleImages) {
725
766
  const options = resolvedOptions(spec);
767
+ if (spec.source && !spec.revision) return;
726
768
  if (spec.width < 16 || spec.height < 16 || spec.width > 4096 || spec.height > 4096) {
727
769
  throw new Error("ComfyUI output dimensions must be between 16 and 4096 pixels");
728
770
  }
@@ -735,6 +777,31 @@ var ComfyUIProvider = class _ComfyUIProvider {
735
777
  if (spec.seed != null && !options.bindings.seed) {
736
778
  throw new Error("ComfyUI requires bindings.seed when the style declares a seed");
737
779
  }
780
+ if (!options.bindings.width !== !options.bindings.height) {
781
+ throw new Error("ComfyUI width and height bindings must be declared together");
782
+ }
783
+ if (!spec.revision && (!options.bindings.width || !options.bindings.height)) {
784
+ throw new Error("ComfyUI text-to-image generation requires width and height bindings");
785
+ }
786
+ if (options.numImages > 1 && !options.bindings.batchSize) {
787
+ throw new Error("ComfyUI numImages greater than 1 requires bindings.batchSize");
788
+ }
789
+ if (spec.revision) {
790
+ if (!options.bindings.sourceImage) {
791
+ throw new Error("ComfyUI revisions require bindings.sourceImage");
792
+ }
793
+ if (spec.revision.mode === "inpaint" && !options.bindings.maskImage) {
794
+ throw new Error("ComfyUI inpaint revisions require bindings.maskImage");
795
+ }
796
+ if (spec.revision.strength != null && !options.bindings.strength) {
797
+ throw new Error("ComfyUI revision strength requires bindings.strength");
798
+ }
799
+ if ((!options.bindings.width || !options.bindings.height) && spec.revision.sourceWidth != null && spec.revision.sourceHeight != null && (spec.width !== spec.revision.sourceWidth || spec.height !== spec.revision.sourceHeight)) {
800
+ throw new Error(
801
+ "ComfyUI revisions without width/height bindings must keep the source dimensions"
802
+ );
803
+ }
804
+ }
738
805
  validateWorkflowBindings(options.workflow, options);
739
806
  }
740
807
  rateLimit() {
@@ -745,14 +812,61 @@ var ComfyUIProvider = class _ComfyUIProvider {
745
812
  const options = resolvedOptions(spec);
746
813
  const workflow = structuredClone(options.workflow);
747
814
  setBinding(workflow, options.bindings.prompt, spec.prompt);
748
- setBinding(workflow, options.bindings.width, spec.width);
749
- setBinding(workflow, options.bindings.height, spec.height);
750
- setBinding(workflow, options.bindings.batchSize, options.numImages);
815
+ if (options.bindings.width) setBinding(workflow, options.bindings.width, spec.width);
816
+ if (options.bindings.height) setBinding(workflow, options.bindings.height, spec.height);
817
+ if (options.bindings.batchSize) {
818
+ setBinding(workflow, options.bindings.batchSize, options.numImages);
819
+ }
751
820
  if (spec.seed != null && options.bindings.seed) {
752
821
  setBinding(workflow, options.bindings.seed, spec.seed);
753
822
  }
823
+ if (spec.revision) {
824
+ if (!spec.revision.sourceSha256 || !spec.revision.sourceFormat) {
825
+ throw new Error("ComfyUI revision source is not ready");
826
+ }
827
+ const sourceBytes = await readFile(spec.revision.sourceFile);
828
+ if (sha256(sourceBytes) !== spec.revision.sourceSha256) {
829
+ throw new Error("ComfyUI revision source changed before upload");
830
+ }
831
+ const uploadedSource = await this.client.uploadImage(
832
+ sourceBytes,
833
+ spec.revision.sourceSha256,
834
+ spec.revision.sourceFormat
835
+ );
836
+ setBinding(workflow, options.bindings.sourceImage, uploadedSource);
837
+ if (spec.revision.maskFile) {
838
+ if (!spec.revision.maskSha256 || !spec.revision.maskFormat) {
839
+ throw new Error("ComfyUI revision mask is not ready");
840
+ }
841
+ const maskBytes = await readFile(spec.revision.maskFile);
842
+ if (sha256(maskBytes) !== spec.revision.maskSha256) {
843
+ throw new Error("ComfyUI revision mask changed before upload");
844
+ }
845
+ const uploadedMask = await this.client.uploadImage(
846
+ maskBytes,
847
+ spec.revision.maskSha256,
848
+ spec.revision.maskFormat
849
+ );
850
+ setBinding(workflow, options.bindings.maskImage, uploadedMask);
851
+ }
852
+ if (spec.revision.strength != null) {
853
+ setBinding(workflow, options.bindings.strength, spec.revision.strength);
854
+ }
855
+ }
754
856
  const promptId = await this.client.submit(workflow);
755
- return { jobId: encodeJob(promptId, options.outputNodeId) };
857
+ return {
858
+ jobId: encodeJob(promptId, options.outputNodeId),
859
+ ...spec.revision ? {
860
+ metadata: {
861
+ revision: {
862
+ mode: spec.revision.mode,
863
+ sourceAssetId: spec.revision.sourceAssetId,
864
+ sourceSha256: spec.revision.sourceSha256,
865
+ ...spec.revision.maskSha256 ? { maskSha256: spec.revision.maskSha256 } : {}
866
+ }
867
+ }
868
+ } : {}
869
+ };
756
870
  }
757
871
  async poll(jobId, _generator, context) {
758
872
  const { promptId, outputNodeId } = decodeJob(jobId);
@@ -827,15 +941,27 @@ function parseOptions(value) {
827
941
  throw new Error("ComfyUI numImages must be a whole number from 1 to 16");
828
942
  }
829
943
  if (!isObject(value.bindings)) throw new Error("ComfyUI bindings must be an object");
830
- const bindingKeys = /* @__PURE__ */ new Set(["prompt", "width", "height", "batchSize", "seed"]);
944
+ const bindingKeys = /* @__PURE__ */ new Set([
945
+ "prompt",
946
+ "width",
947
+ "height",
948
+ "batchSize",
949
+ "seed",
950
+ "sourceImage",
951
+ "maskImage",
952
+ "strength"
953
+ ]);
831
954
  const extraBindings = Object.keys(value.bindings).filter((key) => !bindingKeys.has(key));
832
955
  if (extraBindings.length) throw new Error(`Unknown ComfyUI binding(s): ${extraBindings.join(", ")}`);
833
956
  const bindings = {
834
957
  prompt: parseBinding(value.bindings.prompt, "prompt"),
835
- width: parseBinding(value.bindings.width, "width"),
836
- height: parseBinding(value.bindings.height, "height"),
837
- batchSize: parseBinding(value.bindings.batchSize, "batchSize"),
838
- ...value.bindings.seed == null ? {} : { seed: parseBinding(value.bindings.seed, "seed") }
958
+ ...value.bindings.width == null ? {} : { width: parseBinding(value.bindings.width, "width") },
959
+ ...value.bindings.height == null ? {} : { height: parseBinding(value.bindings.height, "height") },
960
+ ...value.bindings.batchSize == null ? {} : { batchSize: parseBinding(value.bindings.batchSize, "batchSize") },
961
+ ...value.bindings.seed == null ? {} : { seed: parseBinding(value.bindings.seed, "seed") },
962
+ ...value.bindings.sourceImage == null ? {} : { sourceImage: parseBinding(value.bindings.sourceImage, "sourceImage") },
963
+ ...value.bindings.maskImage == null ? {} : { maskImage: parseBinding(value.bindings.maskImage, "maskImage") },
964
+ ...value.bindings.strength == null ? {} : { strength: parseBinding(value.bindings.strength, "strength") }
839
965
  };
840
966
  const workflow = value.workflow == null ? void 0 : parseWorkflow(value.workflow, workflowFile);
841
967
  const workflowSha256 = value.workflowSha256 == null ? void 0 : requiredString(value.workflowSha256, "workflowSha256");
@@ -928,7 +1054,16 @@ function comfyMetadata(spec, promptId, outputNodeId, images) {
928
1054
  outputCount: images.length,
929
1055
  workflowFile: options.workflowFile,
930
1056
  workflowSha256: options.workflowSha256,
931
- files: images.map((image) => ({ ...image }))
1057
+ files: images.map((image) => ({ ...image })),
1058
+ ...spec.revision ? {
1059
+ revision: {
1060
+ mode: spec.revision.mode,
1061
+ sourceAssetId: spec.revision.sourceAssetId,
1062
+ sourceSha256: spec.revision.sourceSha256,
1063
+ ...spec.revision.maskSha256 ? { maskSha256: spec.revision.maskSha256 } : {},
1064
+ ...spec.revision.strength == null ? {} : { strength: spec.revision.strength }
1065
+ }
1066
+ } : {}
932
1067
  };
933
1068
  }
934
1069
  function encodeJob(promptId, outputNodeId) {
@@ -1129,11 +1264,11 @@ var PixelLabClient = class {
1129
1264
  * common than the failure mode of retrying (a duplicate object), and a
1130
1265
  * duplicate is visible and free to delete whereas a silent gap is neither.
1131
1266
  */
1132
- async request(path24, init, attempt = 0) {
1267
+ async request(path25, init, attempt = 0) {
1133
1268
  const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
1134
1269
  let res;
1135
1270
  try {
1136
- res = await fetch(`${BASE}${path24}`, {
1271
+ res = await fetch(`${BASE}${path25}`, {
1137
1272
  ...init,
1138
1273
  signal: init?.signal ?? AbortSignal.timeout(this.timeoutMs),
1139
1274
  headers: {
@@ -1145,24 +1280,24 @@ var PixelLabClient = class {
1145
1280
  } catch (err) {
1146
1281
  if (attempt < MAX_RETRIES) {
1147
1282
  await sleep(backoffMs(attempt));
1148
- return this.request(path24, init, attempt + 1);
1283
+ return this.request(path25, init, attempt + 1);
1149
1284
  }
1150
1285
  throw err;
1151
1286
  }
1152
1287
  if (!res.ok && shouldRetry(res.status) && attempt < MAX_RETRIES) {
1153
1288
  const waitMs = retryAfterMs(res.headers.get("retry-after")) ?? backoffMs(attempt);
1154
1289
  await sleep(waitMs);
1155
- return this.request(path24, init, attempt + 1);
1290
+ return this.request(path25, init, attempt + 1);
1156
1291
  }
1157
1292
  const text = await res.text();
1158
1293
  if (!res.ok) {
1159
- throw new PixelLabError(`${init?.method ?? "GET"} ${path24} \u2192 ${res.status}`, res.status, text);
1294
+ throw new PixelLabError(`${init?.method ?? "GET"} ${path25} \u2192 ${res.status}`, res.status, text);
1160
1295
  }
1161
1296
  if (!text) return {};
1162
1297
  try {
1163
1298
  return JSON.parse(text);
1164
1299
  } catch {
1165
- throw new Error(`${init?.method ?? "GET"} ${path24} returned invalid JSON`);
1300
+ throw new Error(`${init?.method ?? "GET"} ${path25} returned invalid JSON`);
1166
1301
  }
1167
1302
  }
1168
1303
  async balance() {
@@ -1369,6 +1504,7 @@ import { z as z2 } from "zod";
1369
1504
  var MediaTypeSchema = z2.enum(["image/png", "image/gif"]);
1370
1505
  var GeneratorSchema = z2.enum(["1dir", "map", "pixflux", "tiles", "animation"]);
1371
1506
  var GridConfidenceSchema = z2.enum(["low", "medium", "high"]);
1507
+ var RevisionModeSchema = z2.enum(["image-to-image", "inpaint", "outpaint"]);
1372
1508
  function tileVariationCount(descriptions) {
1373
1509
  return Math.max(1, descriptions) * 4;
1374
1510
  }
@@ -1419,6 +1555,31 @@ var QualityProfileSchema = z2.object({
1419
1555
  (profile) => new Set(profile.palette.map((color) => color.replace(/^#/, "").toLowerCase())).size === profile.palette.length,
1420
1556
  { message: "quality palette colors must be unique", path: ["palette"] }
1421
1557
  );
1558
+ var RevisionSchema = z2.object({
1559
+ /** What kind of controlled change the provider workflow performs. */
1560
+ mode: RevisionModeSchema,
1561
+ /** Parent asset id in the same style. */
1562
+ from: z2.string().min(1),
1563
+ /** Manifest-relative black/white PNG. Required only for masked inpainting. */
1564
+ mask: z2.string().min(1).optional(),
1565
+ /** Provider-neutral edit strength. The active adapter must bind it explicitly. */
1566
+ strength: z2.number().min(0).max(1).optional()
1567
+ }).strict().superRefine((revision, context) => {
1568
+ if (revision.mode === "inpaint" && !revision.mask) {
1569
+ context.addIssue({
1570
+ code: z2.ZodIssueCode.custom,
1571
+ message: "inpaint revisions require a mask",
1572
+ path: ["mask"]
1573
+ });
1574
+ }
1575
+ if (revision.mode !== "inpaint" && revision.mask) {
1576
+ context.addIssue({
1577
+ code: z2.ZodIssueCode.custom,
1578
+ message: `${revision.mode} revisions do not accept a mask`,
1579
+ path: ["mask"]
1580
+ });
1581
+ }
1582
+ });
1422
1583
  var StyleSchema = z2.object({
1423
1584
  /** Generation backend for this style. Omit to inherit the manifest default. */
1424
1585
  provider: z2.string().min(1).optional(),
@@ -1584,6 +1745,8 @@ var AssetSchema = z2.object({
1584
1745
  * that wasn't. `prompt` still records what was asked for.
1585
1746
  */
1586
1747
  source: z2.string().optional(),
1748
+ /** Controlled generation derived from another asset in the same style. */
1749
+ revision: RevisionSchema.optional(),
1587
1750
  /**
1588
1751
  * Generated output role to place in `cell` when this asset expands to
1589
1752
  * several files. Omit for ordinary single-output assets. A structural set
@@ -1604,7 +1767,15 @@ var AssetSchema = z2.object({
1604
1767
  * style would invalidate every other style's already-generated art.
1605
1768
  */
1606
1769
  promptByStyle: z2.record(z2.string()).default({})
1607
- }).strict();
1770
+ }).strict().superRefine((asset, context) => {
1771
+ if (asset.source && asset.revision) {
1772
+ context.addIssue({
1773
+ code: z2.ZodIssueCode.custom,
1774
+ message: "source and revision are mutually exclusive",
1775
+ path: ["revision"]
1776
+ });
1777
+ }
1778
+ });
1608
1779
  var ManifestSchema = z2.object({
1609
1780
  $schema: z2.string().optional(),
1610
1781
  name: z2.string(),
@@ -1625,6 +1796,14 @@ var LockEntrySchema = z2.object({
1625
1796
  prompt: z2.string(),
1626
1797
  width: z2.number().int(),
1627
1798
  height: z2.number().int(),
1799
+ /** Explicit parent/input lineage for a revision generation. */
1800
+ revision: z2.object({
1801
+ mode: RevisionModeSchema,
1802
+ sourceAssetId: z2.string().min(1),
1803
+ sourceSha256: z2.string().regex(/^[0-9a-f]{64}$/),
1804
+ maskSha256: z2.string().regex(/^[0-9a-f]{64}$/).optional(),
1805
+ strength: z2.number().min(0).max(1).optional()
1806
+ }).strict().nullable().default(null),
1628
1807
  /** Set at submit time, before the request is awaited, so a crash is recoverable. */
1629
1808
  jobId: z2.string().nullable().default(null),
1630
1809
  /** For `1dir`: the multi-candidate parent object awaiting selection. */
@@ -2032,9 +2211,9 @@ var RetroDiffusionClient = class {
2032
2211
  }
2033
2212
  return balance;
2034
2213
  }
2035
- async call(path24, init = {}) {
2214
+ async call(path25, init = {}) {
2036
2215
  if (!this.token) throw new Error("RD_API_KEY is not set");
2037
- const response = await this.request(`${this.baseUrl}${path24}`, {
2216
+ const response = await this.request(`${this.baseUrl}${path25}`, {
2038
2217
  ...init,
2039
2218
  headers: {
2040
2219
  "Content-Type": "application/json",
@@ -2908,7 +3087,36 @@ ${issues}`);
2908
3087
  unknownReferences.push(`assets.${assetId}.promptByStyle: unknown style "${styleId}"`);
2909
3088
  }
2910
3089
  }
3090
+ if (asset.revision) {
3091
+ if (!parsed.data.assets[asset.revision.from]) {
3092
+ unknownReferences.push(
3093
+ `assets.${assetId}.revision.from: unknown asset "${asset.revision.from}"`
3094
+ );
3095
+ } else if (asset.revision.from === assetId) {
3096
+ unknownReferences.push(`assets.${assetId}.revision.from: an asset cannot revise itself`);
3097
+ }
3098
+ }
2911
3099
  }
3100
+ const visiting = /* @__PURE__ */ new Set();
3101
+ const visited = /* @__PURE__ */ new Set();
3102
+ const visitRevision = (assetId, chain) => {
3103
+ if (visited.has(assetId)) return;
3104
+ if (visiting.has(assetId)) {
3105
+ const start = chain.indexOf(assetId);
3106
+ unknownReferences.push(
3107
+ `assets.${assetId}.revision.from: revision cycle ${[...chain.slice(start), assetId].join(" -> ")}`
3108
+ );
3109
+ return;
3110
+ }
3111
+ visiting.add(assetId);
3112
+ const parent = parsed.data.assets[assetId]?.revision?.from;
3113
+ if (parent && parent !== assetId && parsed.data.assets[parent]) {
3114
+ visitRevision(parent, [...chain, assetId]);
3115
+ }
3116
+ visiting.delete(assetId);
3117
+ visited.add(assetId);
3118
+ };
3119
+ for (const assetId of Object.keys(parsed.data.assets)) visitRevision(assetId, []);
2912
3120
  if (unknownReferences.length) {
2913
3121
  throw new Error(`Manifest at ${abs} is invalid:
2914
3122
  ${unknownReferences.map((i) => ` ${i}`).join("\n")}`);
@@ -2944,6 +3152,15 @@ async function resolveSpecs(loaded, filter) {
2944
3152
  throw new Error(`Unknown asset "${unknownAsset}".`);
2945
3153
  }
2946
3154
  }
3155
+ const requestedAssetIds = new Set(filter?.assets?.length ? filter.assets : Object.keys(manifest.assets));
3156
+ const resolutionAssetIds = new Set(requestedAssetIds);
3157
+ for (const assetId of [...requestedAssetIds]) {
3158
+ let current = manifest.assets[assetId]?.revision?.from;
3159
+ while (current && !resolutionAssetIds.has(current)) {
3160
+ resolutionAssetIds.add(current);
3161
+ current = manifest.assets[current]?.revision?.from;
3162
+ }
3163
+ }
2947
3164
  const styleImageCache = /* @__PURE__ */ new Map();
2948
3165
  async function loadStyleImage(rel) {
2949
3166
  const abs = path4.resolve(root, rel);
@@ -2975,8 +3192,13 @@ async function resolveSpecs(loaded, filter) {
2975
3192
  styleImageHashes.push(loadedImage.hash);
2976
3193
  styleImageDimensions.push(loadedImage);
2977
3194
  }
3195
+ const resolvedImages = style.styleImages.map((image) => {
3196
+ const hit = styleImageCache.get(path4.resolve(root, image.path));
3197
+ return { base64: hit.base64, width: hit.width, height: hit.height, format: hit.format };
3198
+ });
3199
+ const styleSpecs = /* @__PURE__ */ new Map();
2978
3200
  for (const [assetId, asset] of Object.entries(manifest.assets)) {
2979
- if (filter?.assets?.length && !filter.assets.includes(assetId)) continue;
3201
+ if (!resolutionAssetIds.has(assetId)) continue;
2980
3202
  if (asset.styles.length && !asset.styles.includes(styleId)) continue;
2981
3203
  const generator = style.generator;
2982
3204
  if (!activeProvider.supports(generator)) {
@@ -3056,18 +3278,69 @@ async function resolveSpecs(loaded, filter) {
3056
3278
  } : {},
3057
3279
  tags,
3058
3280
  source: asset.source,
3059
- specHash: specHash(base, styleImageHashes, providerOptionIdentity)
3281
+ // Revision identity is attached after every dependency in this style
3282
+ // has a concrete output target. Finalization below computes the hash.
3283
+ specHash: ""
3060
3284
  };
3061
- const resolvedImages = style.styleImages.map((image) => {
3062
- const hit = styleImageCache.get(path4.resolve(root, image.path));
3063
- return { base64: hit.base64, width: hit.width, height: hit.height, format: hit.format };
3064
- });
3285
+ styleSpecs.set(assetId, resolved);
3286
+ }
3287
+ const finalized = /* @__PURE__ */ new Set();
3288
+ const finalize = async (assetId) => {
3289
+ const resolved = styleSpecs.get(assetId);
3290
+ if (!resolved) {
3291
+ throw new Error(
3292
+ `Asset "${assetId}" is not available in style "${styleId}"; revision parents must participate in the same style.`
3293
+ );
3294
+ }
3295
+ if (finalized.has(assetId)) return resolved;
3296
+ const asset = manifest.assets[assetId];
3297
+ if (asset.revision) {
3298
+ if (!activeProvider.supportsRevision?.(asset.revision.mode)) {
3299
+ throw new Error(
3300
+ `Provider "${activeProvider.id}" does not support ${asset.revision.mode} revisions`
3301
+ );
3302
+ }
3303
+ const sourceSpec = await finalize(asset.revision.from);
3304
+ const sourceFile = sourceSpec.quality?.outFile ?? (sourceSpec.source ? path4.resolve(root, sourceSpec.source) : sourceSpec.outFile);
3305
+ const sourceImage = await optionalRevisionImage(sourceFile, "revision source");
3306
+ const maskFile = asset.revision.mask ? path4.resolve(root, asset.revision.mask) : void 0;
3307
+ const maskImage = maskFile ? await optionalRevisionImage(maskFile, "revision mask", "png") : null;
3308
+ if (sourceImage && maskImage && (sourceImage.width !== maskImage.width || sourceImage.height !== maskImage.height)) {
3309
+ throw new Error(
3310
+ `Revision mask for ${styleId}/${assetId} is ${maskImage.width}x${maskImage.height}; source ${asset.revision.from} is ${sourceImage.width}x${sourceImage.height}`
3311
+ );
3312
+ }
3313
+ resolved.revision = {
3314
+ mode: asset.revision.mode,
3315
+ sourceAssetId: asset.revision.from,
3316
+ sourceFile,
3317
+ sourceSha256: sourceImage?.hash ?? null,
3318
+ sourceWidth: sourceImage?.width ?? null,
3319
+ sourceHeight: sourceImage?.height ?? null,
3320
+ sourceFormat: sourceImage?.format ?? null,
3321
+ sourceSpec,
3322
+ ...maskFile ? {
3323
+ maskFile,
3324
+ maskSha256: maskImage?.hash ?? null,
3325
+ maskWidth: maskImage?.width ?? null,
3326
+ maskHeight: maskImage?.height ?? null,
3327
+ maskFormat: maskImage?.format ?? null
3328
+ } : {},
3329
+ ...asset.revision.strength == null ? {} : { strength: asset.revision.strength }
3330
+ };
3331
+ }
3332
+ resolved.specHash = specHash(resolved, styleImageHashes, providerOptionIdentity);
3065
3333
  activeProvider.validate?.(resolved, resolvedImages);
3066
3334
  const estimate = validateCostEstimate(activeProvider.id, activeProvider.estimate(resolved));
3067
3335
  resolved.cost = estimate.amount;
3068
3336
  resolved.costUnit = estimate.unit;
3069
3337
  resolved.candidates = estimate.candidates;
3070
- specs.push(resolved);
3338
+ finalized.add(assetId);
3339
+ return resolved;
3340
+ };
3341
+ for (const assetId of Object.keys(manifest.assets)) {
3342
+ if (!requestedAssetIds.has(assetId) || !styleSpecs.has(assetId)) continue;
3343
+ specs.push(await finalize(assetId));
3071
3344
  }
3072
3345
  }
3073
3346
  const byOutput = /* @__PURE__ */ new Map();
@@ -3089,6 +3362,16 @@ function pngPath(file) {
3089
3362
  const extension = path4.extname(file);
3090
3363
  return extension ? `${file.slice(0, -extension.length)}.png` : `${file}.png`;
3091
3364
  }
3365
+ async function optionalRevisionImage(file, label, requiredFormat) {
3366
+ if (!existsSync3(file)) return null;
3367
+ const bytes = await readFile2(file);
3368
+ const metadata = imageMetadata(bytes);
3369
+ if (!metadata) throw new Error(`${label} is not a readable PNG or JPEG: ${file}`);
3370
+ if (requiredFormat && metadata.format !== requiredFormat) {
3371
+ throw new Error(`${label} must be ${requiredFormat.toUpperCase()}: ${file}`);
3372
+ }
3373
+ return { hash: sha256(bytes), ...metadata };
3374
+ }
3092
3375
  async function resolveStyleImages(loaded, styleId) {
3093
3376
  const style = loaded.manifest.styles[styleId];
3094
3377
  if (!style) throw new Error(`Unknown style "${styleId}"`);
@@ -3677,8 +3960,8 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
3677
3960
  }
3678
3961
 
3679
3962
  // src/pipeline/plan.ts
3680
- import { existsSync as existsSync7 } from "fs";
3681
- import path12 from "path";
3963
+ import { existsSync as existsSync8 } from "fs";
3964
+ import path13 from "path";
3682
3965
 
3683
3966
  // src/pipeline/quality-profile.ts
3684
3967
  import { existsSync as existsSync6 } from "fs";
@@ -4831,6 +5114,83 @@ async function requireApprovedQualitySources(specs, lock, assetIds) {
4831
5114
  return sources;
4832
5115
  }
4833
5116
 
5117
+ // src/pipeline/revision.ts
5118
+ import { existsSync as existsSync7 } from "fs";
5119
+ import path12 from "path";
5120
+ async function inspectRevisionReadiness(spec, lock) {
5121
+ if (!spec.revision) return null;
5122
+ return inspectRevision(spec, lock, /* @__PURE__ */ new Set());
5123
+ }
5124
+ async function inspectRevision(spec, lock, seen) {
5125
+ const revision = spec.revision;
5126
+ if (!revision) return { ready: true, reason: "asset has no revision inputs" };
5127
+ const dependency = await inspectParent(revision.sourceSpec, lock, seen);
5128
+ if (!dependency.ready) {
5129
+ return {
5130
+ ready: false,
5131
+ reason: `revision source ${spec.styleId}/${revision.sourceAssetId} is not ready: ${dependency.reason}`
5132
+ };
5133
+ }
5134
+ if (!revision.sourceSha256 || !existsSync7(revision.sourceFile)) {
5135
+ return { ready: false, reason: `revision source is missing: ${revision.sourceFile}` };
5136
+ }
5137
+ if (await sha256File(revision.sourceFile) !== revision.sourceSha256) {
5138
+ return { ready: false, reason: "revision source changed after the manifest was resolved" };
5139
+ }
5140
+ if (revision.maskFile) {
5141
+ if (!revision.maskSha256 || !existsSync7(revision.maskFile)) {
5142
+ return { ready: false, reason: `revision mask is missing: ${revision.maskFile}` };
5143
+ }
5144
+ if (await sha256File(revision.maskFile) !== revision.maskSha256) {
5145
+ return { ready: false, reason: "revision mask changed after the manifest was resolved" };
5146
+ }
5147
+ }
5148
+ return { ready: true, reason: "revision inputs are current" };
5149
+ }
5150
+ async function requireRevisionReady(spec, lock) {
5151
+ const inspection = await inspectRevisionReadiness(spec, lock);
5152
+ if (inspection && !inspection.ready) throw new Error(inspection.reason);
5153
+ }
5154
+ async function inspectParent(spec, lock, seen) {
5155
+ const key = lockKey(spec.styleId, spec.assetId);
5156
+ if (seen.has(key)) return { ready: false, reason: `revision dependency cycle at ${key}` };
5157
+ const nextSeen = new Set(seen);
5158
+ nextSeen.add(key);
5159
+ if (spec.revision) {
5160
+ const inputs = await inspectRevision(spec, lock, nextSeen);
5161
+ if (!inputs.ready) return inputs;
5162
+ }
5163
+ if (spec.quality) {
5164
+ const quality = await inspectQualityProfile(spec, lock);
5165
+ return quality?.state === "approved" ? { ready: true, reason: "approved quality output is current" } : {
5166
+ ready: false,
5167
+ reason: `quality output is ${quality?.state ?? "missing"}` + (quality?.reason ? ` (${quality.reason})` : "")
5168
+ };
5169
+ }
5170
+ if (spec.source) {
5171
+ const source = path12.resolve(spec.root, spec.source);
5172
+ return existsSync7(source) ? { ready: true, reason: "declared source is present" } : { ready: false, reason: `declared source is missing: ${spec.source}` };
5173
+ }
5174
+ const entry = lock.entries[key];
5175
+ if (!entry) return { ready: false, reason: "parent is not in the lockfile" };
5176
+ if (entry.specHash !== spec.specHash) {
5177
+ return { ready: false, reason: "parent generation spec is stale" };
5178
+ }
5179
+ if (entry.status !== "downloaded") {
5180
+ return { ready: false, reason: `parent is ${entry.status}, not downloaded` };
5181
+ }
5182
+ if (!entry.outputs.length) return { ready: false, reason: "parent has no recorded output" };
5183
+ for (let index = 0; index < entry.outputs.length; index++) {
5184
+ const output = entry.outputs[index];
5185
+ const file = currentEntryOutputPath(entry, spec, index);
5186
+ if (!existsSync7(file)) return { ready: false, reason: `parent output is missing: ${file}` };
5187
+ if (await sha256File(file) !== output.sha256) {
5188
+ return { ready: false, reason: `parent output was modified: ${file}` };
5189
+ }
5190
+ }
5191
+ return { ready: true, reason: "parent output is current" };
5192
+ }
5193
+
4834
5194
  // src/pipeline/plan.ts
4835
5195
  async function anyOutputModified(entry, spec) {
4836
5196
  for (let index = 0; index < entry.outputs.length; index++) {
@@ -4846,11 +5206,15 @@ async function buildPlan(specs, lock, opts = {}) {
4846
5206
  const entry = lock.entries[key];
4847
5207
  let state;
4848
5208
  let reason;
4849
- if (opts.force) {
5209
+ const revision = await inspectRevisionReadiness(spec, lock);
5210
+ if (revision && !revision.ready) {
5211
+ state = "blocked";
5212
+ reason = revision.reason;
5213
+ } else if (opts.force) {
4850
5214
  state = "missing";
4851
5215
  reason = "--force";
4852
5216
  } else if (!entry && spec.source) {
4853
- if (existsSync7(path12.resolve(spec.root, spec.source))) {
5217
+ if (existsSync8(path13.resolve(spec.root, spec.source))) {
4854
5218
  state = "ok";
4855
5219
  reason = `placed from ${spec.source}; not generated`;
4856
5220
  } else {
@@ -4858,7 +5222,7 @@ async function buildPlan(specs, lock, opts = {}) {
4858
5222
  reason = `declared source is missing: ${spec.source}`;
4859
5223
  }
4860
5224
  } else if (!entry) {
4861
- if (existsSync7(spec.outFile)) {
5225
+ if (existsSync8(spec.outFile)) {
4862
5226
  state = "untracked";
4863
5227
  reason = "file on disk, no provenance recorded";
4864
5228
  } else {
@@ -4877,9 +5241,9 @@ async function buildPlan(specs, lock, opts = {}) {
4877
5241
  } else if (entry.status !== "downloaded") {
4878
5242
  state = "in-flight";
4879
5243
  reason = `awaiting ${entry.status}`;
4880
- } else if (entry.outputs.length === 0 || entry.outputs.some((_output, index) => !existsSync7(currentEntryOutputPath(entry, spec, index)))) {
5244
+ } else if (entry.outputs.length === 0 || entry.outputs.some((_output, index) => !existsSync8(currentEntryOutputPath(entry, spec, index)))) {
4881
5245
  state = "orphaned";
4882
- reason = entry.outputs.length === 0 ? "no recorded output files" : `missing on disk: ${entry.outputs.filter((_output, index) => !existsSync7(currentEntryOutputPath(entry, spec, index))).map((o) => o.role ?? "asset").join(", ")}`;
5246
+ reason = entry.outputs.length === 0 ? "no recorded output files" : `missing on disk: ${entry.outputs.filter((_output, index) => !existsSync8(currentEntryOutputPath(entry, spec, index))).map((o) => o.role ?? "asset").join(", ")}`;
4883
5247
  } else if (await anyOutputModified(entry, spec)) {
4884
5248
  state = "orphaned";
4885
5249
  reason = "output modified since download";
@@ -4936,6 +5300,7 @@ function summarize(plan) {
4936
5300
  orphaned: 0,
4937
5301
  "in-flight": 0,
4938
5302
  recoverable: 0,
5303
+ blocked: 0,
4939
5304
  failed: 0
4940
5305
  };
4941
5306
  for (const item of plan.items) counts[item.state]++;
@@ -5008,6 +5373,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
5008
5373
  await waitForSlot();
5009
5374
  const since = Date.now() - lastSubmitAt;
5010
5375
  if (since < spacing) await sleep2(spacing - since);
5376
+ await requireRevisionReady(spec, lock);
5011
5377
  upsert(lock, key, {
5012
5378
  styleId: spec.styleId,
5013
5379
  assetId: spec.assetId,
@@ -5017,6 +5383,13 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
5017
5383
  prompt: spec.prompt,
5018
5384
  width: spec.width,
5019
5385
  height: spec.height,
5386
+ revision: spec.revision ? {
5387
+ mode: spec.revision.mode,
5388
+ sourceAssetId: spec.revision.sourceAssetId,
5389
+ sourceSha256: spec.revision.sourceSha256,
5390
+ ...spec.revision.maskSha256 ? { maskSha256: spec.revision.maskSha256 } : {},
5391
+ ...spec.revision.strength == null ? {} : { strength: spec.revision.strength }
5392
+ } : null,
5020
5393
  status: "pending",
5021
5394
  jobId: null,
5022
5395
  objectId: null,
@@ -5142,9 +5515,9 @@ async function poll(provider, lock, lockPath, opts = {}) {
5142
5515
  }
5143
5516
 
5144
5517
  // src/pipeline/fetch.ts
5145
- import { existsSync as existsSync8 } from "fs";
5518
+ import { existsSync as existsSync9 } from "fs";
5146
5519
  import { mkdir as mkdir3, readFile as readFile7, rename as rename3, rm as rm4, writeFile as writeFile3 } from "fs/promises";
5147
- import path13 from "path";
5520
+ import path14 from "path";
5148
5521
 
5149
5522
  // src/source-url.ts
5150
5523
  var SENSITIVE_QUERY_KEYS = /* @__PURE__ */ new Set([
@@ -5190,13 +5563,13 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
5190
5563
  const result = { downloaded: 0, skipped: 0, failed: 0 };
5191
5564
  const specByKey = new Map(specs.map((s) => [lockKey(s.styleId, s.assetId), s]));
5192
5565
  normalizeLockOutputPaths(lock, specs);
5193
- const cacheDir = opts.cacheDir === false ? null : path13.resolve(opts.cacheDir ?? path13.join(path13.dirname(lockPath), ".pixelkiln", "cache"));
5566
+ const cacheDir = opts.cacheDir === false ? null : path14.resolve(opts.cacheDir ?? path14.join(path14.dirname(lockPath), ".pixelkiln", "cache"));
5194
5567
  const pending = Object.entries(lock.entries).filter(([key, e]) => {
5195
5568
  if (e.provider !== provider.id || !specByKey.has(key)) return false;
5196
5569
  if (e.status === "selected" || e.status === "download-failed") return true;
5197
5570
  if (!opts.repair || e.status !== "downloaded") return false;
5198
5571
  const spec = specByKey.get(key);
5199
- return e.outputs.length === 0 || !spec || e.outputs.some((output) => !existsSync8(resolveOutputPath(output.path, spec.root)));
5572
+ return e.outputs.length === 0 || !spec || e.outputs.some((output) => !existsSync9(resolveOutputPath(output.path, spec.root)));
5200
5573
  });
5201
5574
  const concurrency = Math.min(Math.max(1, opts.concurrency ?? 8), Math.max(1, pending.length));
5202
5575
  let cursor = 0;
@@ -5233,7 +5606,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
5233
5606
  (o) => source.role ? o.role === source.role : !o.role && sources.length === 1
5234
5607
  );
5235
5608
  const target = recorded ? resolveOutputPath(recorded.path, spec.root) : expectedOutputPath(spec, source.role, index, sources.length, source.mediaType);
5236
- if (existsSync8(target)) {
5609
+ if (existsSync9(target)) {
5237
5610
  if (!recorded) {
5238
5611
  throw new Error(`refusing to overwrite untracked output ${target}`);
5239
5612
  }
@@ -5254,7 +5627,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
5254
5627
  const expectedMediaType = source.mediaType ?? recorded?.mediaType ?? MediaType.PNG;
5255
5628
  let buf = recorded && cacheDir ? await readCachedMedia(cacheDir, recorded.sha256, expectedMediaType) : null;
5256
5629
  if (buf) {
5257
- log2(` cached ${path13.relative(process.cwd(), target)}`);
5630
+ log2(` cached ${path14.relative(process.cwd(), target)}`);
5258
5631
  } else {
5259
5632
  if (!source.url) {
5260
5633
  throw new Error(`no source URL or cached bytes remain for ${source.role ?? "asset"}`);
@@ -5272,7 +5645,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
5272
5645
  );
5273
5646
  }
5274
5647
  if (cacheDir) await cacheMedia(cacheDir, buf, mediaType);
5275
- await mkdir3(path13.dirname(target), { recursive: true });
5648
+ await mkdir3(path14.dirname(target), { recursive: true });
5276
5649
  const tmp = `${target}.pixelkiln.tmp`;
5277
5650
  await writeFile3(tmp, buf);
5278
5651
  outputs.push({
@@ -5284,7 +5657,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
5284
5657
  upsert(lock, key, { outputs: mergeOutputs(entry.outputs, outputs) });
5285
5658
  await saveLock(lockPath, lock);
5286
5659
  await rename3(tmp, target);
5287
- log2(` wrote ${path13.relative(process.cwd(), target)}`);
5660
+ log2(` wrote ${path14.relative(process.cwd(), target)}`);
5288
5661
  }
5289
5662
  const persistentSources = sources.filter((source) => shouldPersistSourceUrl(source.url));
5290
5663
  upsert(lock, key, {
@@ -5318,8 +5691,8 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
5318
5691
  return result;
5319
5692
  }
5320
5693
  async function readCachedMedia(cacheDir, hash, mediaType) {
5321
- const file = path13.join(cacheDir, cacheFileName(hash, mediaType));
5322
- if (!existsSync8(file)) return null;
5694
+ const file = path14.join(cacheDir, cacheFileName(hash, mediaType));
5695
+ if (!existsSync9(file)) return null;
5323
5696
  try {
5324
5697
  const buf = await readFile7(file);
5325
5698
  if (sha256(buf) !== hash) return null;
@@ -5332,7 +5705,7 @@ async function readCachedMedia(cacheDir, hash, mediaType) {
5332
5705
  async function cacheMedia(cacheDir, buf, mediaType, knownHash) {
5333
5706
  const hash = knownHash ?? sha256(buf);
5334
5707
  validateMedia(buf, mediaType);
5335
- const file = path13.join(cacheDir, cacheFileName(hash, mediaType));
5708
+ const file = path14.join(cacheDir, cacheFileName(hash, mediaType));
5336
5709
  if (await readCachedMedia(cacheDir, hash, mediaType)) return;
5337
5710
  await mkdir3(cacheDir, { recursive: true });
5338
5711
  const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
@@ -5340,7 +5713,7 @@ async function cacheMedia(cacheDir, buf, mediaType, knownHash) {
5340
5713
  try {
5341
5714
  await rename3(tmp, file);
5342
5715
  } catch (err) {
5343
- if (!existsSync8(file)) throw err;
5716
+ if (!existsSync9(file)) throw err;
5344
5717
  } finally {
5345
5718
  await rm4(tmp, { force: true });
5346
5719
  }
@@ -5373,9 +5746,9 @@ async function pushTags(provider, specs, lock, opts = {}) {
5373
5746
  }
5374
5747
 
5375
5748
  // src/pipeline/doctor.ts
5376
- import { constants, existsSync as existsSync9 } from "fs";
5749
+ import { constants, existsSync as existsSync10 } from "fs";
5377
5750
  import { access, stat as stat2 } from "fs/promises";
5378
- import path14 from "path";
5751
+ import path15 from "path";
5379
5752
  async function doctor(loaded, specs, lock, lockPath, opts = {}) {
5380
5753
  const checks = [];
5381
5754
  const add = (id, level, message5) => checks.push({ id, level, message: message5 });
@@ -5386,8 +5759,8 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
5386
5759
  );
5387
5760
  add("lock", "ok", `${Object.keys(lock.entries).length} valid v2 lock entr(ies)`);
5388
5761
  const dirs = [...new Set(specs.flatMap((spec) => [
5389
- path14.dirname(spec.outFile),
5390
- ...spec.quality ? [path14.dirname(spec.quality.outFile)] : []
5762
+ path15.dirname(spec.outFile),
5763
+ ...spec.quality ? [path15.dirname(spec.quality.outFile)] : []
5391
5764
  ]))];
5392
5765
  const unwritable = [];
5393
5766
  for (const dir of dirs) {
@@ -5410,7 +5783,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
5410
5783
  for (let index = 0; index < entry.outputs.length; index++) {
5411
5784
  const output = entry.outputs[index];
5412
5785
  const spec = specByKey.get(key);
5413
- const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, path14.dirname(lockPath));
5786
+ const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, path15.dirname(lockPath));
5414
5787
  const owner = outputOwners.get(absolute);
5415
5788
  if (owner && owner !== key) duplicateOutputs.push(`${output.path} (${owner}, ${key})`);
5416
5789
  outputOwners.set(absolute, key);
@@ -5425,7 +5798,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
5425
5798
  } else {
5426
5799
  add("lock-outputs", "ok", `${outputOwners.size} recorded output path(s) are uniquely owned`);
5427
5800
  }
5428
- const cacheDir = path14.join(path14.dirname(lockPath), ".pixelkiln", "cache");
5801
+ const cacheDir = path15.join(path15.dirname(lockPath), ".pixelkiln", "cache");
5429
5802
  const stranded = [];
5430
5803
  const invalidState = [];
5431
5804
  const staleMapUrls = [];
@@ -5441,11 +5814,11 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
5441
5814
  const hasSource = Boolean(entry.sourceUrl || entry.sourceUrls?.length);
5442
5815
  let hasCache = false;
5443
5816
  for (const output of entry.outputs) {
5444
- const cached = path14.join(
5817
+ const cached = path15.join(
5445
5818
  cacheDir,
5446
5819
  cacheFileName(output.sha256, output.mediaType ?? MediaType.PNG)
5447
5820
  );
5448
- if (existsSync9(cached) && await sha256File(cached) === output.sha256) {
5821
+ if (existsSync10(cached) && await sha256File(cached) === output.sha256) {
5449
5822
  hasCache = true;
5450
5823
  break;
5451
5824
  }
@@ -5533,20 +5906,20 @@ async function nearestExistingDirectory(start) {
5533
5906
  if ((await stat2(current)).isDirectory()) return current;
5534
5907
  } catch {
5535
5908
  }
5536
- const parent = path14.dirname(current);
5909
+ const parent = path15.dirname(current);
5537
5910
  if (parent === current) return current;
5538
5911
  current = parent;
5539
5912
  }
5540
5913
  }
5541
5914
 
5542
5915
  // src/pipeline/adopt.ts
5543
- import { existsSync as existsSync11 } from "fs";
5916
+ import { existsSync as existsSync12 } from "fs";
5544
5917
  import { readFile as readFile9, writeFile as writeFile5 } from "fs/promises";
5545
- import path15 from "path";
5918
+ import path16 from "path";
5546
5919
 
5547
5920
  // src/cache.ts
5548
5921
  import { mkdir as mkdir4, readFile as readFile8, rename as rename4, rm as rm5, writeFile as writeFile4 } from "fs/promises";
5549
- import { existsSync as existsSync10 } from "fs";
5922
+ import { existsSync as existsSync11 } from "fs";
5550
5923
  import pathModule from "path";
5551
5924
  import { z as z3 } from "zod";
5552
5925
  var HashCacheSchema = z3.object({
@@ -5561,10 +5934,10 @@ function isSha256Hash(value) {
5561
5934
  function parseCache(value) {
5562
5935
  return HashCacheSchema.parse(value);
5563
5936
  }
5564
- async function loadCache(path24) {
5565
- if (!existsSync10(path24)) return { version: 1, hashes: {} };
5937
+ async function loadCache(path25) {
5938
+ if (!existsSync11(path25)) return { version: 1, hashes: {} };
5566
5939
  try {
5567
- const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile8(path24, "utf8")));
5940
+ const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile8(path25, "utf8")));
5568
5941
  if (!parsed.success) return { version: 1, hashes: {} };
5569
5942
  return {
5570
5943
  version: 1,
@@ -5576,18 +5949,18 @@ async function loadCache(path24) {
5576
5949
  return { version: 1, hashes: {} };
5577
5950
  }
5578
5951
  }
5579
- async function saveCache(path24, cache) {
5952
+ async function saveCache(path25, cache) {
5580
5953
  const sorted = {};
5581
5954
  for (const key of Object.keys(cache.hashes).sort()) {
5582
5955
  const hash = cache.hashes[key];
5583
5956
  if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
5584
5957
  sorted[key] = hash;
5585
5958
  }
5586
- await mkdir4(pathModule.dirname(pathModule.resolve(path24)), { recursive: true });
5587
- const tmp = `${path24}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
5959
+ await mkdir4(pathModule.dirname(pathModule.resolve(path25)), { recursive: true });
5960
+ const tmp = `${path25}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
5588
5961
  try {
5589
5962
  await writeFile4(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
5590
- await rename4(tmp, path24);
5963
+ await rename4(tmp, path25);
5591
5964
  } finally {
5592
5965
  await rm5(tmp, { force: true });
5593
5966
  }
@@ -5618,7 +5991,7 @@ async function adopt(provider, specs, lock, lockPath, opts = {}) {
5618
5991
  const localByHash = /* @__PURE__ */ new Map();
5619
5992
  const unmatchedLocal = [];
5620
5993
  for (const spec of specs) {
5621
- if (!existsSync11(spec.outFile)) {
5994
+ if (!existsSync12(spec.outFile)) {
5622
5995
  unmatchedLocal.push(`${spec.styleId}/${spec.assetId} (no file at ${spec.outFile})`);
5623
5996
  continue;
5624
5997
  }
@@ -5781,7 +6154,13 @@ function renderSheet(groups) {
5781
6154
  ...g,
5782
6155
  styleId: escapeHtml(g.styleId),
5783
6156
  assetId: escapeHtml(g.assetId),
5784
- prompt: escapeHtml(g.prompt)
6157
+ prompt: escapeHtml(g.prompt),
6158
+ ...g.revision ? {
6159
+ revision: {
6160
+ ...g.revision,
6161
+ sourceAssetId: escapeHtml(g.revision.sourceAssetId)
6162
+ }
6163
+ } : {}
5785
6164
  }));
5786
6165
  const data = JSON.stringify(safe).replace(/<\//g, "<\\/");
5787
6166
  return `<!doctype html>
@@ -5822,6 +6201,18 @@ function renderSheet(groups) {
5822
6201
  color:var(--accent-soft); border:1px solid var(--accent); border-radius:0; padding:2px 6px; }
5823
6202
  .aid { font-weight:650; font-family:ui-monospace,SFMono-Regular,Menlo,monospace; }
5824
6203
  .prompt { flex:1; min-width:280px; color:var(--dim); font-size:12.5px; max-width:80ch; }
6204
+ .review { display:grid; grid-template-columns:minmax(180px,280px) minmax(0,1fr); gap:16px;
6205
+ align-items:start; }
6206
+ .review.no-context { display:block; }
6207
+ .context { border:1px solid var(--line); padding:8px; background:var(--panel-deep);
6208
+ min-width:0; }
6209
+ .context-label { display:flex; justify-content:space-between; gap:8px; margin-bottom:7px;
6210
+ color:var(--dim); font-size:10.5px; font-variant-numeric:tabular-nums; }
6211
+ .context img { display:block; width:100%; height:auto; max-height:420px; object-fit:contain;
6212
+ image-rendering:pixelated;
6213
+ background-image:linear-gradient(45deg,#0000 25%,#7f7f7f22 25%,#7f7f7f22 75%,#0000 75%),
6214
+ linear-gradient(45deg,#0000 25%,#7f7f7f22 25%,#7f7f7f22 75%,#0000 75%);
6215
+ background-size:12px 12px; background-position:0 0,6px 6px; }
5825
6216
  .frames { display:flex; flex-wrap:wrap; gap:10px; max-width:100%; overflow-x:auto; }
5826
6217
  .cand { border:2px solid var(--line-strong); border-radius:0; padding:7px; background:var(--panel-deep);
5827
6218
  cursor:pointer; display:flex; flex:0 0 auto; flex-direction:column; align-items:center; gap:5px;
@@ -5848,6 +6239,10 @@ function renderSheet(groups) {
5848
6239
  .prompt { min-width:100%; }
5849
6240
  .cand img.preview { max-width:calc(100vw - 50px); }
5850
6241
  }
6242
+ @media (max-width:900px) {
6243
+ .review { grid-template-columns:1fr; }
6244
+ .context { max-width:280px; }
6245
+ }
5851
6246
  </style>
5852
6247
  </head>
5853
6248
  <body>
@@ -5890,8 +6285,26 @@ GROUPS.forEach((g, gi) => {
5890
6285
  '<div class="ghead"><span class="style">' + g.styleId + '</span>' +
5891
6286
  '<span class="aid">' + g.assetId + '</span>' +
5892
6287
  '<span class="prompt">' + g.prompt + '</span></div>' +
5893
- '<div class="frames"></div>';
6288
+ '<div class="review' + (g.revision ? '' : ' no-context') + '">' +
6289
+ '<div class="frames"></div></div>';
6290
+ const review = el.querySelector('.review');
5894
6291
  const frames = el.querySelector('.frames');
6292
+ if (g.revision) {
6293
+ const context = document.createElement('aside');
6294
+ context.className = 'context';
6295
+ const label = document.createElement('div');
6296
+ label.className = 'context-label';
6297
+ const identity = document.createElement('span');
6298
+ identity.textContent = g.revision.mode.toUpperCase() + ' \xB7 SOURCE ' + g.revision.sourceAssetId;
6299
+ const dimensions = document.createElement('span');
6300
+ dimensions.textContent = g.revision.width + '\xD7' + g.revision.height;
6301
+ label.append(identity, dimensions);
6302
+ const source = document.createElement('img');
6303
+ source.src = g.revision.sourceUrl;
6304
+ source.alt = 'Revision source ' + g.revision.sourceAssetId;
6305
+ context.append(label, source);
6306
+ review.insertBefore(context, frames);
6307
+ }
5895
6308
  g.frameUrls.forEach((url, i) => {
5896
6309
  const c = document.createElement('button');
5897
6310
  c.type = 'button';
@@ -6028,11 +6441,28 @@ document.getElementById('submit').onclick = async () => {
6028
6441
  // src/pick/review-server.ts
6029
6442
  import { createServer } from "http";
6030
6443
  import { spawn } from "child_process";
6444
+ import { readFile as readFile10 } from "fs/promises";
6031
6445
  function serveReviewPage(opts) {
6032
6446
  const log2 = opts.onProgress ?? (() => {
6033
6447
  });
6034
6448
  return new Promise((resolve, reject) => {
6035
6449
  const server = createServer(async (req, res) => {
6450
+ if (req.method === "GET" && req.url && opts.assets?.has(req.url)) {
6451
+ try {
6452
+ const asset = opts.assets.get(req.url);
6453
+ const bytes = await readFile10(asset.path);
6454
+ res.writeHead(200, {
6455
+ "Content-Type": asset.contentType,
6456
+ "Cache-Control": "no-store",
6457
+ "X-Content-Type-Options": "nosniff"
6458
+ });
6459
+ res.end(bytes);
6460
+ } catch {
6461
+ res.writeHead(404, { "Content-Type": "text/plain" });
6462
+ res.end("review asset is unavailable");
6463
+ }
6464
+ return;
6465
+ }
6036
6466
  if (req.method === "GET" && (req.url === "/" || req.url?.startsWith("/?"))) {
6037
6467
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
6038
6468
  res.end(opts.html);
@@ -6096,13 +6526,25 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
6096
6526
  const log2 = opts.onProgress ?? (() => {
6097
6527
  });
6098
6528
  const selectedKeys = opts.keys ? new Set(opts.keys) : null;
6529
+ const specByKey = new Map(
6530
+ (opts.specs ?? []).map((spec) => [lockKey(spec.styleId, spec.assetId), spec])
6531
+ );
6532
+ const reviewAssets = /* @__PURE__ */ new Map();
6099
6533
  const groups = [];
6100
6534
  for (const [key, entry] of Object.entries(lock.entries)) {
6101
6535
  if (entry.provider !== provider.id || selectedKeys && !selectedKeys.has(key)) continue;
6102
6536
  if (entry.status !== "review" || !entry.reviewObjectId) continue;
6103
6537
  try {
6104
- const state = await provider.poll(entry.reviewObjectId, entry.generator);
6538
+ const spec = specByKey.get(key);
6539
+ const state = await provider.poll(entry.reviewObjectId, entry.generator, { spec });
6105
6540
  if (state.status !== "review" || !state.candidateUrls.length) continue;
6541
+ const sourceRoute = spec?.revision ? `/revision-source/${encodeURIComponent(String(groups.length))}` : null;
6542
+ if (sourceRoute && spec?.revision) {
6543
+ reviewAssets.set(sourceRoute, {
6544
+ path: spec.revision.sourceFile,
6545
+ contentType: spec.revision.sourceFormat === "jpeg" ? "image/jpeg" : "image/png"
6546
+ });
6547
+ }
6106
6548
  groups.push({
6107
6549
  key,
6108
6550
  assetId: entry.assetId,
@@ -6111,7 +6553,16 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
6111
6553
  reviewObjectId: entry.reviewObjectId,
6112
6554
  frameUrls: state.candidateUrls,
6113
6555
  width: entry.width,
6114
- height: entry.height
6556
+ height: entry.height,
6557
+ ...sourceRoute && spec?.revision && spec.revision.sourceWidth && spec.revision.sourceHeight ? {
6558
+ revision: {
6559
+ mode: spec.revision.mode,
6560
+ sourceAssetId: spec.revision.sourceAssetId,
6561
+ sourceUrl: sourceRoute,
6562
+ width: spec.revision.sourceWidth,
6563
+ height: spec.revision.sourceHeight
6564
+ }
6565
+ } : {}
6115
6566
  });
6116
6567
  } catch (err) {
6117
6568
  log2(` could not load candidates for ${key}: ${err instanceof Error ? err.message : String(err)}`);
@@ -6125,6 +6576,7 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
6125
6576
  port: opts.port,
6126
6577
  open: opts.open,
6127
6578
  onProgress: log2,
6579
+ assets: reviewAssets,
6128
6580
  onReady: (url) => {
6129
6581
  log2(`
6130
6582
  ${groups.length} asset(s) awaiting selection: ${url}`);
@@ -6170,9 +6622,9 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
6170
6622
  }
6171
6623
 
6172
6624
  // src/pipeline/init.ts
6173
- import { readdir, readFile as readFile10, writeFile as writeFile6 } from "fs/promises";
6174
- import { existsSync as existsSync12 } from "fs";
6175
- import path16 from "path";
6625
+ import { readdir, readFile as readFile11, writeFile as writeFile6 } from "fs/promises";
6626
+ import { existsSync as existsSync13 } from "fs";
6627
+ import path17 from "path";
6176
6628
  function pngSize(buf) {
6177
6629
  if (buf.length < 24) return null;
6178
6630
  const isPng = buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834;
@@ -6186,7 +6638,7 @@ function slugify(value) {
6186
6638
  async function walk(dir, exclude) {
6187
6639
  const out = [];
6188
6640
  for (const entry of await readdir(dir, { withFileTypes: true })) {
6189
- const full = path16.join(dir, entry.name);
6641
+ const full = path17.join(dir, entry.name);
6190
6642
  if (exclude.some((x) => full.includes(x))) continue;
6191
6643
  if (entry.isDirectory()) out.push(...await walk(full, exclude));
6192
6644
  else if (entry.name.toLowerCase().endsWith(".png")) out.push(full);
@@ -6200,14 +6652,14 @@ async function scanAssets(root, opts = {}) {
6200
6652
  const skipped = [];
6201
6653
  const seen = /* @__PURE__ */ new Map();
6202
6654
  for (const file of files.sort()) {
6203
- const size = pngSize(await readFile10(file));
6655
+ const size = pngSize(await readFile11(file));
6204
6656
  if (!size) {
6205
6657
  skipped.push(`${file} (not a readable PNG)`);
6206
6658
  continue;
6207
6659
  }
6208
- const rel = path16.relative(root, file);
6209
- const category = path16.dirname(rel) === "." ? "" : path16.dirname(rel);
6210
- let id = slugify(path16.basename(rel));
6660
+ const rel = path17.relative(root, file);
6661
+ const category = path17.dirname(rel) === "." ? "" : path17.dirname(rel);
6662
+ let id = slugify(path17.basename(rel));
6211
6663
  if (seen.has(id)) {
6212
6664
  const n = seen.get(id) + 1;
6213
6665
  seen.set(id, n);
@@ -6262,26 +6714,26 @@ function mode(values) {
6262
6714
  return [...counts.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? 64;
6263
6715
  }
6264
6716
  async function writeManifestFile(target, manifest) {
6265
- if (existsSync12(target)) {
6717
+ if (existsSync13(target)) {
6266
6718
  throw new Error(`${target} already exists \u2014 refusing to overwrite an existing manifest.`);
6267
6719
  }
6268
6720
  await writeFile6(target, JSON.stringify(manifest, null, 2) + "\n");
6269
6721
  }
6270
6722
 
6271
6723
  // src/pipeline/salvage.ts
6272
- import { readFile as readFile11 } from "fs/promises";
6273
- import { existsSync as existsSync13 } from "fs";
6274
- import path17 from "path";
6724
+ import { readFile as readFile12 } from "fs/promises";
6725
+ import { existsSync as existsSync14 } from "fs";
6726
+ import path18 from "path";
6275
6727
  function providerClaimId(provider, objectId) {
6276
6728
  return `${encodeURIComponent(provider)}:${encodeURIComponent(objectId)}`;
6277
6729
  }
6278
6730
  async function loadClaims(lockPaths, opts = {}) {
6279
6731
  const claimed = /* @__PURE__ */ new Set();
6280
6732
  for (const p of lockPaths) {
6281
- if (!existsSync13(p)) throw new Error(`Claim lockfile not found: ${p}`);
6733
+ if (!existsSync14(p)) throw new Error(`Claim lockfile not found: ${p}`);
6282
6734
  let parsed;
6283
6735
  try {
6284
- parsed = parseLock(JSON.parse(await readFile11(p, "utf8")));
6736
+ parsed = parseLock(JSON.parse(await readFile12(p, "utf8")));
6285
6737
  } catch {
6286
6738
  throw new Error(`Claim lockfile is malformed: ${p}`);
6287
6739
  }
@@ -6329,20 +6781,20 @@ function matchStyleByPattern(prompt, manifest) {
6329
6781
  return null;
6330
6782
  }
6331
6783
  async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, claimPaths) {
6332
- const own = path17.resolve(ownManifestPath);
6784
+ const own = path18.resolve(ownManifestPath);
6333
6785
  const siblingManifestPaths = [
6334
6786
  .../* @__PURE__ */ new Set([
6335
6787
  ...workspaceManifestPaths,
6336
- ...claimPaths.map((c) => path17.join(path17.dirname(path17.resolve(c)), "pixelkiln.manifest.json"))
6788
+ ...claimPaths.map((c) => path18.join(path18.dirname(path18.resolve(c)), "pixelkiln.manifest.json"))
6337
6789
  ])
6338
6790
  ];
6339
6791
  const siblings = [];
6340
6792
  for (const siblingManifestPath of siblingManifestPaths) {
6341
- if (path17.resolve(siblingManifestPath) === own) continue;
6342
- if (!existsSync13(siblingManifestPath)) continue;
6793
+ if (path18.resolve(siblingManifestPath) === own) continue;
6794
+ if (!existsSync14(siblingManifestPath)) continue;
6343
6795
  try {
6344
6796
  const { manifest } = await loadManifest(siblingManifestPath);
6345
- siblings.push({ label: path17.basename(path17.dirname(siblingManifestPath)), manifest });
6797
+ siblings.push({ label: path18.basename(path18.dirname(siblingManifestPath)), manifest });
6346
6798
  } catch {
6347
6799
  }
6348
6800
  }
@@ -6458,9 +6910,9 @@ async function applyTags(provider, decisions, existing, opts = {}) {
6458
6910
  }
6459
6911
 
6460
6912
  // src/workspace.ts
6461
- import { mkdir as mkdir5, readFile as readFile12, rename as rename5, rm as rm6, writeFile as writeFile7 } from "fs/promises";
6462
- import { existsSync as existsSync14 } from "fs";
6463
- import path18 from "path";
6913
+ import { mkdir as mkdir5, readFile as readFile13, rename as rename5, rm as rm6, writeFile as writeFile7 } from "fs/promises";
6914
+ import { existsSync as existsSync15 } from "fs";
6915
+ import path19 from "path";
6464
6916
  import { z as z4 } from "zod";
6465
6917
  var WorkspaceProjectSchema = z4.object({
6466
6918
  id: z4.string().min(1),
@@ -6485,10 +6937,10 @@ ${parsed.error.issues.slice(0, 5).map((i) => ` ${i.path.join(".")}: ${i.message
6485
6937
  );
6486
6938
  }
6487
6939
  async function loadWorkspace(workspacePath) {
6488
- if (!existsSync14(workspacePath)) return { version: 1, projects: [] };
6940
+ if (!existsSync15(workspacePath)) return { version: 1, projects: [] };
6489
6941
  let raw;
6490
6942
  try {
6491
- raw = JSON.parse(await readFile12(workspacePath, "utf8"));
6943
+ raw = JSON.parse(await readFile13(workspacePath, "utf8"));
6492
6944
  } catch (err) {
6493
6945
  throw new Error(
6494
6946
  `Workspace catalog at ${workspacePath} is malformed:
@@ -6502,7 +6954,7 @@ async function saveWorkspace(workspacePath, ws) {
6502
6954
  version: 1,
6503
6955
  projects: [...ws.projects].sort((a, b) => a.id.localeCompare(b.id))
6504
6956
  };
6505
- await mkdir5(path18.dirname(path18.resolve(workspacePath)), { recursive: true });
6957
+ await mkdir5(path19.dirname(path19.resolve(workspacePath)), { recursive: true });
6506
6958
  const tmp = `${workspacePath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
6507
6959
  try {
6508
6960
  await writeFile7(tmp, JSON.stringify(sorted, null, 2) + "\n");
@@ -6512,12 +6964,12 @@ async function saveWorkspace(workspacePath, ws) {
6512
6964
  }
6513
6965
  }
6514
6966
  function toPortablePath(dir, absolute) {
6515
- return path18.relative(dir, absolute).split(path18.sep).join("/");
6967
+ return path19.relative(dir, absolute).split(path19.sep).join("/");
6516
6968
  }
6517
6969
  function resolveProject(dir, project) {
6518
6970
  return {
6519
- manifestPath: path18.resolve(dir, project.manifest.split("/").join(path18.sep)),
6520
- lockPath: path18.resolve(dir, project.lock.split("/").join(path18.sep))
6971
+ manifestPath: path19.resolve(dir, project.manifest.split("/").join(path19.sep)),
6972
+ lockPath: path19.resolve(dir, project.lock.split("/").join(path19.sep))
6521
6973
  };
6522
6974
  }
6523
6975
  function validateWorkspace(ws, dir) {
@@ -6530,21 +6982,21 @@ function validateWorkspace(ws, dir) {
6530
6982
  const { manifestPath, lockPath } = resolveProject(dir, project);
6531
6983
  lockOwners.set(lockPath, [...lockOwners.get(lockPath) ?? [], project.id]);
6532
6984
  manifestOwners.set(manifestPath, [...manifestOwners.get(manifestPath) ?? [], project.id]);
6533
- if (path18.isAbsolute(project.manifest) || path18.isAbsolute(project.lock)) {
6985
+ if (path19.isAbsolute(project.manifest) || path19.isAbsolute(project.lock)) {
6534
6986
  diagnostics.push({
6535
6987
  id: "absolute-path",
6536
6988
  level: "warning",
6537
6989
  message: `project "${project.id}" stores an absolute path \u2014 the catalog will not resolve correctly if this tree is cloned or moved elsewhere`
6538
6990
  });
6539
6991
  }
6540
- if (!existsSync14(manifestPath)) {
6992
+ if (!existsSync15(manifestPath)) {
6541
6993
  diagnostics.push({
6542
6994
  id: "missing-manifest",
6543
6995
  level: "error",
6544
6996
  message: `project "${project.id}" manifest not found: ${manifestPath}`
6545
6997
  });
6546
6998
  }
6547
- if (!existsSync14(lockPath)) {
6999
+ if (!existsSync15(lockPath)) {
6548
7000
  diagnostics.push({
6549
7001
  id: "missing-lock",
6550
7002
  level: "error",
@@ -6620,6 +7072,7 @@ function emptyStateCounts() {
6620
7072
  orphaned: 0,
6621
7073
  "in-flight": 0,
6622
7074
  recoverable: 0,
7075
+ blocked: 0,
6623
7076
  failed: 0
6624
7077
  };
6625
7078
  }
@@ -6693,15 +7146,15 @@ async function workspaceStatus(ws, dir) {
6693
7146
  }
6694
7147
 
6695
7148
  // src/pipeline/cache-health.ts
6696
- import { existsSync as existsSync15 } from "fs";
6697
- import { readFile as readFile13, readdir as readdir2, rm as rm7 } from "fs/promises";
6698
- import path19 from "path";
7149
+ import { existsSync as existsSync16 } from "fs";
7150
+ import { readFile as readFile14, readdir as readdir2, rm as rm7 } from "fs/promises";
7151
+ import path20 from "path";
6699
7152
  async function inspectCaches(lock, lockPath, options = {}) {
6700
- if (options.prune && !existsSync15(lockPath)) {
6701
- throw new Error(`Refusing to prune without an existing lockfile at ${path19.resolve(lockPath)}`);
7153
+ if (options.prune && !existsSync16(lockPath)) {
7154
+ throw new Error(`Refusing to prune without an existing lockfile at ${path20.resolve(lockPath)}`);
6702
7155
  }
6703
- const contentDir = path19.resolve(path19.dirname(lockPath), ".pixelkiln", "cache");
6704
- const remotePath = path19.resolve(cachePathFor(lockPath));
7156
+ const contentDir = path20.resolve(path20.dirname(lockPath), ".pixelkiln", "cache");
7157
+ const remotePath = path20.resolve(cachePathFor(lockPath));
6705
7158
  const referenced = new Set(
6706
7159
  Object.values(lock.entries).flatMap((entry) => entry.outputs.map((output) => output.sha256))
6707
7160
  );
@@ -6715,7 +7168,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
6715
7168
  ]);
6716
7169
  for (const name of names) {
6717
7170
  try {
6718
- await rm7(path19.join(contentDir, name), { force: true });
7171
+ await rm7(path20.join(contentDir, name), { force: true });
6719
7172
  removed.contentFiles++;
6720
7173
  } catch {
6721
7174
  }
@@ -6724,7 +7177,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
6724
7177
  await saveCache(remotePath, { version: 1, hashes: {} });
6725
7178
  removed.resetRemoteHashCache = true;
6726
7179
  } else if (remoteHashes.invalidIds.length) {
6727
- const cache = parseCache(JSON.parse(await readFile13(remotePath, "utf8")));
7180
+ const cache = parseCache(JSON.parse(await readFile14(remotePath, "utf8")));
6728
7181
  for (const id of remoteHashes.invalidIds) delete cache.hashes[id];
6729
7182
  removed.remoteHashEntries = remoteHashes.invalidIds.length;
6730
7183
  await saveCache(remotePath, cache);
@@ -6742,7 +7195,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
6742
7195
  async function inspectContentCache(contentDir, referenced) {
6743
7196
  const report = {
6744
7197
  path: contentDir,
6745
- exists: existsSync15(contentDir),
7198
+ exists: existsSync16(contentDir),
6746
7199
  files: 0,
6747
7200
  bytes: 0,
6748
7201
  valid: 0,
@@ -6763,12 +7216,12 @@ async function inspectContentCache(contentDir, referenced) {
6763
7216
  continue;
6764
7217
  }
6765
7218
  report.files++;
6766
- const file = path19.join(contentDir, entry.name);
7219
+ const file = path20.join(contentDir, entry.name);
6767
7220
  const mediaType = mediaTypeFromExtension(entry.name);
6768
7221
  const expected = mediaType ? entry.name.slice(0, -4) : "";
6769
7222
  let bytes;
6770
7223
  try {
6771
- bytes = await readFile13(file);
7224
+ bytes = await readFile14(file);
6772
7225
  report.bytes += bytes.length;
6773
7226
  } catch (err) {
6774
7227
  report.invalid.push({
@@ -6806,7 +7259,7 @@ async function inspectContentCache(contentDir, referenced) {
6806
7259
  async function inspectRemoteHashCache(remotePath) {
6807
7260
  const report = {
6808
7261
  path: remotePath,
6809
- exists: existsSync15(remotePath),
7262
+ exists: existsSync16(remotePath),
6810
7263
  entries: 0,
6811
7264
  valid: 0,
6812
7265
  invalidIds: [],
@@ -6815,7 +7268,7 @@ async function inspectRemoteHashCache(remotePath) {
6815
7268
  if (!report.exists) return report;
6816
7269
  let cache;
6817
7270
  try {
6818
- cache = parseCache(JSON.parse(await readFile13(remotePath, "utf8")));
7271
+ cache = parseCache(JSON.parse(await readFile14(remotePath, "utf8")));
6819
7272
  } catch (err) {
6820
7273
  report.error = err instanceof Error ? err.message : String(err);
6821
7274
  return report;
@@ -7086,8 +7539,8 @@ function isRecord2(value) {
7086
7539
  }
7087
7540
 
7088
7541
  // src/pick/salvage-server.ts
7089
- import { mkdir as mkdir6, writeFile as writeFile8, readFile as readFile14 } from "fs/promises";
7090
- import path20 from "path";
7542
+ import { mkdir as mkdir6, writeFile as writeFile8, readFile as readFile15 } from "fs/promises";
7543
+ import path21 from "path";
7091
7544
 
7092
7545
  // src/pick/salvage-sheet.ts
7093
7546
  var escapeHtml2 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
@@ -7252,7 +7705,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
7252
7705
  });
7253
7706
  const html = renderSalvageSheet(orphans, {
7254
7707
  styleId: ctx.styleId,
7255
- importDir: path20.relative(process.cwd(), ctx.importDir) || "."
7708
+ importDir: path21.relative(process.cwd(), ctx.importDir) || "."
7256
7709
  });
7257
7710
  const byId = new Map(orphans.map((o) => [o.id, o]));
7258
7711
  const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
@@ -7281,9 +7734,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
7281
7734
  if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
7282
7735
  decodePng(buf);
7283
7736
  const assetId = idFromPrompt(orphan.prompt, taken);
7284
- const rel = path20.join("_salvaged", `${assetId}.png`);
7285
- const outFile = path20.resolve(ctx.importDir, rel);
7286
- await mkdir6(path20.dirname(outFile), { recursive: true });
7737
+ const rel = path21.join("_salvaged", `${assetId}.png`);
7738
+ const outFile = path21.resolve(ctx.importDir, rel);
7739
+ await mkdir6(path21.dirname(outFile), { recursive: true });
7287
7740
  await writeFile8(outFile, buf);
7288
7741
  ctx.manifest.assets[assetId] = {
7289
7742
  prompt: orphan.prompt,
@@ -7314,7 +7767,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
7314
7767
  sourceUrl: durableSource,
7315
7768
  sourceUrls: durableSource ? [{ url: durableSource }] : [],
7316
7769
  outputs: [{
7317
- path: portableOutputPath(outFile, path20.dirname(ctx.manifestPath)),
7770
+ path: portableOutputPath(outFile, path21.dirname(ctx.manifestPath)),
7318
7771
  sha256: sha256(buf)
7319
7772
  }],
7320
7773
  submittedAt: orphan.createdAt,
@@ -7338,7 +7791,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
7338
7791
  }
7339
7792
  }
7340
7793
  await applyTags(provider, decisions, existingTags, { onProgress: log2 });
7341
- const raw = JSON.parse(await readFile14(ctx.manifestPath, "utf8"));
7794
+ const raw = JSON.parse(await readFile15(ctx.manifestPath, "utf8"));
7342
7795
  for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
7343
7796
  await writeFile8(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
7344
7797
  await saveLock(ctx.lockPath, ctx.lock);
@@ -7348,9 +7801,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
7348
7801
  }
7349
7802
 
7350
7803
  // src/recipes.ts
7351
- import path21 from "path";
7352
- import { existsSync as existsSync16 } from "fs";
7353
- import { readdir as readdir3, readFile as readFile15 } from "fs/promises";
7804
+ import path22 from "path";
7805
+ import { existsSync as existsSync17 } from "fs";
7806
+ import { readdir as readdir3, readFile as readFile16 } from "fs/promises";
7354
7807
  import { fileURLToPath } from "url";
7355
7808
  import { z as z5 } from "zod";
7356
7809
  var SHA256_RE = /^[0-9a-f]{64}$/;
@@ -7359,7 +7812,7 @@ var VERSION_RE = /^\d+\.\d+\.\d+$/;
7359
7812
  var RECIPE_FILE = "pixelkiln.recipe.json";
7360
7813
  var RECIPE_DIR_TOKEN = "{{recipeDir}}";
7361
7814
  var PortablePathSchema = z5.string().min(1).refine((value) => {
7362
- if (value.includes("\\") || path21.posix.isAbsolute(value)) return false;
7815
+ if (value.includes("\\") || path22.posix.isAbsolute(value)) return false;
7363
7816
  const parts = value.split("/");
7364
7817
  return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
7365
7818
  }, "expected a portable relative path without '.', '..', or backslashes");
@@ -7394,10 +7847,13 @@ var RecipeSchema = z5.object({
7394
7847
  numImages: z5.number().int().min(1).max(16),
7395
7848
  bindings: z5.object({
7396
7849
  prompt: RecipeBindingSchema,
7397
- width: RecipeBindingSchema,
7398
- height: RecipeBindingSchema,
7399
- batchSize: RecipeBindingSchema,
7400
- seed: RecipeBindingSchema.optional()
7850
+ width: RecipeBindingSchema.optional(),
7851
+ height: RecipeBindingSchema.optional(),
7852
+ batchSize: RecipeBindingSchema.optional(),
7853
+ seed: RecipeBindingSchema.optional(),
7854
+ sourceImage: RecipeBindingSchema.optional(),
7855
+ maskImage: RecipeBindingSchema.optional(),
7856
+ strength: RecipeBindingSchema.optional()
7401
7857
  }).strict()
7402
7858
  }).strict().optional(),
7403
7859
  quality: z5.object({
@@ -7532,10 +7988,10 @@ function message3(error) {
7532
7988
  return error instanceof Error ? error.message : String(error);
7533
7989
  }
7534
7990
  async function readRecipeFile(recipePath, bundled) {
7535
- const absolute = path21.resolve(recipePath);
7991
+ const absolute = path22.resolve(recipePath);
7536
7992
  let raw;
7537
7993
  try {
7538
- raw = JSON.parse(await readFile15(absolute, "utf8"));
7994
+ raw = JSON.parse(await readFile16(absolute, "utf8"));
7539
7995
  } catch (error) {
7540
7996
  throw new Error(`Could not read recipe ${absolute}: ${message3(error)}`, { cause: error });
7541
7997
  }
@@ -7546,13 +8002,13 @@ async function readRecipeFile(recipePath, bundled) {
7546
8002
  ` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
7547
8003
  );
7548
8004
  }
7549
- return { recipe: parsed.data, path: absolute, dir: path21.dirname(absolute), bundled };
8005
+ return { recipe: parsed.data, path: absolute, dir: path22.dirname(absolute), bundled };
7550
8006
  }
7551
8007
  async function walkRecipeFiles(root) {
7552
- if (!existsSync16(root)) return [];
8008
+ if (!existsSync17(root)) return [];
7553
8009
  const out = [];
7554
8010
  for (const entry of await readdir3(root, { withFileTypes: true })) {
7555
- const resolved = path21.join(root, entry.name);
8011
+ const resolved = path22.join(root, entry.name);
7556
8012
  if (entry.isDirectory()) out.push(...await walkRecipeFiles(resolved));
7557
8013
  else if (entry.isFile() && entry.name === RECIPE_FILE) out.push(resolved);
7558
8014
  }
@@ -7563,7 +8019,7 @@ function bundledRecipeRoot() {
7563
8019
  }
7564
8020
  async function listBundledRecipes(root = bundledRecipeRoot()) {
7565
8021
  const recipes = await Promise.all(
7566
- (await walkRecipeFiles(path21.resolve(root))).map((file) => readRecipeFile(file, true))
8022
+ (await walkRecipeFiles(path22.resolve(root))).map((file) => readRecipeFile(file, true))
7567
8023
  );
7568
8024
  return recipes.sort(
7569
8025
  (a, b) => a.recipe.id.localeCompare(b.recipe.id) || compareVersions(b.recipe.version, a.recipe.version)
@@ -7586,9 +8042,9 @@ function parseSelector(selector) {
7586
8042
  return { id: match[1], version: match[2] };
7587
8043
  }
7588
8044
  async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
7589
- const local = path21.resolve(target);
7590
- if (existsSync16(local)) {
7591
- const recipePath = path21.basename(local) === RECIPE_FILE ? local : path21.join(local, RECIPE_FILE);
8045
+ const local = path22.resolve(target);
8046
+ if (existsSync17(local)) {
8047
+ const recipePath = path22.basename(local) === RECIPE_FILE ? local : path22.join(local, RECIPE_FILE);
7592
8048
  return readRecipeFile(recipePath, false);
7593
8049
  }
7594
8050
  if (target.startsWith(".") || target.startsWith("/") || target.includes("\\")) {
@@ -7602,8 +8058,8 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
7602
8058
  return matches[0];
7603
8059
  }
7604
8060
  async function verifyFile(root, file) {
7605
- const absolute = path21.resolve(root, ...file.path.split("/"));
7606
- if (!existsSync16(absolute)) {
8061
+ const absolute = path22.resolve(root, ...file.path.split("/"));
8062
+ if (!existsSync17(absolute)) {
7607
8063
  return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
7608
8064
  }
7609
8065
  const actualSha256 = await sha256File(absolute);
@@ -7618,7 +8074,7 @@ async function verifyRecipe(target, options = {}) {
7618
8074
  const loaded = await resolveRecipe(target, options.bundledRoot);
7619
8075
  const actualIntegrity = recipeDigest(loaded.recipe);
7620
8076
  const files = await Promise.all(loaded.recipe.files.map((file) => verifyFile(loaded.dir, file)));
7621
- const modelRoot = options.modelRoot ? path21.resolve(options.modelRoot) : null;
8077
+ const modelRoot = options.modelRoot ? path22.resolve(options.modelRoot) : null;
7622
8078
  const models = modelRoot ? await Promise.all(loaded.recipe.models.map(async (model) => ({
7623
8079
  ...await verifyFile(modelRoot, model),
7624
8080
  source: model.source,
@@ -7652,7 +8108,7 @@ async function verifyRecipe(target, options = {}) {
7652
8108
  };
7653
8109
  }
7654
8110
  function renderStyle(style, destination, cwd) {
7655
- const portableDir = path21.relative(cwd, destination).split(path21.sep).join("/") || ".";
8111
+ const portableDir = path22.relative(cwd, destination).split(path22.sep).join("/") || ".";
7656
8112
  const replace = (value) => {
7657
8113
  if (typeof value === "string") return value.replaceAll(RECIPE_DIR_TOKEN, portableDir);
7658
8114
  if (Array.isArray(value)) return value.map(replace);
@@ -7664,25 +8120,25 @@ function renderStyle(style, destination, cwd) {
7664
8120
  return replace(style);
7665
8121
  }
7666
8122
  async function installRecipe(target, options = {}) {
7667
- const cwd = path21.resolve(options.cwd ?? process.cwd());
8123
+ const cwd = path22.resolve(options.cwd ?? process.cwd());
7668
8124
  const loaded = await resolveRecipe(target, options.bundledRoot);
7669
8125
  const verification = await verifyRecipe(loaded.path);
7670
8126
  if (!verification.ok) {
7671
8127
  throw new Error(`Recipe ${loaded.recipe.id}@${loaded.recipe.version} failed integrity verification.`);
7672
8128
  }
7673
- const destination = options.out ? path21.resolve(cwd, options.out) : path21.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
8129
+ const destination = options.out ? path22.resolve(cwd, options.out) : path22.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
7674
8130
  const sources = [
7675
8131
  { path: RECIPE_FILE, source: loaded.path },
7676
- ...loaded.recipe.files.map((file) => ({ path: file.path, source: path21.join(loaded.dir, ...file.path.split("/")) }))
8132
+ ...loaded.recipe.files.map((file) => ({ path: file.path, source: path22.join(loaded.dir, ...file.path.split("/")) }))
7677
8133
  ];
7678
8134
  const files = await Promise.all(sources.map(async (file) => ({
7679
- path: path21.join(destination, ...file.path.split("/")),
7680
- data: await readFile15(file.source)
8135
+ path: path22.join(destination, ...file.path.split("/")),
8136
+ data: await readFile16(file.source)
7681
8137
  })));
7682
8138
  if (!options.force) {
7683
8139
  for (const file of files) {
7684
- if (!existsSync16(file.path)) continue;
7685
- const current = await readFile15(file.path);
8140
+ if (!existsSync17(file.path)) continue;
8141
+ const current = await readFile16(file.path);
7686
8142
  if (!current.equals(file.data)) {
7687
8143
  throw new Error(
7688
8144
  `Recipe destination has local changes: ${file.path}. Choose another --out or pass --force to replace declared recipe files.`
@@ -7705,15 +8161,15 @@ async function installRecipe(target, options = {}) {
7705
8161
  }
7706
8162
 
7707
8163
  // src/pipeline/quality-regression.ts
7708
- import path22 from "path";
7709
- import { existsSync as existsSync17 } from "fs";
7710
- import { readFile as readFile16 } from "fs/promises";
8164
+ import path23 from "path";
8165
+ import { existsSync as existsSync18 } from "fs";
8166
+ import { readFile as readFile17 } from "fs/promises";
7711
8167
  import { z as z6 } from "zod";
7712
8168
  var SHA256_RE2 = /^[0-9a-f]{64}$/;
7713
8169
  var HEX_RE = /^#[0-9a-f]{6}$/;
7714
8170
  var MAX_REDMEAN_DISTANCE = 765;
7715
8171
  var QualityPathSchema = z6.string().min(1).refine(
7716
- (value) => !path22.posix.isAbsolute(value) && !value.includes("\\"),
8172
+ (value) => !path23.posix.isAbsolute(value) && !value.includes("\\"),
7717
8173
  "expected a portable relative path"
7718
8174
  );
7719
8175
  var DEFAULT_QUALITY_TOLERANCES = {
@@ -7860,10 +8316,10 @@ function measureDecodedImageQuality(png, digest2) {
7860
8316
  };
7861
8317
  }
7862
8318
  async function measureImageQuality(file) {
7863
- const absolute = path22.resolve(file);
8319
+ const absolute = path23.resolve(file);
7864
8320
  let bytes;
7865
8321
  try {
7866
- bytes = await readFile16(absolute);
8322
+ bytes = await readFile17(absolute);
7867
8323
  } catch (error) {
7868
8324
  throw new Error(`Cannot read quality image ${absolute}: ${message4(error)}`, { cause: error });
7869
8325
  }
@@ -7882,11 +8338,11 @@ function resolveQualityInputs(raw, inputsFilePath) {
7882
8338
  }
7883
8339
  const ids = parsed.data.map((entry) => entry.id);
7884
8340
  if (new Set(ids).size !== ids.length) throw new Error("--inputs contains a duplicate quality case id.");
7885
- const root = path22.dirname(path22.resolve(inputsFilePath));
8341
+ const root = path23.dirname(path23.resolve(inputsFilePath));
7886
8342
  return parsed.data.map((entry) => ({
7887
8343
  id: entry.id,
7888
- file: path22.resolve(root, entry.path),
7889
- ...entry.record ? { record: path22.resolve(root, entry.record) } : {},
8344
+ file: path23.resolve(root, entry.path),
8345
+ ...entry.record ? { record: path23.resolve(root, entry.record) } : {},
7890
8346
  tolerances: QualityToleranceSchema.parse({
7891
8347
  ...DEFAULT_QUALITY_TOLERANCES,
7892
8348
  ...entry.tolerances
@@ -7894,7 +8350,7 @@ function resolveQualityInputs(raw, inputsFilePath) {
7894
8350
  }));
7895
8351
  }
7896
8352
  function portableRelative2(from, to) {
7897
- return path22.relative(from, path22.resolve(to)).split(path22.sep).join("/") || ".";
8353
+ return path23.relative(from, path23.resolve(to)).split(path23.sep).join("/") || ".";
7898
8354
  }
7899
8355
  async function verifyRecordForImage(recordPath, imagePath) {
7900
8356
  const verification = await verifyArtifactBundle(recordPath);
@@ -7909,20 +8365,20 @@ async function verifyRecordForImage(recordPath, imagePath) {
7909
8365
  if (record.kind !== "refine" || options === null || typeof options !== "object" || options.schema !== "pixelkiln-quality") {
7910
8366
  throw new Error(`Quality record ${recordPath} is not a PixelKiln refinement record.`);
7911
8367
  }
7912
- const outputs = record.outputs.map((output) => path22.resolve(path22.dirname(recordPath), output.path));
7913
- if (!outputs.includes(path22.resolve(imagePath))) {
8368
+ const outputs = record.outputs.map((output) => path23.resolve(path23.dirname(recordPath), output.path));
8369
+ if (!outputs.includes(path23.resolve(imagePath))) {
7914
8370
  throw new Error(`Quality record ${recordPath} does not own ${imagePath}.`);
7915
8371
  }
7916
8372
  }
7917
8373
  async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
7918
8374
  if (!inputs.length) throw new Error("Cannot snapshot an empty quality baseline.");
7919
- const absolute = path22.resolve(baselinePath);
8375
+ const absolute = path23.resolve(baselinePath);
7920
8376
  for (const input of inputs) {
7921
- if (path22.resolve(input.file) === absolute || input.record && path22.resolve(input.record) === absolute) {
8377
+ if (path23.resolve(input.file) === absolute || input.record && path23.resolve(input.record) === absolute) {
7922
8378
  throw new Error(`Quality baseline output would overwrite an input: ${absolute}.`);
7923
8379
  }
7924
8380
  }
7925
- const root = path22.dirname(absolute);
8381
+ const root = path23.dirname(absolute);
7926
8382
  const cases = [];
7927
8383
  for (const input of [...inputs].sort((a, b) => a.id.localeCompare(b.id))) {
7928
8384
  const expected = await measureImageQuality(input.file);
@@ -7949,8 +8405,8 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
7949
8405
  cases
7950
8406
  });
7951
8407
  const data = Buffer.from(JSON.stringify(baseline, null, 2) + "\n");
7952
- if (existsSync17(absolute) && !options.force) {
7953
- const current = await readFile16(absolute);
8408
+ if (existsSync18(absolute) && !options.force) {
8409
+ const current = await readFile17(absolute);
7954
8410
  if (!current.equals(data)) {
7955
8411
  throw new Error(`Quality baseline already exists with different content: ${absolute}. Pass --force to replace it.`);
7956
8412
  }
@@ -7959,10 +8415,10 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
7959
8415
  return { baseline, path: absolute, changed: result.changed.length > 0 };
7960
8416
  }
7961
8417
  async function readQualityBaseline(baselinePath) {
7962
- const absolute = path22.resolve(baselinePath);
8418
+ const absolute = path23.resolve(baselinePath);
7963
8419
  let raw;
7964
8420
  try {
7965
- raw = JSON.parse(await readFile16(absolute, "utf8"));
8421
+ raw = JSON.parse(await readFile17(absolute, "utf8"));
7966
8422
  } catch (error) {
7967
8423
  throw new Error(`Could not read quality baseline ${absolute}: ${message4(error)}`, { cause: error });
7968
8424
  }
@@ -8032,12 +8488,12 @@ function regressionViolations(expected, actual, tolerances) {
8032
8488
  return { violations, warnings };
8033
8489
  }
8034
8490
  async function checkQualityBaseline(baselinePath) {
8035
- const absolute = path22.resolve(baselinePath);
8491
+ const absolute = path23.resolve(baselinePath);
8036
8492
  const baseline = await readQualityBaseline(absolute);
8037
- const root = path22.dirname(absolute);
8493
+ const root = path23.dirname(absolute);
8038
8494
  const cases = [];
8039
8495
  for (const entry of baseline.cases) {
8040
- const file = path22.resolve(root, entry.file);
8496
+ const file = path23.resolve(root, entry.file);
8041
8497
  let actual = null;
8042
8498
  const violations = [];
8043
8499
  const warnings = [];
@@ -8050,7 +8506,7 @@ async function checkQualityBaseline(baselinePath) {
8050
8506
  violations.push(message4(error));
8051
8507
  }
8052
8508
  if (entry.record) {
8053
- const recordPath = path22.resolve(root, entry.record.path);
8509
+ const recordPath = path23.resolve(root, entry.record.path);
8054
8510
  try {
8055
8511
  const recordHash = await sha256File(recordPath);
8056
8512
  if (recordHash !== entry.record.sha256) violations.push("quality record hash changed");
@@ -8088,11 +8544,11 @@ async function checkQualityBaseline(baselinePath) {
8088
8544
  // src/cli.ts
8089
8545
  var log = (msg = "") => console.log(msg);
8090
8546
  async function provenanceFile(id, file) {
8091
- const absolute = path23.resolve(file);
8547
+ const absolute = path24.resolve(file);
8092
8548
  return {
8093
8549
  id,
8094
8550
  path: absolute,
8095
- sha256: existsSync18(absolute) ? await sha256File(absolute) : null,
8551
+ sha256: existsSync19(absolute) ? await sha256File(absolute) : null,
8096
8552
  included: true
8097
8553
  };
8098
8554
  }
@@ -8349,7 +8805,7 @@ function parseArgs(argv) {
8349
8805
  return {
8350
8806
  command,
8351
8807
  manifest,
8352
- lock: get("--lock") ?? path23.join(path23.dirname(path23.resolve(manifest)), "pixelkiln.lock.json"),
8808
+ lock: get("--lock") ?? path24.join(path24.dirname(path24.resolve(manifest)), "pixelkiln.lock.json"),
8353
8809
  explicitLock: get("--lock"),
8354
8810
  styles: list("--style"),
8355
8811
  assets: list("--only"),
@@ -8497,7 +8953,17 @@ Examples
8497
8953
  `;
8498
8954
  function printPlan(plan) {
8499
8955
  const counts = summarize(plan);
8500
- const order = ["missing", "untracked", "stale", "failed", "recoverable", "in-flight", "orphaned", "ok"];
8956
+ const order = [
8957
+ "blocked",
8958
+ "missing",
8959
+ "untracked",
8960
+ "stale",
8961
+ "failed",
8962
+ "recoverable",
8963
+ "in-flight",
8964
+ "orphaned",
8965
+ "ok"
8966
+ ];
8501
8967
  for (const state of order) {
8502
8968
  const items = plan.items.filter((i) => i.state === state);
8503
8969
  if (!items.length) continue;
@@ -8510,7 +8976,7 @@ function printPlan(plan) {
8510
8976
  }
8511
8977
  log(
8512
8978
  `
8513
- totals: ${counts.ok} ok \xB7 ${counts.missing} missing \xB7 ${counts.untracked} untracked \xB7 ${counts.stale} stale \xB7 ${counts["in-flight"]} in-flight \xB7 ${counts.orphaned} orphaned \xB7 ${counts.recoverable} recoverable \xB7 ${counts.failed} failed`
8979
+ totals: ${counts.ok} ok \xB7 ${counts.blocked} blocked \xB7 ${counts.missing} missing \xB7 ${counts.untracked} untracked \xB7 ${counts.stale} stale \xB7 ${counts["in-flight"]} in-flight \xB7 ${counts.orphaned} orphaned \xB7 ${counts.recoverable} recoverable \xB7 ${counts.failed} failed`
8514
8980
  );
8515
8981
  if (plan.actionable.length) {
8516
8982
  for (const group of plan.groups) {
@@ -8604,10 +9070,10 @@ async function confirm(question, auto) {
8604
9070
  return answer === "y" || answer === "yes";
8605
9071
  }
8606
9072
  async function requireCompleteWorkspaceClaims(workspacePath) {
8607
- if (!existsSync18(workspacePath)) {
9073
+ if (!existsSync19(workspacePath)) {
8608
9074
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
8609
9075
  }
8610
- const dir = path23.dirname(path23.resolve(workspacePath));
9076
+ const dir = path24.dirname(path24.resolve(workspacePath));
8611
9077
  const ws = await loadWorkspace(workspacePath);
8612
9078
  const diagnostics = validateWorkspace(ws, dir);
8613
9079
  const errors = diagnostics.filter((d) => d.level === "error");
@@ -8628,7 +9094,7 @@ async function main() {
8628
9094
  }
8629
9095
  if (args.command === "--version" || args.command === "-v") {
8630
9096
  const pkg = JSON.parse(
8631
- await readFile17(new URL("../package.json", import.meta.url), "utf8")
9097
+ await readFile18(new URL("../package.json", import.meta.url), "utf8")
8632
9098
  );
8633
9099
  log(`${pkg.name} ${pkg.version}`);
8634
9100
  return;
@@ -8639,15 +9105,15 @@ async function main() {
8639
9105
  if (!args.out) throw new Error("quality snapshot needs --out <pixelkiln.quality.json>.");
8640
9106
  let raw;
8641
9107
  try {
8642
- raw = JSON.parse(await readFile17(path23.resolve(args.inputs), "utf8"));
9108
+ raw = JSON.parse(await readFile18(path24.resolve(args.inputs), "utf8"));
8643
9109
  } catch (error) {
8644
9110
  throw new Error(
8645
- `Could not read quality inputs ${path23.resolve(args.inputs)}: ${error instanceof Error ? error.message : String(error)}`,
9111
+ `Could not read quality inputs ${path24.resolve(args.inputs)}: ${error instanceof Error ? error.message : String(error)}`,
8646
9112
  { cause: error }
8647
9113
  );
8648
9114
  }
8649
9115
  const inputs = resolveQualityInputs(raw, args.inputs);
8650
- if (path23.resolve(args.inputs) === path23.resolve(args.out)) {
9116
+ if (path24.resolve(args.inputs) === path24.resolve(args.out)) {
8651
9117
  throw new Error("quality snapshot --out must not overwrite its --inputs file.");
8652
9118
  }
8653
9119
  const result = await snapshotQualityBaseline(inputs, args.out, { force: args.force });
@@ -8660,7 +9126,7 @@ async function main() {
8660
9126
  }, null, 2));
8661
9127
  } else {
8662
9128
  log(
8663
- ` ${result.changed ? "wrote" : "unchanged"} ${path23.relative(process.cwd(), result.path)} (${result.baseline.cases.length} case(s))`
9129
+ ` ${result.changed ? "wrote" : "unchanged"} ${path24.relative(process.cwd(), result.path)} (${result.baseline.cases.length} case(s))`
8664
9130
  );
8665
9131
  log(` Review the tolerances, commit the baseline, then run quality check in CI.`);
8666
9132
  }
@@ -8755,14 +9221,14 @@ async function main() {
8755
9221
  }, null, 2));
8756
9222
  } else {
8757
9223
  log(` installed ${result.recipe.id}@${result.recipe.version}`);
8758
- log(` ${path23.relative(process.cwd(), result.destination) || "."}`);
9224
+ log(` ${path24.relative(process.cwd(), result.destination) || "."}`);
8759
9225
  log(`
8760
9226
  Add this entry under your manifest's styles object:`);
8761
9227
  log(JSON.stringify({ [result.styleId]: result.style }, null, 2));
8762
9228
  if (result.recipe.models.length) {
8763
9229
  log(`
8764
9230
  Models are not downloaded automatically. Verify them with:`);
8765
- log(` pixelkiln recipe verify ${path23.relative(process.cwd(), result.destination)} --model-root <ComfyUI/models>`);
9231
+ log(` pixelkiln recipe verify ${path24.relative(process.cwd(), result.destination)} --model-root <ComfyUI/models>`);
8766
9232
  }
8767
9233
  }
8768
9234
  return;
@@ -8796,12 +9262,12 @@ async function main() {
8796
9262
  ` recovered ${result2.detection.columns}x${result2.detection.rows} native grid (${result2.detection.confidence}: ${result2.detection.consensus})`
8797
9263
  );
8798
9264
  log(` enforced ${result2.palette.length}-color palette without dithering`);
8799
- log(` wrote ${path23.relative(process.cwd(), result2.output)}`);
8800
- log(` quality record: ${path23.relative(process.cwd(), result2.record)}`);
9265
+ log(` wrote ${path24.relative(process.cwd(), result2.output)}`);
9266
+ log(` quality record: ${path24.relative(process.cwd(), result2.record)}`);
8801
9267
  log(`
8802
9268
  Automated checks passed. Human 1\xD7 review is still required:`);
8803
9269
  log(
8804
- ` pixelkiln refine approve --from ${path23.relative(process.cwd(), result2.record)} --reviewer "Your Name"`
9270
+ ` pixelkiln refine approve --from ${path24.relative(process.cwd(), result2.record)} --reviewer "Your Name"`
8805
9271
  );
8806
9272
  }
8807
9273
  return;
@@ -8821,21 +9287,21 @@ async function main() {
8821
9287
  note: args.note
8822
9288
  });
8823
9289
  if (args.json) log(JSON.stringify(result2, null, 2));
8824
- else log(` approved ${path23.relative(process.cwd(), result2.output)} by ${args.reviewer.trim()}`);
9290
+ else log(` approved ${path24.relative(process.cwd(), result2.output)} by ${args.reviewer.trim()}`);
8825
9291
  return;
8826
9292
  }
8827
9293
  const result = await checkQualityRecord(args.from);
8828
9294
  if (args.json) {
8829
9295
  log(JSON.stringify(result, null, 2));
8830
9296
  } else {
8831
- log(` ${result.safe ? "release-ready" : "not release-ready"}: ${path23.relative(process.cwd(), result.output || result.record)}`);
9297
+ log(` ${result.safe ? "release-ready" : "not release-ready"}: ${path24.relative(process.cwd(), result.output || result.record)}`);
8832
9298
  for (const reason of result.reasons) log(` ${reason}`);
8833
9299
  }
8834
9300
  if (!result.safe) process.exitCode = 1;
8835
9301
  return;
8836
9302
  }
8837
9303
  if (args.command === "balance") {
8838
- loadEnvFiles(path23.dirname(path23.resolve(args.manifest)));
9304
+ loadEnvFiles(path24.dirname(path24.resolve(args.manifest)));
8839
9305
  loadEnvFiles(process.cwd());
8840
9306
  const loaded2 = await loadManifest(args.manifest);
8841
9307
  const selectedProvider = accountProviderId(loaded2.manifest, args.provider, "balance");
@@ -8848,31 +9314,31 @@ async function main() {
8848
9314
  }
8849
9315
  if (args.command === "init") {
8850
9316
  if (!args.from) throw new Error("init needs --from <dir> pointing at your existing PNGs.");
8851
- const root = path23.resolve(args.from);
8852
- if (!existsSync18(root)) throw new Error(`No directory at ${root}`);
9317
+ const root = path24.resolve(args.from);
9318
+ if (!existsSync19(root)) throw new Error(`No directory at ${root}`);
8853
9319
  const generator = args.generator ?? "map";
8854
9320
  if (generator !== "1dir" && generator !== "map") {
8855
9321
  throw new Error(`--generator must be "1dir" or "map", got "${args.generator}".`);
8856
9322
  }
8857
- const target = path23.resolve(args.out ?? "pixelkiln.manifest.json");
9323
+ const target = path24.resolve(args.out ?? "pixelkiln.manifest.json");
8858
9324
  const { assets, skipped } = await scanAssets(root, { exclude: args.exclude });
8859
9325
  if (!assets.length) throw new Error(`No PNGs found under ${root}`);
8860
9326
  const manifest = buildManifest(
8861
- args.name ?? path23.basename(path23.dirname(target)),
9327
+ args.name ?? path24.basename(path24.dirname(target)),
8862
9328
  args.styles[0] ?? "base",
8863
9329
  generator,
8864
- path23.relative(path23.dirname(target), root) || ".",
9330
+ path24.relative(path24.dirname(target), root) || ".",
8865
9331
  assets
8866
9332
  );
8867
9333
  await writeManifestFile(target, manifest);
8868
- log(` scanned ${assets.length} PNG(s) under ${path23.relative(process.cwd(), root)}`);
9334
+ log(` scanned ${assets.length} PNG(s) under ${path24.relative(process.cwd(), root)}`);
8869
9335
  if (skipped.length) log(` skipped ${skipped.length} unreadable file(s)`);
8870
- log(` wrote ${path23.relative(process.cwd(), target)}`);
9336
+ log(` wrote ${path24.relative(process.cwd(), target)}`);
8871
9337
  log(`
8872
9338
  Prompts are intentionally empty. To recover the real ones from your`);
8873
9339
  log(` PixelLab account instead of inventing them:`);
8874
9340
  log(`
8875
- pixelkiln adopt --manifest ${path23.relative(process.cwd(), target)} --write-prompts
9341
+ pixelkiln adopt --manifest ${path24.relative(process.cwd(), target)} --write-prompts
8876
9342
  `);
8877
9343
  return;
8878
9344
  }
@@ -8920,10 +9386,10 @@ async function main() {
8920
9386
  if (args.primaryOnly || args.outputRoles.length) {
8921
9387
  throw new Error("--primary-only and --output-role require manifest-driven pack");
8922
9388
  }
8923
- const raw = JSON.parse(await readFile17(path23.resolve(args.inputs), "utf8"));
9389
+ const raw = JSON.parse(await readFile18(path24.resolve(args.inputs), "utf8"));
8924
9390
  const inputs = resolvePackInputs(raw, args.inputs);
8925
9391
  const { png, atlas, skipped, sources } = packSprites(inputs, { columns: args.columns });
8926
- const base = path23.resolve(args.out.replace(/\.png$/, ""));
9392
+ const base = path24.resolve(args.out.replace(/\.png$/, ""));
8927
9393
  const outputs = [
8928
9394
  { path: `${base}.png`, data: png },
8929
9395
  { path: `${base}.json`, data: JSON.stringify(atlas, null, 2) + "\n" }
@@ -8936,17 +9402,17 @@ async function main() {
8936
9402
  log(
8937
9403
  ` ${atlas.frames.length} sprite(s), ${atlas.sheet.width}x${atlas.sheet.height} in ${atlas.columns} column(s) \u2014 ${(png.length / 1024).toFixed(1)} KB`
8938
9404
  );
8939
- log(` ${path23.relative(process.cwd(), base)}.png + .json + .pixelkiln.json`);
9405
+ log(` ${path24.relative(process.cwd(), base)}.png + .json + .pixelkiln.json`);
8940
9406
  for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
8941
9407
  return;
8942
9408
  }
8943
9409
  if (args.command === "workspace") {
8944
- const workspacePath = path23.resolve(args.workspace ?? "pixelkiln.workspace.json");
8945
- const dir = path23.dirname(workspacePath);
9410
+ const workspacePath = path24.resolve(args.workspace ?? "pixelkiln.workspace.json");
9411
+ const dir = path24.dirname(workspacePath);
8946
9412
  if (args.subcommand === "add") {
8947
- const manifestPath = path23.resolve(args.target);
9413
+ const manifestPath = path24.resolve(args.target);
8948
9414
  const loadedTarget = await loadManifest(manifestPath);
8949
- const lockPath = args.explicitLock ? path23.resolve(args.explicitLock) : path23.join(path23.dirname(manifestPath), "pixelkiln.lock.json");
9415
+ const lockPath = args.explicitLock ? path24.resolve(args.explicitLock) : path24.join(path24.dirname(manifestPath), "pixelkiln.lock.json");
8950
9416
  const ws = await loadWorkspace(workspacePath);
8951
9417
  const id = args.name ?? loadedTarget.manifest.name;
8952
9418
  if (ws.projects.some((p) => p.id === id)) {
@@ -8966,10 +9432,10 @@ async function main() {
8966
9432
  ...args.account ? { account: args.account } : {}
8967
9433
  };
8968
9434
  await saveWorkspace(workspacePath, { version: 1, projects: [...ws.projects, project] });
8969
- log(` registered "${id}" in ${path23.relative(process.cwd(), workspacePath)}`);
9435
+ log(` registered "${id}" in ${path24.relative(process.cwd(), workspacePath)}`);
8970
9436
  log(` manifest: ${project.manifest}`);
8971
9437
  log(` lock: ${project.lock}`);
8972
- if (!existsSync18(lockPath)) {
9438
+ if (!existsSync19(lockPath)) {
8973
9439
  log(
8974
9440
  ` warning: no lockfile there yet \u2014 this project contributes no claims until one is generated`
8975
9441
  );
@@ -8978,7 +9444,7 @@ async function main() {
8978
9444
  }
8979
9445
  if (args.subcommand === "remove") {
8980
9446
  const ws = await loadWorkspace(workspacePath);
8981
- const resolvedTarget = path23.resolve(args.target);
9447
+ const resolvedTarget = path24.resolve(args.target);
8982
9448
  const match = ws.projects.find(
8983
9449
  (p) => p.id === args.target || resolveProject(dir, p).manifestPath === resolvedTarget
8984
9450
  );
@@ -8989,11 +9455,11 @@ async function main() {
8989
9455
  version: 1,
8990
9456
  projects: ws.projects.filter((p) => p !== match)
8991
9457
  });
8992
- log(` removed "${match.id}" from ${path23.relative(process.cwd(), workspacePath)}`);
9458
+ log(` removed "${match.id}" from ${path24.relative(process.cwd(), workspacePath)}`);
8993
9459
  return;
8994
9460
  }
8995
9461
  if (args.subcommand === "list") {
8996
- if (!existsSync18(workspacePath)) {
9462
+ if (!existsSync19(workspacePath)) {
8997
9463
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
8998
9464
  }
8999
9465
  const ws = await loadWorkspace(workspacePath);
@@ -9001,9 +9467,9 @@ async function main() {
9001
9467
  if (args.json) {
9002
9468
  log(JSON.stringify({ version: 1, workspace: workspacePath, projects: ws.projects, diagnostics }, null, 2));
9003
9469
  } else if (!ws.projects.length) {
9004
- log(` no projects registered in ${path23.relative(process.cwd(), workspacePath)}`);
9470
+ log(` no projects registered in ${path24.relative(process.cwd(), workspacePath)}`);
9005
9471
  } else {
9006
- log(` ${ws.projects.length} project(s) in ${path23.relative(process.cwd(), workspacePath)}:`);
9472
+ log(` ${ws.projects.length} project(s) in ${path24.relative(process.cwd(), workspacePath)}:`);
9007
9473
  for (const p of ws.projects) {
9008
9474
  log(` ${p.id.padEnd(24)} ${p.manifest.padEnd(40)} (${p.provider}${p.account ? `, ${p.account}` : ""})`);
9009
9475
  }
@@ -9013,7 +9479,7 @@ async function main() {
9013
9479
  return;
9014
9480
  }
9015
9481
  if (args.subcommand === "status") {
9016
- if (!existsSync18(workspacePath)) {
9482
+ if (!existsSync19(workspacePath)) {
9017
9483
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
9018
9484
  }
9019
9485
  const ws = await loadWorkspace(workspacePath);
@@ -9021,7 +9487,7 @@ async function main() {
9021
9487
  if (args.json) {
9022
9488
  log(JSON.stringify({ ...report, workspace: workspacePath }, null, 2));
9023
9489
  } else {
9024
- log(` workspace: ${path23.relative(process.cwd(), workspacePath)}`);
9490
+ log(` workspace: ${path24.relative(process.cwd(), workspacePath)}`);
9025
9491
  for (const p of report.projects) {
9026
9492
  if (p.error) {
9027
9493
  log(`
@@ -9067,14 +9533,14 @@ async function main() {
9067
9533
  return;
9068
9534
  }
9069
9535
  }
9070
- if (!existsSync18(path23.resolve(args.manifest))) {
9536
+ if (!existsSync19(path24.resolve(args.manifest))) {
9071
9537
  throw new Error(
9072
- `No manifest at ${path23.resolve(args.manifest)}. Pass --manifest, or run \`pixelkiln init --from <dir>\`.`
9538
+ `No manifest at ${path24.resolve(args.manifest)}. Pass --manifest, or run \`pixelkiln init --from <dir>\`.`
9073
9539
  );
9074
9540
  }
9075
- const manifestDir = path23.dirname(path23.resolve(args.manifest));
9541
+ const manifestDir = path24.dirname(path24.resolve(args.manifest));
9076
9542
  const envFiles = [...loadEnvFiles(manifestDir)];
9077
- if (path23.resolve(process.cwd()) !== manifestDir) envFiles.push(...loadEnvFiles(process.cwd()));
9543
+ if (path24.resolve(process.cwd()) !== manifestDir) envFiles.push(...loadEnvFiles(process.cwd()));
9078
9544
  const loaded = await loadManifest(args.manifest);
9079
9545
  let specs = await resolveSpecs(loaded, {
9080
9546
  styles: args.styles,
@@ -9167,7 +9633,7 @@ async function main() {
9167
9633
  log(` ${item.state.padEnd(16)} ${item.key} ${item.reason}`);
9168
9634
  if (item.state === "needs-approval") {
9169
9635
  log(
9170
- ` pixelkiln refine approve --from ${path23.relative(process.cwd(), item.record)} --reviewer "Your Name"`
9636
+ ` pixelkiln refine approve --from ${path24.relative(process.cwd(), item.record)} --reviewer "Your Name"`
9171
9637
  );
9172
9638
  }
9173
9639
  }
@@ -9225,10 +9691,19 @@ async function main() {
9225
9691
  actionable: group.actionable.map((item) => item.key)
9226
9692
  })),
9227
9693
  actionable: plan.actionable.map((i) => i.key),
9228
- items: plan.items.map(({ key, state, reason, quality }) => ({
9694
+ items: plan.items.map(({ key, state, reason, quality, spec }) => ({
9229
9695
  key,
9230
9696
  state,
9231
9697
  reason,
9698
+ ...spec.revision ? {
9699
+ revision: {
9700
+ mode: spec.revision.mode,
9701
+ from: spec.revision.sourceAssetId,
9702
+ sourceSha256: spec.revision.sourceSha256,
9703
+ ...spec.revision.maskSha256 ? { maskSha256: spec.revision.maskSha256 } : {},
9704
+ ...spec.revision.strength == null ? {} : { strength: spec.revision.strength }
9705
+ }
9706
+ } : {},
9232
9707
  ...quality ? { quality: { state: quality.state, reason: quality.reason } } : {}
9233
9708
  }))
9234
9709
  }, null, 2));
@@ -9249,7 +9724,7 @@ async function main() {
9249
9724
  let intact = true;
9250
9725
  for (const output of entry.outputs) {
9251
9726
  const file = resolveOutputPath(output.path, item.spec.root);
9252
- if (!existsSync18(file) || await sha256File(file) !== output.sha256) {
9727
+ if (!existsSync19(file) || await sha256File(file) !== output.sha256) {
9253
9728
  intact = false;
9254
9729
  break;
9255
9730
  }
@@ -9307,7 +9782,7 @@ async function main() {
9307
9782
  if (args.primaryOnly && args.outputRoles.length) {
9308
9783
  throw new Error("pack accepts either --primary-only or --output-role, not both");
9309
9784
  }
9310
- const manifestDir2 = path23.dirname(path23.resolve(args.manifest));
9785
+ const manifestDir2 = path24.dirname(path24.resolve(args.manifest));
9311
9786
  const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
9312
9787
  for (const styleId of styleIds) {
9313
9788
  const packagingSpecs = await resolveSpecs(loaded, { styles: [styleId] });
@@ -9319,7 +9794,7 @@ async function main() {
9319
9794
  sourceOverrides: qualitySources
9320
9795
  });
9321
9796
  const style = loaded.manifest.styles[styleId];
9322
- const base = args.out ? path23.resolve(args.out.replace(/\.png$/, "")) : path23.resolve(manifestDir2, style.outDir, `${styleId}-sheet`);
9797
+ const base = args.out ? path24.resolve(args.out.replace(/\.png$/, "")) : path24.resolve(manifestDir2, style.outDir, `${styleId}-sheet`);
9323
9798
  const outputs = [
9324
9799
  { path: `${base}.png`, data: png },
9325
9800
  { path: `${base}.json`, data: JSON.stringify(atlas, null, 2) + "\n" }
@@ -9346,13 +9821,13 @@ async function main() {
9346
9821
  log(
9347
9822
  ` ${styleId} \u2014 ${atlas.frames.length} sprite(s), ${atlas.sheet.width}x${atlas.sheet.height} in ${atlas.columns} column(s)`
9348
9823
  );
9349
- log(` ${path23.relative(process.cwd(), base)}.png + .json + .pixelkiln.json`);
9824
+ log(` ${path24.relative(process.cwd(), base)}.png + .json + .pixelkiln.json`);
9350
9825
  for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
9351
9826
  }
9352
9827
  return;
9353
9828
  }
9354
9829
  if (args.command === "mount") {
9355
- const manifestDir2 = path23.dirname(path23.resolve(args.manifest));
9830
+ const manifestDir2 = path24.dirname(path24.resolve(args.manifest));
9356
9831
  const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
9357
9832
  for (const styleId of styleIds) {
9358
9833
  const style = loaded.manifest.styles[styleId];
@@ -9389,7 +9864,7 @@ async function main() {
9389
9864
  sources,
9390
9865
  outputRoles
9391
9866
  );
9392
- const out = path23.resolve(manifestDir2, style.mount.out);
9867
+ const out = path24.resolve(manifestDir2, style.mount.out);
9393
9868
  const metadata = out.replace(/\.png$/, "") + ".json";
9394
9869
  const companion = out.replace(/\.png$/, "") + ".pixelkiln.json";
9395
9870
  const outputs = [
@@ -9406,7 +9881,7 @@ async function main() {
9406
9881
  await provenanceFile("$lock", args.lock),
9407
9882
  ...qualityRecords,
9408
9883
  ...artifactSources.filter(
9409
- (source) => source.id !== "$base" || path23.resolve(source.path) !== out
9884
+ (source) => source.id !== "$base" || path24.resolve(source.path) !== out
9410
9885
  )
9411
9886
  ],
9412
9887
  options: {
@@ -9419,7 +9894,7 @@ async function main() {
9419
9894
  log(
9420
9895
  ` ${styleId} \u2014 ${atlas.frames.length} cell(s) into ${atlas.sheet.width}x${atlas.sheet.height}` + (overBase ? ` over ${style.mount.base}` : " (new sheet)")
9421
9896
  );
9422
- log(` ${path23.relative(process.cwd(), out)} + atlas/provenance JSON`);
9897
+ log(` ${path24.relative(process.cwd(), out)} + atlas/provenance JSON`);
9423
9898
  for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
9424
9899
  }
9425
9900
  return;
@@ -9475,7 +9950,7 @@ async function main() {
9475
9950
  }
9476
9951
  if (args.command === "export") {
9477
9952
  const format = args.format ?? "generic";
9478
- const manifestDir2 = path23.dirname(path23.resolve(args.manifest));
9953
+ const manifestDir2 = path24.dirname(path24.resolve(args.manifest));
9479
9954
  const selected = specs.filter((spec) => {
9480
9955
  if (spec.generator !== "tiles") return false;
9481
9956
  if (args.styles.length && !args.styles.includes(spec.styleId)) return false;
@@ -9491,12 +9966,12 @@ async function main() {
9491
9966
  for (const spec of selected) {
9492
9967
  const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
9493
9968
  const style = loaded.manifest.styles[spec.styleId];
9494
- const defaultBase = path23.resolve(manifestDir2, style.outDir, `${spec.assetId}-tileset`);
9495
- const base = args.out ? path23.resolve(args.out.replace(/\.(?:png|json|tsj|tres)$/i, "")) : defaultBase;
9969
+ const defaultBase = path24.resolve(manifestDir2, style.outDir, `${spec.assetId}-tileset`);
9970
+ const base = args.out ? path24.resolve(args.out.replace(/\.(?:png|json|tsj|tres)$/i, "")) : defaultBase;
9496
9971
  const result = exportTileset(entry, spec, {
9497
9972
  format,
9498
9973
  manifestDir: manifestDir2,
9499
- imageName: path23.basename(`${base}.png`),
9974
+ imageName: path24.basename(`${base}.png`),
9500
9975
  columns: args.columns
9501
9976
  });
9502
9977
  const outputs = [
@@ -9514,7 +9989,7 @@ async function main() {
9514
9989
  asset: spec.assetId,
9515
9990
  columns: args.columns ?? null,
9516
9991
  format,
9517
- image: path23.basename(`${base}.png`),
9992
+ image: path24.basename(`${base}.png`),
9518
9993
  providerRules: result.generic.providerRules,
9519
9994
  style: spec.styleId,
9520
9995
  tileType: spec.tileType ?? null
@@ -9524,7 +9999,7 @@ async function main() {
9524
9999
  ` ${spec.styleId}/${spec.assetId} \u2014 ${result.generic.tiles.length} tile(s), ${result.generic.sheet.width}x${result.generic.sheet.height} (${format})`
9525
10000
  );
9526
10001
  log(
9527
- ` ${path23.relative(process.cwd(), base)}.png + ${path23.basename(base)}${result.extension} + .pixelkiln.json`
10002
+ ` ${path24.relative(process.cwd(), base)}.png + ${path24.basename(base)}${result.extension} + .pixelkiln.json`
9528
10003
  );
9529
10004
  }
9530
10005
  return;
@@ -9574,12 +10049,12 @@ async function main() {
9574
10049
  tagged ${n} object(s) upstream`);
9575
10050
  }
9576
10051
  if (args.writePrompts) {
9577
- const { filled, stillEmpty } = await writePromptsBack(path23.resolve(args.manifest), lock, {
10052
+ const { filled, stillEmpty } = await writePromptsBack(path24.resolve(args.manifest), lock, {
9578
10053
  onProgress: log,
9579
10054
  provider: provider.id,
9580
10055
  assetIds: specs.map((spec) => spec.assetId)
9581
10056
  });
9582
- log(` recovered ${filled} prompt(s) into ${path23.relative(process.cwd(), args.manifest)}`);
10057
+ log(` recovered ${filled} prompt(s) into ${path24.relative(process.cwd(), args.manifest)}`);
9583
10058
  const reloaded = await loadManifest(args.manifest);
9584
10059
  const rebased = (await resolveSpecs(reloaded, {
9585
10060
  styles: args.styles,
@@ -9608,12 +10083,12 @@ async function main() {
9608
10083
  const provider = providerFor(selectedAccountProvider);
9609
10084
  const jsonMode = args.dryRun && args.json;
9610
10085
  const diag = jsonMode ? (msg = "") => console.error(msg) : log;
9611
- const ownLock = path23.resolve(args.lock);
10086
+ const ownLock = path24.resolve(args.lock);
9612
10087
  let workspaceProjects = [];
9613
10088
  let workspaceDir = "";
9614
10089
  if (args.workspace) {
9615
- const workspacePath = path23.resolve(args.workspace);
9616
- workspaceDir = path23.dirname(workspacePath);
10090
+ const workspacePath = path24.resolve(args.workspace);
10091
+ workspaceDir = path24.dirname(workspacePath);
9617
10092
  const complete = await requireCompleteWorkspaceClaims(workspacePath);
9618
10093
  workspaceProjects = complete.ws.projects;
9619
10094
  for (const d of complete.diagnostics) diag(` WARN ${d.id}: ${d.message}`);
@@ -9622,12 +10097,12 @@ async function main() {
9622
10097
  const lockPaths = [
9623
10098
  .../* @__PURE__ */ new Set([
9624
10099
  ...workspaceLockPaths,
9625
- ...existsSync18(ownLock) ? [ownLock] : [],
9626
- ...args.claims.map((c) => path23.resolve(c))
10100
+ ...existsSync19(ownLock) ? [ownLock] : [],
10101
+ ...args.claims.map((c) => path24.resolve(c))
9627
10102
  ])
9628
10103
  ];
9629
10104
  diag(` claim set (${lockPaths.length} lockfile(s)):`);
9630
- for (const p of lockPaths) diag(` ${path23.relative(process.cwd(), p)}`);
10105
+ for (const p of lockPaths) diag(` ${path24.relative(process.cwd(), p)}`);
9631
10106
  if (!args.claims.length && !args.workspace) {
9632
10107
  diag(
9633
10108
  `
@@ -9635,7 +10110,7 @@ async function main() {
9635
10110
  pass every other project's lockfile via --claims a.json,b.json, or
9636
10111
  register every project in a workspace catalog and pass --workspace.`
9637
10112
  );
9638
- } else if (args.workspace && !workspaceProjects.some((p) => resolveProject(workspaceDir, p).manifestPath === path23.resolve(args.manifest))) {
10113
+ } else if (args.workspace && !workspaceProjects.some((p) => resolveProject(workspaceDir, p).manifestPath === path24.resolve(args.manifest))) {
9639
10114
  diag(
9640
10115
  `
9641
10116
  This project's manifest is not registered in the workspace catalog. Its own
@@ -9719,7 +10194,7 @@ async function main() {
9719
10194
  manifestPath: loaded.path,
9720
10195
  manifest: accountManifest,
9721
10196
  styleId,
9722
- importDir: path23.resolve(loaded.root, style.outDir),
10197
+ importDir: path24.resolve(loaded.root, style.outDir),
9723
10198
  lock,
9724
10199
  lockPath: args.lock
9725
10200
  },
@@ -9929,7 +10404,8 @@ async function main() {
9929
10404
  port: args.port,
9930
10405
  open: !args.noOpen,
9931
10406
  onProgress: log,
9932
- keys: providerSpecsForRun.map((spec) => lockKey(spec.styleId, spec.assetId))
10407
+ keys: providerSpecsForRun.map((spec) => lockKey(spec.styleId, spec.assetId)),
10408
+ specs: providerSpecsForRun
9933
10409
  });
9934
10410
  total.selected += res.selected;
9935
10411
  total.skipped += res.skipped;