pixelkiln 0.6.0 → 0.8.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/index.js CHANGED
@@ -40,8 +40,8 @@ var StyleSchema = z.object({
40
40
  // `map` is the default because it is 20-40x cheaper and correct for any
41
41
  // asset that is not going to be rotated or animated.
42
42
  generator: GeneratorSchema.default("map"),
43
- /** Square edge length for `1dir`. 32-256. */
44
- size: z.number().int().min(32).max(256).optional(),
43
+ /** Square edge length. The active provider owns its exact limit. */
44
+ size: z.number().int().min(16).max(8192).optional(),
45
45
  view: z.string().optional(),
46
46
  /** Appended to every asset prompt in this style. Where the look is defined. */
47
47
  promptSuffix: z.string().default(""),
@@ -157,8 +157,8 @@ var AssetSchema = z.object({
157
157
  /** Subdirectory under the style's outDir. Optional. */
158
158
  category: z.string().optional(),
159
159
  /** Overrides the style default. `map` generator only. */
160
- width: z.number().int().min(16).max(400).optional(),
161
- height: z.number().int().min(16).max(400).optional(),
160
+ width: z.number().int().min(16).max(8192).optional(),
161
+ height: z.number().int().min(16).max(8192).optional(),
162
162
  /** Overrides the style default. `1dir` generator only. */
163
163
  size: z.number().int().min(32).max(256).optional(),
164
164
  /** Explicit output path relative to outDir. Media-aware providers may replace its extension. */
@@ -416,11 +416,11 @@ var PixelLabClient = class {
416
416
  * common than the failure mode of retrying (a duplicate object), and a
417
417
  * duplicate is visible and free to delete whereas a silent gap is neither.
418
418
  */
419
- async request(path17, init, attempt = 0) {
419
+ async request(path18, init, attempt = 0) {
420
420
  const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
421
421
  let res;
422
422
  try {
423
- res = await fetch(`${BASE}${path17}`, {
423
+ res = await fetch(`${BASE}${path18}`, {
424
424
  ...init,
425
425
  signal: init?.signal ?? AbortSignal.timeout(this.timeoutMs),
426
426
  headers: {
@@ -432,24 +432,24 @@ var PixelLabClient = class {
432
432
  } catch (err) {
433
433
  if (attempt < MAX_RETRIES) {
434
434
  await sleep(backoffMs(attempt));
435
- return this.request(path17, init, attempt + 1);
435
+ return this.request(path18, init, attempt + 1);
436
436
  }
437
437
  throw err;
438
438
  }
439
439
  if (!res.ok && shouldRetry(res.status) && attempt < MAX_RETRIES) {
440
440
  const waitMs = retryAfterMs(res.headers.get("retry-after")) ?? backoffMs(attempt);
441
441
  await sleep(waitMs);
442
- return this.request(path17, init, attempt + 1);
442
+ return this.request(path18, init, attempt + 1);
443
443
  }
444
444
  const text = await res.text();
445
445
  if (!res.ok) {
446
- throw new PixelLabError(`${init?.method ?? "GET"} ${path17} \u2192 ${res.status}`, res.status, text);
446
+ throw new PixelLabError(`${init?.method ?? "GET"} ${path18} \u2192 ${res.status}`, res.status, text);
447
447
  }
448
448
  if (!text) return {};
449
449
  try {
450
450
  return JSON.parse(text);
451
451
  } catch {
452
- throw new Error(`${init?.method ?? "GET"} ${path17} returned invalid JSON`);
452
+ throw new Error(`${init?.method ?? "GET"} ${path18} returned invalid JSON`);
453
453
  }
454
454
  }
455
455
  async balance() {
@@ -1213,6 +1213,12 @@ var PixelLabProvider = class _PixelLabProvider {
1213
1213
  };
1214
1214
  }
1215
1215
  validate(spec, styleImages) {
1216
+ if (spec.generator === "1dir" && (spec.width < 32 || spec.width > 256)) {
1217
+ throw new Error("PixelLab 1dir dimensions must be between 32 and 256 pixels");
1218
+ }
1219
+ if ((spec.generator === "map" || spec.generator === "pixflux") && (spec.width < 16 || spec.height < 16 || spec.width > 400 || spec.height > 400)) {
1220
+ throw new Error(`PixelLab ${spec.generator} dimensions must be between 16 and 400 pixels`);
1221
+ }
1216
1222
  if (spec.generator === "map") {
1217
1223
  requirePixelLabOption("view", spec.view, ["low top-down", "high top-down", "side"]);
1218
1224
  requirePixelLabOption("outline", spec.outline, [
@@ -1489,9 +1495,9 @@ var RetroDiffusionClient = class {
1489
1495
  }
1490
1496
  return balance;
1491
1497
  }
1492
- async call(path17, init = {}) {
1498
+ async call(path18, init = {}) {
1493
1499
  if (!this.token) throw new Error("RD_API_KEY is not set");
1494
- const response = await this.request(`${this.baseUrl}${path17}`, {
1500
+ const response = await this.request(`${this.baseUrl}${path18}`, {
1495
1501
  ...init,
1496
1502
  headers: {
1497
1503
  "Content-Type": "application/json",
@@ -1761,8 +1767,470 @@ function retroError(value) {
1761
1767
  return JSON.stringify(value);
1762
1768
  }
1763
1769
 
1764
- // src/providers/fake.ts
1770
+ // src/providers/comfyui.ts
1771
+ import { randomUUID as randomUUID2 } from "crypto";
1772
+ import { readFile as readFile2 } from "fs/promises";
1773
+ import path2 from "path";
1774
+
1775
+ // src/hash.ts
1765
1776
  import { createHash } from "crypto";
1777
+ import { readFile } from "fs/promises";
1778
+ function sha256(data) {
1779
+ return createHash("sha256").update(data).digest("hex");
1780
+ }
1781
+ async function sha256File(path18) {
1782
+ return sha256(await readFile(path18));
1783
+ }
1784
+ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.providerOptions) {
1785
+ return sha256(
1786
+ JSON.stringify({
1787
+ // Preserve every existing PixelLab hash while making a provider switch
1788
+ // invalidate the spec. Older manifests implicitly mean pixellab.
1789
+ provider: spec.provider === "pixellab" ? void 0 : spec.provider,
1790
+ providerOptions: providerOptionIdentity && (typeof providerOptionIdentity !== "object" || Object.keys(providerOptionIdentity).length > 0) ? providerOptionIdentity : void 0,
1791
+ generator: spec.generator,
1792
+ prompt: spec.prompt,
1793
+ width: spec.width,
1794
+ height: spec.height,
1795
+ view: spec.view,
1796
+ outline: spec.outline ?? null,
1797
+ shading: spec.shading ?? null,
1798
+ detail: spec.detail ?? null,
1799
+ seed: spec.seed ?? null,
1800
+ palette: spec.palette,
1801
+ // `noBackground` only reaches the wire for pixflux; the tile fields are
1802
+ // undefined for every other generator. `tileSize` is intentionally
1803
+ // absent — width/height are derived from it, so it is already covered.
1804
+ noBackground: spec.generator === "pixflux" || spec.provider !== "pixellab" ? spec.noBackground : void 0,
1805
+ tileType: spec.tileType,
1806
+ tileView: spec.tileView,
1807
+ tileFeature: spec.tileFeature,
1808
+ outlineMode: spec.outlineMode,
1809
+ styleImages: styleImageHashes
1810
+ })
1811
+ );
1812
+ }
1813
+
1814
+ // src/providers/comfyui.ts
1815
+ var DEFAULT_BASE_URL2 = "http://127.0.0.1:8188";
1816
+ var ComfyUIClient = class {
1817
+ constructor(baseUrl = process.env.COMFYUI_BASE_URL ?? DEFAULT_BASE_URL2, request = fetch) {
1818
+ this.request = request;
1819
+ this.baseUrl = normalizeBaseUrl(baseUrl);
1820
+ }
1821
+ request;
1822
+ baseUrl;
1823
+ async checkConnection() {
1824
+ const value = await this.json("system_stats");
1825
+ if (!isObject(value)) throw new Error("ComfyUI returned invalid system stats");
1826
+ }
1827
+ async submit(workflow) {
1828
+ const value = await this.json("prompt", {
1829
+ method: "POST",
1830
+ body: JSON.stringify({ prompt: workflow, client_id: randomUUID2() })
1831
+ });
1832
+ if (!isObject(value)) throw new Error("ComfyUI returned an invalid queue response");
1833
+ const promptId = value.prompt_id;
1834
+ if (typeof promptId !== "string" || !promptId) {
1835
+ const nodes = isObject(value.node_errors) ? Object.keys(value.node_errors) : [];
1836
+ throw new Error(
1837
+ "ComfyUI did not return a prompt id" + (nodes.length ? `; invalid workflow node(s): ${nodes.slice(0, 8).join(", ")}` : "")
1838
+ );
1839
+ }
1840
+ return promptId;
1841
+ }
1842
+ async history(promptId) {
1843
+ return this.json(`history/${encodeURIComponent(promptId)}`);
1844
+ }
1845
+ viewUrl(image) {
1846
+ const url = this.endpoint("view");
1847
+ url.searchParams.set("filename", image.filename);
1848
+ url.searchParams.set("subfolder", image.subfolder);
1849
+ url.searchParams.set("type", image.type);
1850
+ return url.toString();
1851
+ }
1852
+ async download(source) {
1853
+ let target = source;
1854
+ if (source.startsWith("comfyui://")) target = this.viewUrl(parseSource(source));
1855
+ const response = await this.request(target);
1856
+ if (!response.ok) throw new Error(`ComfyUI download failed (${response.status})`);
1857
+ return Buffer.from(await response.arrayBuffer());
1858
+ }
1859
+ endpoint(route) {
1860
+ return new URL(route.replace(/^\/+/, ""), `${this.baseUrl}/`);
1861
+ }
1862
+ async json(route, init = {}) {
1863
+ const url = this.endpoint(route);
1864
+ const response = await this.request(url, {
1865
+ ...init,
1866
+ headers: {
1867
+ ...init.body ? { "Content-Type": "application/json" } : {},
1868
+ ...init.headers
1869
+ }
1870
+ });
1871
+ const text = await response.text();
1872
+ let value = null;
1873
+ try {
1874
+ value = text ? JSON.parse(text) : null;
1875
+ } catch {
1876
+ if (response.ok) throw new Error(`ComfyUI returned invalid JSON from ${url.pathname}`);
1877
+ value = null;
1878
+ }
1879
+ if (!response.ok) {
1880
+ const detail = apiError(value);
1881
+ throw new Error(
1882
+ `ComfyUI request failed (${response.status}) at ${url.pathname}` + (detail ? `: ${detail}` : "")
1883
+ );
1884
+ }
1885
+ return value;
1886
+ }
1887
+ };
1888
+ var ComfyUIProvider = class _ComfyUIProvider {
1889
+ constructor(client = new ComfyUIClient()) {
1890
+ this.client = client;
1891
+ }
1892
+ client;
1893
+ id = "comfyui";
1894
+ static fromEnv() {
1895
+ return new _ComfyUIProvider();
1896
+ }
1897
+ static forOffline() {
1898
+ return new _ComfyUIProvider(new ComfyUIClient(DEFAULT_BASE_URL2));
1899
+ }
1900
+ static forDownloads() {
1901
+ return new _ComfyUIProvider();
1902
+ }
1903
+ async resolveOptions(value, context) {
1904
+ const options = parseOptions(value);
1905
+ const workflowPath = path2.resolve(context.root, options.workflowFile);
1906
+ let raw;
1907
+ try {
1908
+ raw = await readFile2(workflowPath, "utf8");
1909
+ } catch (err) {
1910
+ throw new Error(
1911
+ `ComfyUI workflow for style "${context.styleId}" could not be read at ${workflowPath}: ${err instanceof Error ? err.message : String(err)}`
1912
+ );
1913
+ }
1914
+ let parsed;
1915
+ try {
1916
+ parsed = JSON.parse(raw);
1917
+ } catch {
1918
+ throw new Error(`ComfyUI workflow for style "${context.styleId}" is not valid JSON: ${workflowPath}`);
1919
+ }
1920
+ const workflow = parseWorkflow(parsed, workflowPath);
1921
+ validateWorkflowBindings(workflow, options);
1922
+ const workflowSha256 = sha256(JSON.stringify(canonical(workflow)));
1923
+ const resolved = { ...options, workflow, workflowSha256 };
1924
+ return {
1925
+ options: resolved,
1926
+ identity: {
1927
+ outputNodeId: options.outputNodeId,
1928
+ numImages: options.numImages,
1929
+ bindings: options.bindings,
1930
+ workflowSha256
1931
+ }
1932
+ };
1933
+ }
1934
+ supports(generator) {
1935
+ return generator === "map";
1936
+ }
1937
+ estimate(spec) {
1938
+ return { unit: "free", amount: 0, candidates: resolvedOptions(spec).numImages };
1939
+ }
1940
+ validate(spec, styleImages) {
1941
+ const options = resolvedOptions(spec);
1942
+ if (spec.width < 16 || spec.height < 16 || spec.width > 4096 || spec.height > 4096) {
1943
+ throw new Error("ComfyUI output dimensions must be between 16 and 4096 pixels");
1944
+ }
1945
+ if (styleImages.length) {
1946
+ throw new Error("ComfyUI styleImages are not supported yet; keep references inside the workflow");
1947
+ }
1948
+ if (spec.palette.length) {
1949
+ throw new Error("ComfyUI does not map PixelKiln palette values yet; encode palette control in the workflow");
1950
+ }
1951
+ if (spec.seed != null && !options.bindings.seed) {
1952
+ throw new Error("ComfyUI requires bindings.seed when the style declares a seed");
1953
+ }
1954
+ validateWorkflowBindings(options.workflow, options);
1955
+ }
1956
+ rateLimit() {
1957
+ return { spacingMs: 0, maxInFlight: 1 };
1958
+ }
1959
+ async submit(spec, styleImages) {
1960
+ this.validate(spec, styleImages);
1961
+ const options = resolvedOptions(spec);
1962
+ const workflow = structuredClone(options.workflow);
1963
+ setBinding(workflow, options.bindings.prompt, spec.prompt);
1964
+ setBinding(workflow, options.bindings.width, spec.width);
1965
+ setBinding(workflow, options.bindings.height, spec.height);
1966
+ setBinding(workflow, options.bindings.batchSize, options.numImages);
1967
+ if (spec.seed != null && options.bindings.seed) {
1968
+ setBinding(workflow, options.bindings.seed, spec.seed);
1969
+ }
1970
+ const promptId = await this.client.submit(workflow);
1971
+ return { jobId: encodeJob(promptId, options.outputNodeId) };
1972
+ }
1973
+ async poll(jobId, _generator, context) {
1974
+ const { promptId, outputNodeId } = decodeJob(jobId);
1975
+ const history = await this.client.history(promptId);
1976
+ const entry = historyEntry(history, promptId);
1977
+ if (!entry) return { status: "processing" };
1978
+ const status = isObject(entry.status) ? entry.status : {};
1979
+ const statusText = typeof status.status_str === "string" ? status.status_str : "";
1980
+ if (statusText === "error") {
1981
+ return { status: "failed", error: historyError(status) };
1982
+ }
1983
+ if (status.completed === false) return { status: "processing" };
1984
+ const images = outputImages(entry, outputNodeId);
1985
+ if (images instanceof Error) return { status: "failed", error: images.message };
1986
+ const expected = context?.spec ? resolvedOptions(context.spec).numImages : null;
1987
+ if (expected != null && images.length !== expected) {
1988
+ return {
1989
+ status: "failed",
1990
+ error: `ComfyUI output node "${outputNodeId}" returned ${images.length} image(s), expected ${expected}`
1991
+ };
1992
+ }
1993
+ const metadata = context?.spec ? comfyMetadata(context.spec, promptId, outputNodeId, images) : {
1994
+ promptId,
1995
+ outputNodeId,
1996
+ outputCount: images.length
1997
+ };
1998
+ if (images.length > 1) {
1999
+ return {
2000
+ status: "review",
2001
+ candidateUrls: images.map((image) => this.client.viewUrl(image)),
2002
+ metadata
2003
+ };
2004
+ }
2005
+ const sourceUrl = sourceRef(images[0]);
2006
+ return {
2007
+ status: "ready",
2008
+ objectId: `${promptId}#${outputNodeId}#0`,
2009
+ sourceUrl,
2010
+ sources: [{ url: sourceUrl, mediaType: MediaType.PNG }],
2011
+ metadata
2012
+ };
2013
+ }
2014
+ async selectCandidate(jobId, index) {
2015
+ const { promptId, outputNodeId } = decodeJob(jobId);
2016
+ const history = await this.client.history(promptId);
2017
+ const entry = historyEntry(history, promptId);
2018
+ if (!entry) throw new Error(`ComfyUI prompt ${promptId} is not complete`);
2019
+ const images = outputImages(entry, outputNodeId);
2020
+ if (images instanceof Error) throw images;
2021
+ const image = images[index];
2022
+ if (!image) throw new Error(`ComfyUI prompt ${promptId} has no candidate at index ${index}`);
2023
+ return {
2024
+ objectId: `${promptId}#${outputNodeId}#${index}`,
2025
+ sourceUrl: sourceRef(image)
2026
+ };
2027
+ }
2028
+ async download(url) {
2029
+ return this.client.download(url);
2030
+ }
2031
+ async checkConnection() {
2032
+ await this.client.checkConnection();
2033
+ }
2034
+ };
2035
+ function parseOptions(value) {
2036
+ const allowed = /* @__PURE__ */ new Set(["workflowFile", "outputNodeId", "numImages", "bindings", "workflow", "workflowSha256"]);
2037
+ const extra = Object.keys(value).filter((key) => !allowed.has(key));
2038
+ if (extra.length) throw new Error(`Unknown ComfyUI option(s): ${extra.join(", ")}`);
2039
+ const workflowFile = requiredString(value.workflowFile, "workflowFile");
2040
+ const outputNodeId = requiredString(value.outputNodeId, "outputNodeId");
2041
+ const numImages = value.numImages ?? 1;
2042
+ if (!Number.isInteger(numImages) || Number(numImages) < 1 || Number(numImages) > 16) {
2043
+ throw new Error("ComfyUI numImages must be a whole number from 1 to 16");
2044
+ }
2045
+ if (!isObject(value.bindings)) throw new Error("ComfyUI bindings must be an object");
2046
+ const bindingKeys = /* @__PURE__ */ new Set(["prompt", "width", "height", "batchSize", "seed"]);
2047
+ const extraBindings = Object.keys(value.bindings).filter((key) => !bindingKeys.has(key));
2048
+ if (extraBindings.length) throw new Error(`Unknown ComfyUI binding(s): ${extraBindings.join(", ")}`);
2049
+ const bindings = {
2050
+ prompt: parseBinding(value.bindings.prompt, "prompt"),
2051
+ width: parseBinding(value.bindings.width, "width"),
2052
+ height: parseBinding(value.bindings.height, "height"),
2053
+ batchSize: parseBinding(value.bindings.batchSize, "batchSize"),
2054
+ ...value.bindings.seed == null ? {} : { seed: parseBinding(value.bindings.seed, "seed") }
2055
+ };
2056
+ const workflow = value.workflow == null ? void 0 : parseWorkflow(value.workflow, workflowFile);
2057
+ const workflowSha256 = value.workflowSha256 == null ? void 0 : requiredString(value.workflowSha256, "workflowSha256");
2058
+ return {
2059
+ workflowFile,
2060
+ outputNodeId,
2061
+ numImages: Number(numImages),
2062
+ bindings,
2063
+ ...workflow ? { workflow } : {},
2064
+ ...workflowSha256 ? { workflowSha256 } : {}
2065
+ };
2066
+ }
2067
+ function resolvedOptions(spec) {
2068
+ const options = parseOptions(spec.providerOptions);
2069
+ if (!options.workflow || !options.workflowSha256) {
2070
+ throw new Error("ComfyUI workflow options were not resolved from workflowFile");
2071
+ }
2072
+ return options;
2073
+ }
2074
+ function parseBinding(value, name) {
2075
+ if (!isObject(value)) throw new Error(`ComfyUI bindings.${name} must be an object`);
2076
+ const extra = Object.keys(value).filter((key) => key !== "nodeId" && key !== "input");
2077
+ if (extra.length) throw new Error(`Unknown ComfyUI bindings.${name} field(s): ${extra.join(", ")}`);
2078
+ return {
2079
+ nodeId: requiredString(value.nodeId, `bindings.${name}.nodeId`),
2080
+ input: requiredString(value.input, `bindings.${name}.input`)
2081
+ };
2082
+ }
2083
+ function parseWorkflow(value, label) {
2084
+ if (!isObject(value) || !Object.keys(value).length) {
2085
+ throw new Error(`ComfyUI workflow is not a non-empty API-format object: ${label}`);
2086
+ }
2087
+ for (const [nodeId, node] of Object.entries(value)) {
2088
+ if (!isObject(node) || typeof node.class_type !== "string" || !isObject(node.inputs)) {
2089
+ throw new Error(`ComfyUI workflow node "${nodeId}" must contain class_type and inputs`);
2090
+ }
2091
+ }
2092
+ return value;
2093
+ }
2094
+ function validateWorkflowBindings(workflow, options) {
2095
+ for (const [name, binding] of Object.entries(options.bindings)) {
2096
+ if (!binding) continue;
2097
+ const node = workflow[binding.nodeId];
2098
+ if (!node) throw new Error(`ComfyUI ${name} binding refers to missing node "${binding.nodeId}"`);
2099
+ if (!Object.hasOwn(node.inputs, binding.input)) {
2100
+ throw new Error(
2101
+ `ComfyUI ${name} binding refers to missing input "${binding.input}" on node "${binding.nodeId}"`
2102
+ );
2103
+ }
2104
+ }
2105
+ if (!workflow[options.outputNodeId]) {
2106
+ throw new Error(`ComfyUI outputNodeId refers to missing node "${options.outputNodeId}"`);
2107
+ }
2108
+ }
2109
+ function setBinding(workflow, binding, value) {
2110
+ const node = workflow[binding.nodeId];
2111
+ if (!node || !Object.hasOwn(node.inputs, binding.input)) {
2112
+ throw new Error(`ComfyUI binding ${binding.nodeId}.${binding.input} is unavailable`);
2113
+ }
2114
+ node.inputs[binding.input] = value;
2115
+ }
2116
+ function historyEntry(history, promptId) {
2117
+ if (!isObject(history)) throw new Error("ComfyUI returned invalid history JSON");
2118
+ const entry = history[promptId];
2119
+ if (entry == null) return null;
2120
+ if (!isObject(entry)) throw new Error(`ComfyUI returned invalid history for prompt ${promptId}`);
2121
+ return entry;
2122
+ }
2123
+ function outputImages(entry, outputNodeId) {
2124
+ if (!isObject(entry.outputs)) return new Error("ComfyUI completed without workflow outputs");
2125
+ const output = entry.outputs[outputNodeId];
2126
+ if (!isObject(output) || !Array.isArray(output.images)) {
2127
+ return new Error(`ComfyUI output node "${outputNodeId}" returned no images`);
2128
+ }
2129
+ const images = [];
2130
+ for (const value of output.images) {
2131
+ if (!isObject(value) || typeof value.filename !== "string" || typeof value.subfolder !== "string" || value.type !== "output") {
2132
+ return new Error(`ComfyUI output node "${outputNodeId}" returned an invalid image record`);
2133
+ }
2134
+ images.push({ filename: value.filename, subfolder: value.subfolder, type: "output" });
2135
+ }
2136
+ if (!images.length) return new Error(`ComfyUI output node "${outputNodeId}" returned no images`);
2137
+ return images;
2138
+ }
2139
+ function comfyMetadata(spec, promptId, outputNodeId, images) {
2140
+ const options = resolvedOptions(spec);
2141
+ return {
2142
+ promptId,
2143
+ outputNodeId,
2144
+ outputCount: images.length,
2145
+ workflowFile: options.workflowFile,
2146
+ workflowSha256: options.workflowSha256,
2147
+ files: images.map((image) => ({ ...image }))
2148
+ };
2149
+ }
2150
+ function encodeJob(promptId, outputNodeId) {
2151
+ return `${promptId}#${encodeURIComponent(outputNodeId)}`;
2152
+ }
2153
+ function decodeJob(jobId) {
2154
+ const split = jobId.lastIndexOf("#");
2155
+ if (split < 1 || split === jobId.length - 1) throw new Error(`Invalid ComfyUI job id "${jobId}"`);
2156
+ return {
2157
+ promptId: jobId.slice(0, split),
2158
+ outputNodeId: decodeURIComponent(jobId.slice(split + 1))
2159
+ };
2160
+ }
2161
+ function sourceRef(image) {
2162
+ const url = new URL("comfyui://output");
2163
+ url.searchParams.set("filename", image.filename);
2164
+ url.searchParams.set("subfolder", image.subfolder);
2165
+ url.searchParams.set("type", image.type);
2166
+ return url.toString();
2167
+ }
2168
+ function parseSource(source) {
2169
+ let url;
2170
+ try {
2171
+ url = new URL(source);
2172
+ } catch {
2173
+ throw new Error("Invalid ComfyUI output reference");
2174
+ }
2175
+ const filename = url.searchParams.get("filename");
2176
+ const subfolder = url.searchParams.get("subfolder");
2177
+ const type = url.searchParams.get("type");
2178
+ if (url.protocol !== "comfyui:" || url.hostname !== "output" || !filename || subfolder == null || type !== "output") {
2179
+ throw new Error("Invalid ComfyUI output reference");
2180
+ }
2181
+ return { filename, subfolder, type };
2182
+ }
2183
+ function normalizeBaseUrl(value) {
2184
+ let url;
2185
+ try {
2186
+ url = new URL(value);
2187
+ } catch {
2188
+ throw new Error("COMFYUI_BASE_URL must be an absolute HTTP(S) URL");
2189
+ }
2190
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
2191
+ throw new Error("COMFYUI_BASE_URL must use HTTP or HTTPS");
2192
+ }
2193
+ url.hash = "";
2194
+ url.search = "";
2195
+ return url.toString().replace(/\/$/, "");
2196
+ }
2197
+ function historyError(status) {
2198
+ const messages = Array.isArray(status.messages) ? status.messages : [];
2199
+ for (const message2 of messages) {
2200
+ if (!Array.isArray(message2) || message2[0] !== "execution_error" || !isObject(message2[1])) continue;
2201
+ const detail = message2[1].exception_message;
2202
+ if (typeof detail === "string" && detail.trim()) {
2203
+ return `ComfyUI execution failed: ${detail.trim().slice(0, 300)}`;
2204
+ }
2205
+ }
2206
+ return "ComfyUI execution failed";
2207
+ }
2208
+ function apiError(value) {
2209
+ if (!isObject(value)) return null;
2210
+ const raw = typeof value.error === "string" ? value.error : isObject(value.error) && typeof value.error.message === "string" ? value.error.message : typeof value.message === "string" ? value.message : null;
2211
+ const nodes = isObject(value.node_errors) ? Object.keys(value.node_errors) : [];
2212
+ if (nodes.length) {
2213
+ return `${raw ? `${raw}; ` : ""}invalid workflow node(s): ${nodes.slice(0, 8).join(", ")}`;
2214
+ }
2215
+ return raw?.trim().slice(0, 300) || null;
2216
+ }
2217
+ function requiredString(value, name) {
2218
+ if (typeof value !== "string" || !value.trim()) throw new Error(`ComfyUI ${name} must be a non-empty string`);
2219
+ return value.trim();
2220
+ }
2221
+ function isObject(value) {
2222
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
2223
+ }
2224
+ function canonical(value) {
2225
+ if (Array.isArray(value)) return value.map(canonical);
2226
+ if (!isObject(value)) return value;
2227
+ return Object.fromEntries(
2228
+ Object.keys(value).sort().map((key) => [key, canonical(value[key])])
2229
+ );
2230
+ }
2231
+
2232
+ // src/providers/fake.ts
2233
+ import { createHash as createHash2 } from "crypto";
1766
2234
  var FAKE_PNG = Buffer.from(
1767
2235
  "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
1768
2236
  "base64"
@@ -1846,7 +2314,7 @@ var FakeProvider = class {
1846
2314
  }
1847
2315
  async download(url) {
1848
2316
  if (!url.startsWith("fake://")) throw new Error(`fake provider cannot download ${url}`);
1849
- const salt = createHash("sha256").update(url).digest();
2317
+ const salt = createHash2("sha256").update(url).digest();
1850
2318
  return Buffer.concat([FAKE_PNG, salt.subarray(0, 4)]);
1851
2319
  }
1852
2320
  async balance() {
@@ -1945,56 +2413,23 @@ registerProvider({
1945
2413
  return RetroDiffusionProvider.forOffline();
1946
2414
  }
1947
2415
  });
2416
+ registerProvider({
2417
+ id: "comfyui",
2418
+ create(mode2) {
2419
+ if (mode2 === "online") return ComfyUIProvider.fromEnv();
2420
+ if (mode2 === "downloads") return ComfyUIProvider.forDownloads();
2421
+ return ComfyUIProvider.forOffline();
2422
+ }
2423
+ });
1948
2424
 
1949
2425
  // src/manifest.ts
1950
- import { readFile as readFile2 } from "fs/promises";
2426
+ import { readFile as readFile3 } from "fs/promises";
1951
2427
  import { existsSync as existsSync2 } from "fs";
1952
- import path2 from "path";
1953
-
1954
- // src/hash.ts
1955
- import { createHash as createHash2 } from "crypto";
1956
- import { readFile } from "fs/promises";
1957
- function sha256(data) {
1958
- return createHash2("sha256").update(data).digest("hex");
1959
- }
1960
- async function sha256File(path17) {
1961
- return sha256(await readFile(path17));
1962
- }
1963
- function specHash(spec, styleImageHashes) {
1964
- return sha256(
1965
- JSON.stringify({
1966
- // Preserve every existing PixelLab hash while making a provider switch
1967
- // invalidate the spec. Older manifests implicitly mean pixellab.
1968
- provider: spec.provider === "pixellab" ? void 0 : spec.provider,
1969
- providerOptions: Object.keys(spec.providerOptions).length > 0 ? spec.providerOptions : void 0,
1970
- generator: spec.generator,
1971
- prompt: spec.prompt,
1972
- width: spec.width,
1973
- height: spec.height,
1974
- view: spec.view,
1975
- outline: spec.outline ?? null,
1976
- shading: spec.shading ?? null,
1977
- detail: spec.detail ?? null,
1978
- seed: spec.seed ?? null,
1979
- palette: spec.palette,
1980
- // `noBackground` only reaches the wire for pixflux; the tile fields are
1981
- // undefined for every other generator. `tileSize` is intentionally
1982
- // absent — width/height are derived from it, so it is already covered.
1983
- noBackground: spec.generator === "pixflux" || spec.provider !== "pixellab" ? spec.noBackground : void 0,
1984
- tileType: spec.tileType,
1985
- tileView: spec.tileView,
1986
- tileFeature: spec.tileFeature,
1987
- outlineMode: spec.outlineMode,
1988
- styleImages: styleImageHashes
1989
- })
1990
- );
1991
- }
1992
-
1993
- // src/manifest.ts
2428
+ import path3 from "path";
1994
2429
  async function loadManifest(manifestPath) {
1995
- const abs = path2.resolve(manifestPath);
2430
+ const abs = path3.resolve(manifestPath);
1996
2431
  if (!existsSync2(abs)) throw new Error(`No manifest at ${abs}`);
1997
- const parsed = ManifestSchema.safeParse(JSON.parse(await readFile2(abs, "utf8")));
2432
+ const parsed = ManifestSchema.safeParse(JSON.parse(await readFile3(abs, "utf8")));
1998
2433
  if (!parsed.success) {
1999
2434
  const issues = parsed.error.issues.map((i) => ` ${i.path.join(".")}: ${i.message}`).join("\n");
2000
2435
  throw new Error(`Manifest at ${abs} is invalid:
@@ -2016,7 +2451,7 @@ ${issues}`);
2016
2451
  throw new Error(`Manifest at ${abs} is invalid:
2017
2452
  ${unknownReferences.map((i) => ` ${i}`).join("\n")}`);
2018
2453
  }
2019
- return { manifest: parsed.data, root: path2.dirname(abs), path: abs };
2454
+ return { manifest: parsed.data, root: path3.dirname(abs), path: abs };
2020
2455
  }
2021
2456
  async function resolveSpecs(loaded, filter) {
2022
2457
  const { manifest, root } = loaded;
@@ -2039,11 +2474,11 @@ async function resolveSpecs(loaded, filter) {
2039
2474
  }
2040
2475
  const styleImageCache = /* @__PURE__ */ new Map();
2041
2476
  async function loadStyleImage(rel) {
2042
- const abs = path2.resolve(root, rel);
2477
+ const abs = path3.resolve(root, rel);
2043
2478
  let hit = styleImageCache.get(abs);
2044
2479
  if (!hit) {
2045
2480
  if (!existsSync2(abs)) throw new Error(`Style image not found: ${abs}`);
2046
- const buf = await readFile2(abs);
2481
+ const buf = await readFile3(abs);
2047
2482
  const metadata = imageMetadata(buf);
2048
2483
  if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${abs}`);
2049
2484
  if (metadata.width < 1 || metadata.height < 1) {
@@ -2056,6 +2491,10 @@ async function resolveSpecs(loaded, filter) {
2056
2491
  }
2057
2492
  for (const styleId of styleIds) {
2058
2493
  const style = manifest.styles[styleId];
2494
+ const rawProviderOptions = style.providerOptions[activeProvider.id] ?? {};
2495
+ const optionResolution = activeProvider.resolveOptions ? await activeProvider.resolveOptions(rawProviderOptions, { root, styleId }) : { options: rawProviderOptions };
2496
+ const providerOptions = optionResolution.options;
2497
+ const providerOptionIdentity = optionResolution.identity ?? providerOptions;
2059
2498
  const styleImageHashes = [];
2060
2499
  const styleImageDimensions = [];
2061
2500
  for (const img of style.styleImages) {
@@ -2088,15 +2527,15 @@ async function resolveSpecs(loaded, filter) {
2088
2527
  }
2089
2528
  const subject = asset.promptByStyle[styleId] ?? asset.prompt;
2090
2529
  const prompt = [style.promptPrefix, subject, style.promptSuffix].map((p) => p.trim()).filter(Boolean).join(", ");
2091
- const relFile = asset.file ?? path2.join(asset.category ?? "", `${assetId}.png`);
2092
- const outFile = path2.resolve(root, style.outDir, relFile);
2530
+ const relFile = asset.file ?? path3.join(asset.category ?? "", `${assetId}.png`);
2531
+ const outFile = path3.resolve(root, style.outDir, relFile);
2093
2532
  const tileSize = generator === "tiles" ? size : style.tileSize ?? 32;
2094
2533
  const tileVariations = tileFeatureOutputCount(generator === "tiles" ? style.tileFeature : void 0) ?? tileVariationCount(countNumberedDescriptions(prompt));
2095
2534
  const base = {
2096
2535
  styleId,
2097
2536
  assetId,
2098
2537
  provider: activeProvider.id,
2099
- providerOptions: style.providerOptions[activeProvider.id] ?? {},
2538
+ providerOptions,
2100
2539
  generator,
2101
2540
  prompt,
2102
2541
  width,
@@ -2134,10 +2573,10 @@ async function resolveSpecs(loaded, filter) {
2134
2573
  outFile,
2135
2574
  tags,
2136
2575
  source: asset.source,
2137
- specHash: specHash(base, styleImageHashes)
2576
+ specHash: specHash(base, styleImageHashes, providerOptionIdentity)
2138
2577
  };
2139
2578
  const resolvedImages = style.styleImages.map((image) => {
2140
- const hit = styleImageCache.get(path2.resolve(root, image.path));
2579
+ const hit = styleImageCache.get(path3.resolve(root, image.path));
2141
2580
  return { base64: hit.base64, width: hit.width, height: hit.height, format: hit.format };
2142
2581
  });
2143
2582
  activeProvider.validate?.(resolved, resolvedImages);
@@ -2164,8 +2603,8 @@ async function resolveStyleImages(loaded, styleId) {
2164
2603
  if (!style) throw new Error(`Unknown style "${styleId}"`);
2165
2604
  const out = [];
2166
2605
  for (const img of style.styleImages) {
2167
- const file = path2.resolve(loaded.root, img.path);
2168
- const buf = await readFile2(file);
2606
+ const file = path3.resolve(loaded.root, img.path);
2607
+ const buf = await readFile3(file);
2169
2608
  const metadata = imageMetadata(buf);
2170
2609
  if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${file}`);
2171
2610
  if (metadata.width < 1 || metadata.height < 1) {
@@ -2208,13 +2647,13 @@ function imageMetadata(buf) {
2208
2647
  }
2209
2648
 
2210
2649
  // src/lock.ts
2211
- import { readFile as readFile3, writeFile, rename, mkdir, rmdir, rm, stat } from "fs/promises";
2650
+ import { readFile as readFile4, writeFile, rename, mkdir, rmdir, rm, stat } from "fs/promises";
2212
2651
  import { existsSync as existsSync3 } from "fs";
2213
- import path3 from "path";
2652
+ import path4 from "path";
2214
2653
  async function loadLock(lockPath) {
2215
2654
  if (!existsSync3(lockPath)) return { version: 2, entries: {} };
2216
2655
  try {
2217
- return parseLock(JSON.parse(await readFile3(lockPath, "utf8")));
2656
+ return parseLock(JSON.parse(await readFile4(lockPath, "utf8")));
2218
2657
  } catch (err) {
2219
2658
  throw new Error(
2220
2659
  `Lockfile at ${lockPath} is malformed:
@@ -2226,7 +2665,7 @@ var saveQueues = /* @__PURE__ */ new Map();
2226
2665
  var dirtyPatches = /* @__PURE__ */ new WeakMap();
2227
2666
  var dirtyDeletes = /* @__PURE__ */ new WeakMap();
2228
2667
  function saveLock(lockPath, lock) {
2229
- const queueKey = path3.resolve(lockPath);
2668
+ const queueKey = path4.resolve(lockPath);
2230
2669
  const previous = saveQueues.get(queueKey) ?? Promise.resolve();
2231
2670
  const next = previous.catch(() => {
2232
2671
  }).then(() => writeLockNow(lockPath, lock));
@@ -2238,7 +2677,7 @@ function saveLock(lockPath, lock) {
2238
2677
  return next;
2239
2678
  }
2240
2679
  async function writeLockNow(lockPath, lock) {
2241
- await mkdir(path3.dirname(path3.resolve(lockPath)), { recursive: true });
2680
+ await mkdir(path4.dirname(path4.resolve(lockPath)), { recursive: true });
2242
2681
  const release = await acquireFileLock(lockPath);
2243
2682
  try {
2244
2683
  await writeLockWhileHeld(lockPath, lock);
@@ -2250,7 +2689,7 @@ async function writeLockWhileHeld(file, lock) {
2250
2689
  let disk = { version: 2, entries: {} };
2251
2690
  if (existsSync3(file)) {
2252
2691
  try {
2253
- disk = parseLock(JSON.parse(await readFile3(file, "utf8")));
2692
+ disk = parseLock(JSON.parse(await readFile4(file, "utf8")));
2254
2693
  } catch (err) {
2255
2694
  throw new Error(
2256
2695
  `Refusing to overwrite malformed lockfile at ${file}: ${err instanceof Error ? err.message : String(err)}`
@@ -2381,21 +2820,21 @@ function totalSpend(lock, unit = "generations") {
2381
2820
  }
2382
2821
 
2383
2822
  // src/outputs.ts
2384
- import path4 from "path";
2823
+ import path5 from "path";
2385
2824
  function portableOutputPath(file, manifestDir) {
2386
- const root = path4.resolve(manifestDir);
2387
- const absolute = path4.isAbsolute(file) ? path4.normalize(file) : path4.resolve(root, file);
2388
- const relative = path4.relative(root, absolute);
2389
- const value = path4.isAbsolute(relative) ? absolute : relative;
2390
- return value.split(path4.sep).join("/");
2825
+ const root = path5.resolve(manifestDir);
2826
+ const absolute = path5.isAbsolute(file) ? path5.normalize(file) : path5.resolve(root, file);
2827
+ const relative = path5.relative(root, absolute);
2828
+ const value = path5.isAbsolute(relative) ? absolute : relative;
2829
+ return value.split(path5.sep).join("/");
2391
2830
  }
2392
2831
  function resolveOutputPath(recordedPath, manifestDir) {
2393
- if (path4.isAbsolute(recordedPath)) return path4.normalize(recordedPath);
2394
- if (path4.win32.isAbsolute(recordedPath)) return recordedPath;
2395
- return path4.resolve(manifestDir, recordedPath.split(/[\\/]/).join(path4.sep));
2832
+ if (path5.isAbsolute(recordedPath)) return path5.normalize(recordedPath);
2833
+ if (path5.win32.isAbsolute(recordedPath)) return recordedPath;
2834
+ return path5.resolve(manifestDir, recordedPath.split(/[\\/]/).join(path5.sep));
2396
2835
  }
2397
2836
  function expectedOutputPath(spec, role, index, total, mediaType) {
2398
- const originalExt = path4.extname(spec.outFile);
2837
+ const originalExt = path5.extname(spec.outFile);
2399
2838
  const ext = mediaType ? mediaExtension(mediaType) : originalExt || ".png";
2400
2839
  const stem = originalExt ? spec.outFile.slice(0, -originalExt.length) : spec.outFile;
2401
2840
  if (total === 1) return `${stem}${ext}`;
@@ -2484,15 +2923,15 @@ function resolveSpecOutputs(spec, lock, manifestDir) {
2484
2923
  id: spec.assetId,
2485
2924
  index: 0,
2486
2925
  path: spec.outFile,
2487
- absolutePath: path4.resolve(spec.outFile),
2926
+ absolutePath: path5.resolve(spec.outFile),
2488
2927
  sha256: ""
2489
2928
  }];
2490
2929
  }
2491
2930
 
2492
2931
  // src/artifacts.ts
2493
- import path5 from "path";
2494
- import { createHash as createHash3, randomUUID as randomUUID2 } from "crypto";
2495
- import { mkdir as mkdir2, readFile as readFile4, rename as rename2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
2932
+ import path6 from "path";
2933
+ import { createHash as createHash3, randomUUID as randomUUID3 } from "crypto";
2934
+ import { mkdir as mkdir2, readFile as readFile5, rename as rename2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
2496
2935
  var activeTransactions = /* @__PURE__ */ new Set();
2497
2936
  function message(error) {
2498
2937
  return error instanceof Error ? error.message : String(error);
@@ -2501,27 +2940,27 @@ function digest(data) {
2501
2940
  return createHash3("sha256").update(data).digest("hex");
2502
2941
  }
2503
2942
  function portableRelative(from, to) {
2504
- return path5.relative(from, path5.resolve(to)).split(path5.sep).join("/") || ".";
2943
+ return path6.relative(from, path6.resolve(to)).split(path6.sep).join("/") || ".";
2505
2944
  }
2506
- function canonical(value) {
2945
+ function canonical2(value) {
2507
2946
  if (value === null || typeof value === "string" || typeof value === "boolean") return value;
2508
2947
  if (typeof value === "number") {
2509
2948
  if (!Number.isFinite(value)) throw new Error("Artifact provenance numbers must be finite.");
2510
2949
  return value;
2511
2950
  }
2512
- if (Array.isArray(value)) return value.map(canonical);
2951
+ if (Array.isArray(value)) return value.map(canonical2);
2513
2952
  if (typeof value === "object") {
2514
2953
  const sorted = {};
2515
2954
  for (const key of Object.keys(value).sort()) {
2516
2955
  const item = value[key];
2517
- if (item !== void 0) sorted[key] = canonical(item);
2956
+ if (item !== void 0) sorted[key] = canonical2(item);
2518
2957
  }
2519
2958
  return sorted;
2520
2959
  }
2521
2960
  throw new Error(`Artifact provenance cannot contain ${typeof value} values.`);
2522
2961
  }
2523
2962
  function fingerprint(provenance) {
2524
- return digest(JSON.stringify(canonical({
2963
+ return digest(JSON.stringify(canonical2({
2525
2964
  kind: provenance.kind,
2526
2965
  sources: provenance.sources,
2527
2966
  options: provenance.options,
@@ -2552,7 +2991,7 @@ function parseArtifactManifest(absolute, data) {
2552
2991
  }
2553
2992
  async function readOptional(file) {
2554
2993
  try {
2555
- return await readFile4(file);
2994
+ return await readFile5(file);
2556
2995
  } catch (error) {
2557
2996
  if (isCode(error, "ENOENT")) return null;
2558
2997
  throw error;
@@ -2571,8 +3010,8 @@ function processIsAlive(pid) {
2571
3010
  }
2572
3011
  }
2573
3012
  function validTemporaryPath(candidate, destination, type) {
2574
- return path5.dirname(candidate) === path5.dirname(destination) && path5.basename(candidate).startsWith(
2575
- `.${path5.basename(destination)}.pixelkiln-${type}-`
3013
+ return path6.dirname(candidate) === path6.dirname(destination) && path6.basename(candidate).startsWith(
3014
+ `.${path6.basename(destination)}.pixelkiln-${type}-`
2576
3015
  );
2577
3016
  }
2578
3017
  function parseTransaction(journal, data) {
@@ -2592,7 +3031,7 @@ async function removeTransactionFiles(journal) {
2592
3031
  await rm2(transactionMarker(journal), { force: true });
2593
3032
  }
2594
3033
  async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
2595
- const journal = path5.resolve(recoveryFile);
3034
+ const journal = path6.resolve(recoveryFile);
2596
3035
  const bytes = await readOptional(journal);
2597
3036
  if (!bytes) {
2598
3037
  await rm2(transactionMarker(journal), { force: true });
@@ -2608,7 +3047,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
2608
3047
  );
2609
3048
  }
2610
3049
  for (const entry of transaction.entries) {
2611
- if (typeof entry.destination !== "string" || typeof entry.stage !== "string" || typeof entry.sha256 !== "string" || entry.backup !== void 0 && typeof entry.backup !== "string" || !allowedDestinations.has(path5.resolve(entry.destination)) || !validTemporaryPath(entry.stage, entry.destination, "stage") || entry.backup !== void 0 && !validTemporaryPath(entry.backup, entry.destination, "backup")) {
3050
+ if (typeof entry.destination !== "string" || typeof entry.stage !== "string" || typeof entry.sha256 !== "string" || entry.backup !== void 0 && typeof entry.backup !== "string" || !allowedDestinations.has(path6.resolve(entry.destination)) || !validTemporaryPath(entry.stage, entry.destination, "stage") || entry.backup !== void 0 && !validTemporaryPath(entry.backup, entry.destination, "backup")) {
2612
3051
  throw new Error(
2613
3052
  `Refusing unsafe artifact recovery from ${journal}; its destinations or temporary paths do not match the current bundle.`
2614
3053
  );
@@ -2654,7 +3093,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
2654
3093
  await removeTransactionFiles(journal);
2655
3094
  }
2656
3095
  function createArtifactBundleManifest(manifestPath, outputs, provenance) {
2657
- const root = path5.dirname(path5.resolve(manifestPath));
3096
+ const root = path6.dirname(path6.resolve(manifestPath));
2658
3097
  const sources = provenance.sources.map((source) => ({
2659
3098
  ...source,
2660
3099
  path: portableRelative(root, source.path)
@@ -2662,7 +3101,7 @@ function createArtifactBundleManifest(manifestPath, outputs, provenance) {
2662
3101
  const normalized = {
2663
3102
  kind: provenance.kind,
2664
3103
  sources,
2665
- options: canonical(provenance.options)
3104
+ options: canonical2(provenance.options)
2666
3105
  };
2667
3106
  const manifestOutputs = outputs.map((output) => ({
2668
3107
  path: portableRelative(root, output.path),
@@ -2684,11 +3123,11 @@ function withArtifactManifest(manifestPath, outputs, provenance) {
2684
3123
  ];
2685
3124
  }
2686
3125
  async function writeManagedArtifactBundle(manifestPath, outputs, provenance, options = {}) {
2687
- const absoluteManifest = path5.resolve(manifestPath);
3126
+ const absoluteManifest = path6.resolve(manifestPath);
2688
3127
  const recoveryFile = `${absoluteManifest}.transaction`;
2689
3128
  const allowedDestinations = /* @__PURE__ */ new Set([
2690
3129
  absoluteManifest,
2691
- ...outputs.map((output) => path5.resolve(output.path))
3130
+ ...outputs.map((output) => path6.resolve(output.path))
2692
3131
  ]);
2693
3132
  await recoverArtifactTransaction(recoveryFile, allowedDestinations);
2694
3133
  const existingManifestBytes = await readOptional(absoluteManifest);
@@ -2706,13 +3145,13 @@ async function writeManagedArtifactBundle(manifestPath, outputs, provenance, opt
2706
3145
  }
2707
3146
  }
2708
3147
  if (!options.force) {
2709
- const root = path5.dirname(absoluteManifest);
3148
+ const root = path6.dirname(absoluteManifest);
2710
3149
  const recorded = new Map(
2711
- previous?.outputs.map((output) => [path5.resolve(root, output.path), output.sha256]) ?? []
3150
+ previous?.outputs.map((output) => [path6.resolve(root, output.path), output.sha256]) ?? []
2712
3151
  );
2713
3152
  const conflicts = [];
2714
3153
  for (const output of outputs) {
2715
- const destination = path5.resolve(output.path);
3154
+ const destination = path6.resolve(output.path);
2716
3155
  const current = await readOptional(destination);
2717
3156
  if (!current || digest(current) === digest(output.data)) continue;
2718
3157
  const expected = recorded.get(destination);
@@ -2733,15 +3172,15 @@ Pass { force: true } (CLI: --force) to take ownership and replace the complete b
2733
3172
  );
2734
3173
  }
2735
3174
  async function verifyArtifactBundle(manifestPath) {
2736
- const absolute = path5.resolve(manifestPath);
2737
- const raw = parseArtifactManifest(absolute, await readFile4(absolute, "utf8"));
2738
- const root = path5.dirname(absolute);
3175
+ const absolute = path6.resolve(manifestPath);
3176
+ const raw = parseArtifactManifest(absolute, await readFile5(absolute, "utf8"));
3177
+ const root = path6.dirname(absolute);
2739
3178
  const changedSources = [];
2740
3179
  const changedOutputs = [];
2741
3180
  for (const source of raw.sources) {
2742
3181
  let actual = null;
2743
3182
  try {
2744
- actual = digest(await readFile4(path5.resolve(root, source.path)));
3183
+ actual = digest(await readFile5(path6.resolve(root, source.path)));
2745
3184
  } catch (error) {
2746
3185
  if (!isCode(error, "ENOENT")) throw error;
2747
3186
  }
@@ -2749,7 +3188,7 @@ async function verifyArtifactBundle(manifestPath) {
2749
3188
  }
2750
3189
  for (const output of raw.outputs) {
2751
3190
  try {
2752
- if (digest(await readFile4(path5.resolve(root, output.path))) !== output.sha256) {
3191
+ if (digest(await readFile5(path6.resolve(root, output.path))) !== output.sha256) {
2753
3192
  changedOutputs.push(output.path);
2754
3193
  }
2755
3194
  } catch (error) {
@@ -2805,7 +3244,7 @@ async function writeArtifactBundle(files, options = {}) {
2805
3244
  if (!files.length) throw new Error("An artifact bundle must contain at least one file.");
2806
3245
  const normalized = files.map((file) => {
2807
3246
  if (!file.path.trim()) throw new Error("Artifact paths cannot be empty.");
2808
- return { destination: path5.resolve(file.path), data: Buffer.from(file.data) };
3247
+ return { destination: path6.resolve(file.path), data: Buffer.from(file.data) };
2809
3248
  });
2810
3249
  const destinations = /* @__PURE__ */ new Set();
2811
3250
  for (const artifact of normalized) {
@@ -2821,7 +3260,7 @@ async function writeArtifactBundle(files, options = {}) {
2821
3260
  const unchanged = [];
2822
3261
  for (const artifact of normalized) {
2823
3262
  try {
2824
- const current = await readFile4(artifact.destination);
3263
+ const current = await readFile5(artifact.destination);
2825
3264
  if (current.equals(artifact.data)) {
2826
3265
  unchanged.push(artifact.destination);
2827
3266
  } else {
@@ -2833,23 +3272,23 @@ async function writeArtifactBundle(files, options = {}) {
2833
3272
  }
2834
3273
  }
2835
3274
  if (!changed.length) return { changed: [], unchanged };
2836
- const token = randomUUID2();
3275
+ const token = randomUUID3();
2837
3276
  for (const [index, artifact] of changed.entries()) {
2838
- const basename = path5.basename(artifact.destination);
2839
- artifact.stage = path5.join(
2840
- path5.dirname(artifact.destination),
3277
+ const basename = path6.basename(artifact.destination);
3278
+ artifact.stage = path6.join(
3279
+ path6.dirname(artifact.destination),
2841
3280
  `.${basename}.pixelkiln-stage-${token}-${index}`
2842
3281
  );
2843
3282
  if (artifact.existed) {
2844
- artifact.backup = path5.join(
2845
- path5.dirname(artifact.destination),
3283
+ artifact.backup = path6.join(
3284
+ path6.dirname(artifact.destination),
2846
3285
  `.${basename}.pixelkiln-backup-${token}-${index}`
2847
3286
  );
2848
3287
  }
2849
3288
  }
2850
- const journal = options.recoveryFile ? path5.resolve(options.recoveryFile) : null;
3289
+ const journal = options.recoveryFile ? path6.resolve(options.recoveryFile) : null;
2851
3290
  if (journal) {
2852
- await mkdir2(path5.dirname(journal), { recursive: true });
3291
+ await mkdir2(path6.dirname(journal), { recursive: true });
2853
3292
  const transaction = {
2854
3293
  format: "pixelkiln-artifact-transaction",
2855
3294
  version: 1,
@@ -2874,7 +3313,7 @@ async function writeArtifactBundle(files, options = {}) {
2874
3313
  }
2875
3314
  try {
2876
3315
  for (const [index, artifact] of changed.entries()) {
2877
- await mkdir2(path5.dirname(artifact.destination), { recursive: true });
3316
+ await mkdir2(path6.dirname(artifact.destination), { recursive: true });
2878
3317
  await options.beforeStage?.(artifact.destination, index);
2879
3318
  await writeFile2(artifact.stage, artifact.data, { flag: "wx" });
2880
3319
  }
@@ -2943,7 +3382,7 @@ async function writeArtifactBundle(files, options = {}) {
2943
3382
 
2944
3383
  // src/pipeline/plan.ts
2945
3384
  import { existsSync as existsSync4 } from "fs";
2946
- import path6 from "path";
3385
+ import path7 from "path";
2947
3386
  async function anyOutputModified(entry, spec) {
2948
3387
  for (let index = 0; index < entry.outputs.length; index++) {
2949
3388
  const output = entry.outputs[index];
@@ -2962,7 +3401,7 @@ async function buildPlan(specs, lock, opts = {}) {
2962
3401
  state = "missing";
2963
3402
  reason = "--force";
2964
3403
  } else if (!entry && spec.source) {
2965
- if (existsSync4(path6.resolve(spec.root, spec.source))) {
3404
+ if (existsSync4(path7.resolve(spec.root, spec.source))) {
2966
3405
  state = "ok";
2967
3406
  reason = `placed from ${spec.source}; not generated`;
2968
3407
  } else {
@@ -3028,9 +3467,9 @@ function summarize(plan) {
3028
3467
  }
3029
3468
 
3030
3469
  // src/pipeline/audit.ts
3031
- import { readFile as readFile5 } from "fs/promises";
3470
+ import { readFile as readFile6 } from "fs/promises";
3032
3471
  import { existsSync as existsSync5 } from "fs";
3033
- import path7 from "path";
3472
+ import path8 from "path";
3034
3473
  function colorDistance(a, b) {
3035
3474
  const rmean = (a.r + b.r) / 2;
3036
3475
  const dr = a.r - b.r;
@@ -3080,7 +3519,7 @@ async function auditStyle(loaded, specs, styleId, lock) {
3080
3519
  continue;
3081
3520
  }
3082
3521
  try {
3083
- const png = decodePng(await readFile5(output.absolutePath));
3522
+ const png = decodePng(await readFile6(output.absolutePath));
3084
3523
  const palette = extractPalette(png, 12);
3085
3524
  assets.push({
3086
3525
  assetId: spec.assetId,
@@ -3104,10 +3543,10 @@ async function auditStyle(loaded, specs, styleId, lock) {
3104
3543
  let referenceFromStyleImages = false;
3105
3544
  const refPalettes = [];
3106
3545
  for (const img of style.styleImages) {
3107
- const abs = path7.resolve(loaded.root, img.path);
3546
+ const abs = path8.resolve(loaded.root, img.path);
3108
3547
  if (!existsSync5(abs)) continue;
3109
3548
  try {
3110
- refPalettes.push(extractPalette(decodePng(await readFile5(abs)), 12));
3549
+ refPalettes.push(extractPalette(decodePng(await readFile6(abs)), 12));
3111
3550
  } catch {
3112
3551
  }
3113
3552
  }
@@ -3184,11 +3623,11 @@ function hex(c) {
3184
3623
 
3185
3624
  // src/pipeline/cache-health.ts
3186
3625
  import { existsSync as existsSync7 } from "fs";
3187
- import { readFile as readFile7, readdir, rm as rm4 } from "fs/promises";
3188
- import path8 from "path";
3626
+ import { readFile as readFile8, readdir, rm as rm4 } from "fs/promises";
3627
+ import path9 from "path";
3189
3628
 
3190
3629
  // src/cache.ts
3191
- import { mkdir as mkdir3, readFile as readFile6, rename as rename3, rm as rm3, writeFile as writeFile3 } from "fs/promises";
3630
+ import { mkdir as mkdir3, readFile as readFile7, rename as rename3, rm as rm3, writeFile as writeFile3 } from "fs/promises";
3192
3631
  import { existsSync as existsSync6 } from "fs";
3193
3632
  import pathModule from "path";
3194
3633
  import { z as z3 } from "zod";
@@ -3204,10 +3643,10 @@ function isSha256Hash(value) {
3204
3643
  function parseCache(value) {
3205
3644
  return HashCacheSchema.parse(value);
3206
3645
  }
3207
- async function loadCache(path17) {
3208
- if (!existsSync6(path17)) return { version: 1, hashes: {} };
3646
+ async function loadCache(path18) {
3647
+ if (!existsSync6(path18)) return { version: 1, hashes: {} };
3209
3648
  try {
3210
- const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile6(path17, "utf8")));
3649
+ const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile7(path18, "utf8")));
3211
3650
  if (!parsed.success) return { version: 1, hashes: {} };
3212
3651
  return {
3213
3652
  version: 1,
@@ -3219,18 +3658,18 @@ async function loadCache(path17) {
3219
3658
  return { version: 1, hashes: {} };
3220
3659
  }
3221
3660
  }
3222
- async function saveCache(path17, cache) {
3661
+ async function saveCache(path18, cache) {
3223
3662
  const sorted = {};
3224
3663
  for (const key of Object.keys(cache.hashes).sort()) {
3225
3664
  const hash = cache.hashes[key];
3226
3665
  if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
3227
3666
  sorted[key] = hash;
3228
3667
  }
3229
- await mkdir3(pathModule.dirname(pathModule.resolve(path17)), { recursive: true });
3230
- const tmp = `${path17}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
3668
+ await mkdir3(pathModule.dirname(pathModule.resolve(path18)), { recursive: true });
3669
+ const tmp = `${path18}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
3231
3670
  try {
3232
3671
  await writeFile3(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
3233
- await rename3(tmp, path17);
3672
+ await rename3(tmp, path18);
3234
3673
  } finally {
3235
3674
  await rm3(tmp, { force: true });
3236
3675
  }
@@ -3253,10 +3692,10 @@ function cachePathFor(lockPath) {
3253
3692
  // src/pipeline/cache-health.ts
3254
3693
  async function inspectCaches(lock, lockPath, options = {}) {
3255
3694
  if (options.prune && !existsSync7(lockPath)) {
3256
- throw new Error(`Refusing to prune without an existing lockfile at ${path8.resolve(lockPath)}`);
3695
+ throw new Error(`Refusing to prune without an existing lockfile at ${path9.resolve(lockPath)}`);
3257
3696
  }
3258
- const contentDir = path8.resolve(path8.dirname(lockPath), ".pixelkiln", "cache");
3259
- const remotePath = path8.resolve(cachePathFor(lockPath));
3697
+ const contentDir = path9.resolve(path9.dirname(lockPath), ".pixelkiln", "cache");
3698
+ const remotePath = path9.resolve(cachePathFor(lockPath));
3260
3699
  const referenced = new Set(
3261
3700
  Object.values(lock.entries).flatMap((entry) => entry.outputs.map((output) => output.sha256))
3262
3701
  );
@@ -3270,7 +3709,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
3270
3709
  ]);
3271
3710
  for (const name of names) {
3272
3711
  try {
3273
- await rm4(path8.join(contentDir, name), { force: true });
3712
+ await rm4(path9.join(contentDir, name), { force: true });
3274
3713
  removed.contentFiles++;
3275
3714
  } catch {
3276
3715
  }
@@ -3279,7 +3718,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
3279
3718
  await saveCache(remotePath, { version: 1, hashes: {} });
3280
3719
  removed.resetRemoteHashCache = true;
3281
3720
  } else if (remoteHashes.invalidIds.length) {
3282
- const cache = parseCache(JSON.parse(await readFile7(remotePath, "utf8")));
3721
+ const cache = parseCache(JSON.parse(await readFile8(remotePath, "utf8")));
3283
3722
  for (const id of remoteHashes.invalidIds) delete cache.hashes[id];
3284
3723
  removed.remoteHashEntries = remoteHashes.invalidIds.length;
3285
3724
  await saveCache(remotePath, cache);
@@ -3318,12 +3757,12 @@ async function inspectContentCache(contentDir, referenced) {
3318
3757
  continue;
3319
3758
  }
3320
3759
  report.files++;
3321
- const file = path8.join(contentDir, entry.name);
3760
+ const file = path9.join(contentDir, entry.name);
3322
3761
  const mediaType = mediaTypeFromExtension(entry.name);
3323
3762
  const expected = mediaType ? entry.name.slice(0, -4) : "";
3324
3763
  let bytes;
3325
3764
  try {
3326
- bytes = await readFile7(file);
3765
+ bytes = await readFile8(file);
3327
3766
  report.bytes += bytes.length;
3328
3767
  } catch (err) {
3329
3768
  report.invalid.push({
@@ -3370,7 +3809,7 @@ async function inspectRemoteHashCache(remotePath) {
3370
3809
  if (!report.exists) return report;
3371
3810
  let cache;
3372
3811
  try {
3373
- cache = parseCache(JSON.parse(await readFile7(remotePath, "utf8")));
3812
+ cache = parseCache(JSON.parse(await readFile8(remotePath, "utf8")));
3374
3813
  } catch (err) {
3375
3814
  report.error = err instanceof Error ? err.message : String(err);
3376
3815
  return report;
@@ -3386,18 +3825,18 @@ async function inspectRemoteHashCache(remotePath) {
3386
3825
 
3387
3826
  // src/pipeline/pack.ts
3388
3827
  import { readFileSync as readFileSync2 } from "fs";
3389
- import path9 from "path";
3828
+ import path10 from "path";
3390
3829
  function resolvePackInputs(raw, inputsFilePath) {
3391
3830
  if (!Array.isArray(raw) || !raw.length) {
3392
3831
  throw new Error("--inputs must be a non-empty JSON array of { id, path }");
3393
3832
  }
3394
- const baseDir = path9.dirname(path9.resolve(inputsFilePath));
3833
+ const baseDir = path10.dirname(path10.resolve(inputsFilePath));
3395
3834
  return raw.map((entry, i) => {
3396
3835
  const e = entry;
3397
3836
  if (typeof e.id !== "string" || typeof e.path !== "string") {
3398
3837
  throw new Error(`--inputs[${i}] needs string "id" and "path"`);
3399
3838
  }
3400
- return { id: e.id, path: path9.resolve(baseDir, e.path) };
3839
+ return { id: e.id, path: path10.resolve(baseDir, e.path) };
3401
3840
  });
3402
3841
  }
3403
3842
  function packSprites(inputs, options = {}) {
@@ -3594,7 +4033,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
3594
4033
  const cell = cells[id];
3595
4034
  const source = sources[id];
3596
4035
  if (source) {
3597
- placements.push({ id, path: path9.resolve(manifestDir, source), cell });
4036
+ placements.push({ id, path: path10.resolve(manifestDir, source), cell });
3598
4037
  continue;
3599
4038
  }
3600
4039
  const entry = lock.entries[`${styleId}/${id}`];
@@ -3618,7 +4057,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
3618
4057
  `Nothing to mount for style "${styleId}": ${ids.length} asset(s) declare a cell, but none has generated output or a \`source\`. Run \`pixelkiln gen --style ${styleId}\`, or point each at a file.${detail}`
3619
4058
  );
3620
4059
  }
3621
- const basePng = mount.base ? readFileSync2(path9.resolve(manifestDir, mount.base)) : void 0;
4060
+ const basePng = mount.base ? readFileSync2(path10.resolve(manifestDir, mount.base)) : void 0;
3622
4061
  const mounted = mountSprites(placements, {
3623
4062
  cellWidth: mount.cellWidth,
3624
4063
  cellHeight: mount.cellHeight,
@@ -3628,7 +4067,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
3628
4067
  ...mounted,
3629
4068
  sources: mount.base ? [{
3630
4069
  id: "$base",
3631
- path: path9.resolve(manifestDir, mount.base),
4070
+ path: path10.resolve(manifestDir, mount.base),
3632
4071
  sha256: sha256(basePng),
3633
4072
  included: true
3634
4073
  }, ...mounted.sources] : mounted.sources,
@@ -4040,7 +4479,11 @@ async function poll(provider, lock, lockPath, opts = {}) {
4040
4479
  spec: currentSpec
4041
4480
  });
4042
4481
  if (state.status === "review") {
4043
- upsert(lock, key, { status: "review", reviewObjectId: entry.jobId });
4482
+ upsert(lock, key, {
4483
+ status: "review",
4484
+ reviewObjectId: entry.jobId,
4485
+ providerMetadata: state.metadata ? { ...entry.providerMetadata, [provider.id]: state.metadata } : entry.providerMetadata
4486
+ });
4044
4487
  result.review++;
4045
4488
  log(` review ${key} (${state.candidateUrls.length} candidates)`);
4046
4489
  } else if (state.status === "ready") {
@@ -4073,15 +4516,15 @@ async function poll(provider, lock, lockPath, opts = {}) {
4073
4516
 
4074
4517
  // src/pipeline/fetch.ts
4075
4518
  import { existsSync as existsSync8 } from "fs";
4076
- import { mkdir as mkdir4, readFile as readFile8, rename as rename4, rm as rm5, writeFile as writeFile4 } from "fs/promises";
4077
- import path10 from "path";
4519
+ import { mkdir as mkdir4, readFile as readFile9, rename as rename4, rm as rm5, writeFile as writeFile4 } from "fs/promises";
4520
+ import path11 from "path";
4078
4521
  async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
4079
4522
  const log = opts.onProgress ?? (() => {
4080
4523
  });
4081
4524
  const result = { downloaded: 0, skipped: 0, failed: 0 };
4082
4525
  const specByKey = new Map(specs.map((s) => [lockKey(s.styleId, s.assetId), s]));
4083
4526
  normalizeLockOutputPaths(lock, specs);
4084
- const cacheDir = opts.cacheDir === false ? null : path10.resolve(opts.cacheDir ?? path10.join(path10.dirname(lockPath), ".pixelkiln", "cache"));
4527
+ const cacheDir = opts.cacheDir === false ? null : path11.resolve(opts.cacheDir ?? path11.join(path11.dirname(lockPath), ".pixelkiln", "cache"));
4085
4528
  const pending = Object.entries(lock.entries).filter(([key, e]) => {
4086
4529
  if (e.status === "selected" || e.status === "download-failed") return true;
4087
4530
  if (!opts.repair || e.status !== "downloaded") return false;
@@ -4133,7 +4576,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
4133
4576
  if (cacheDir) {
4134
4577
  await cacheMedia(
4135
4578
  cacheDir,
4136
- await readFile8(target),
4579
+ await readFile9(target),
4137
4580
  recorded.mediaType ?? MediaType.PNG,
4138
4581
  recorded.sha256
4139
4582
  );
@@ -4144,7 +4587,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
4144
4587
  const expectedMediaType = source.mediaType ?? recorded?.mediaType ?? MediaType.PNG;
4145
4588
  let buf = recorded && cacheDir ? await readCachedMedia(cacheDir, recorded.sha256, expectedMediaType) : null;
4146
4589
  if (buf) {
4147
- log(` cached ${path10.relative(process.cwd(), target)}`);
4590
+ log(` cached ${path11.relative(process.cwd(), target)}`);
4148
4591
  } else {
4149
4592
  if (!source.url) {
4150
4593
  throw new Error(`no source URL or cached bytes remain for ${source.role ?? "asset"}`);
@@ -4162,7 +4605,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
4162
4605
  );
4163
4606
  }
4164
4607
  if (cacheDir) await cacheMedia(cacheDir, buf, mediaType);
4165
- await mkdir4(path10.dirname(target), { recursive: true });
4608
+ await mkdir4(path11.dirname(target), { recursive: true });
4166
4609
  const tmp = `${target}.pixelkiln.tmp`;
4167
4610
  await writeFile4(tmp, buf);
4168
4611
  outputs.push({
@@ -4174,7 +4617,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
4174
4617
  upsert(lock, key, { outputs: mergeOutputs(entry.outputs, outputs) });
4175
4618
  await saveLock(lockPath, lock);
4176
4619
  await rename4(tmp, target);
4177
- log(` wrote ${path10.relative(process.cwd(), target)}`);
4620
+ log(` wrote ${path11.relative(process.cwd(), target)}`);
4178
4621
  }
4179
4622
  upsert(lock, key, {
4180
4623
  status: "downloaded",
@@ -4207,10 +4650,10 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
4207
4650
  return result;
4208
4651
  }
4209
4652
  async function readCachedMedia(cacheDir, hash, mediaType) {
4210
- const file = path10.join(cacheDir, cacheFileName(hash, mediaType));
4653
+ const file = path11.join(cacheDir, cacheFileName(hash, mediaType));
4211
4654
  if (!existsSync8(file)) return null;
4212
4655
  try {
4213
- const buf = await readFile8(file);
4656
+ const buf = await readFile9(file);
4214
4657
  if (sha256(buf) !== hash) return null;
4215
4658
  validateMedia(buf, mediaType);
4216
4659
  return buf;
@@ -4221,7 +4664,7 @@ async function readCachedMedia(cacheDir, hash, mediaType) {
4221
4664
  async function cacheMedia(cacheDir, buf, mediaType, knownHash) {
4222
4665
  const hash = knownHash ?? sha256(buf);
4223
4666
  validateMedia(buf, mediaType);
4224
- const file = path10.join(cacheDir, cacheFileName(hash, mediaType));
4667
+ const file = path11.join(cacheDir, cacheFileName(hash, mediaType));
4225
4668
  if (await readCachedMedia(cacheDir, hash, mediaType)) return;
4226
4669
  await mkdir4(cacheDir, { recursive: true });
4227
4670
  const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
@@ -4264,7 +4707,7 @@ async function pushTags(provider, specs, lock, opts = {}) {
4264
4707
  // src/pipeline/doctor.ts
4265
4708
  import { constants, existsSync as existsSync9 } from "fs";
4266
4709
  import { access, stat as stat2 } from "fs/promises";
4267
- import path11 from "path";
4710
+ import path12 from "path";
4268
4711
  async function doctor(loaded, specs, lock, lockPath, opts = {}) {
4269
4712
  const checks = [];
4270
4713
  const add = (id, level, message2) => checks.push({ id, level, message: message2 });
@@ -4274,7 +4717,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
4274
4717
  `${Object.keys(loaded.manifest.styles).length} style(s), ${Object.keys(loaded.manifest.assets).length} asset(s), ${specs.length} resolved spec(s)`
4275
4718
  );
4276
4719
  add("lock", "ok", `${Object.keys(lock.entries).length} valid v2 lock entr(ies)`);
4277
- const dirs = [...new Set(specs.map((s) => path11.dirname(s.outFile)))];
4720
+ const dirs = [...new Set(specs.map((s) => path12.dirname(s.outFile)))];
4278
4721
  const unwritable = [];
4279
4722
  for (const dir of dirs) {
4280
4723
  const ancestor = await nearestExistingDirectory(dir);
@@ -4296,7 +4739,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
4296
4739
  for (let index = 0; index < entry.outputs.length; index++) {
4297
4740
  const output = entry.outputs[index];
4298
4741
  const spec = specByKey.get(key);
4299
- const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, path11.dirname(lockPath));
4742
+ const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, path12.dirname(lockPath));
4300
4743
  const owner = outputOwners.get(absolute);
4301
4744
  if (owner && owner !== key) duplicateOutputs.push(`${output.path} (${owner}, ${key})`);
4302
4745
  outputOwners.set(absolute, key);
@@ -4311,7 +4754,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
4311
4754
  } else {
4312
4755
  add("lock-outputs", "ok", `${outputOwners.size} recorded output path(s) are uniquely owned`);
4313
4756
  }
4314
- const cacheDir = path11.join(path11.dirname(lockPath), ".pixelkiln", "cache");
4757
+ const cacheDir = path12.join(path12.dirname(lockPath), ".pixelkiln", "cache");
4315
4758
  const stranded = [];
4316
4759
  const invalidState = [];
4317
4760
  const staleMapUrls = [];
@@ -4327,7 +4770,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
4327
4770
  const hasSource = Boolean(entry.sourceUrl || entry.sourceUrls?.length);
4328
4771
  let hasCache = false;
4329
4772
  for (const output of entry.outputs) {
4330
- const cached = path11.join(
4773
+ const cached = path12.join(
4331
4774
  cacheDir,
4332
4775
  cacheFileName(output.sha256, output.mediaType ?? MediaType.PNG)
4333
4776
  );
@@ -4362,8 +4805,15 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
4362
4805
  "error",
4363
4806
  opts.apiKeyPresent === false ? `${opts.credentialEnv ?? "PIXELLAB_API_KEY"} is not configured` : "provider is not configured"
4364
4807
  );
4808
+ } else if (!opts.provider.balance && !opts.provider.checkConnection) {
4809
+ add("provider", "ok", `${opts.provider.id} configured; connectivity check unavailable`);
4365
4810
  } else if (!opts.provider.balance) {
4366
- add("provider", "ok", `${opts.provider.id} configured; balance reporting unavailable`);
4811
+ try {
4812
+ await opts.provider.checkConnection();
4813
+ add("provider", "ok", `${opts.provider.id} reachable; balance reporting unavailable`);
4814
+ } catch (err) {
4815
+ add("provider", "error", `provider connectivity failed: ${err instanceof Error ? err.message : String(err)}`);
4816
+ }
4367
4817
  } else {
4368
4818
  const balanceFn = opts.provider.balance.bind(opts.provider);
4369
4819
  try {
@@ -4382,7 +4832,7 @@ async function nearestExistingDirectory(start) {
4382
4832
  if ((await stat2(current)).isDirectory()) return current;
4383
4833
  } catch {
4384
4834
  }
4385
- const parent = path11.dirname(current);
4835
+ const parent = path12.dirname(current);
4386
4836
  if (parent === current) return current;
4387
4837
  current = parent;
4388
4838
  }
@@ -4390,8 +4840,8 @@ async function nearestExistingDirectory(start) {
4390
4840
 
4391
4841
  // src/pipeline/adopt.ts
4392
4842
  import { existsSync as existsSync10 } from "fs";
4393
- import { readFile as readFile9, writeFile as writeFile5 } from "fs/promises";
4394
- import path12 from "path";
4843
+ import { readFile as readFile10, writeFile as writeFile5 } from "fs/promises";
4844
+ import path13 from "path";
4395
4845
  async function adopt(provider, specs, lock, lockPath, opts = {}) {
4396
4846
  const log = opts.onProgress ?? (() => {
4397
4847
  });
@@ -4406,7 +4856,7 @@ async function adopt(provider, specs, lock, lockPath, opts = {}) {
4406
4856
  unmatchedLocal.push(`${spec.styleId}/${spec.assetId} (no file at ${spec.outFile})`);
4407
4857
  continue;
4408
4858
  }
4409
- const bytes = await readFile9(spec.outFile);
4859
+ const bytes = await readFile10(spec.outFile);
4410
4860
  try {
4411
4861
  decodePng(bytes);
4412
4862
  } catch (err) {
@@ -4882,9 +5332,9 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
4882
5332
  }
4883
5333
 
4884
5334
  // src/pipeline/init.ts
4885
- import { readdir as readdir2, readFile as readFile10, writeFile as writeFile6 } from "fs/promises";
5335
+ import { readdir as readdir2, readFile as readFile11, writeFile as writeFile6 } from "fs/promises";
4886
5336
  import { existsSync as existsSync11 } from "fs";
4887
- import path13 from "path";
5337
+ import path14 from "path";
4888
5338
  function pngSize(buf) {
4889
5339
  if (buf.length < 24) return null;
4890
5340
  const isPng = buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834;
@@ -4898,7 +5348,7 @@ function slugify(value) {
4898
5348
  async function walk(dir, exclude) {
4899
5349
  const out = [];
4900
5350
  for (const entry of await readdir2(dir, { withFileTypes: true })) {
4901
- const full = path13.join(dir, entry.name);
5351
+ const full = path14.join(dir, entry.name);
4902
5352
  if (exclude.some((x) => full.includes(x))) continue;
4903
5353
  if (entry.isDirectory()) out.push(...await walk(full, exclude));
4904
5354
  else if (entry.name.toLowerCase().endsWith(".png")) out.push(full);
@@ -4912,14 +5362,14 @@ async function scanAssets(root, opts = {}) {
4912
5362
  const skipped = [];
4913
5363
  const seen = /* @__PURE__ */ new Map();
4914
5364
  for (const file of files.sort()) {
4915
- const size = pngSize(await readFile10(file));
5365
+ const size = pngSize(await readFile11(file));
4916
5366
  if (!size) {
4917
5367
  skipped.push(`${file} (not a readable PNG)`);
4918
5368
  continue;
4919
5369
  }
4920
- const rel = path13.relative(root, file);
4921
- const category = path13.dirname(rel) === "." ? "" : path13.dirname(rel);
4922
- let id = slugify(path13.basename(rel));
5370
+ const rel = path14.relative(root, file);
5371
+ const category = path14.dirname(rel) === "." ? "" : path14.dirname(rel);
5372
+ let id = slugify(path14.basename(rel));
4923
5373
  if (seen.has(id)) {
4924
5374
  const n = seen.get(id) + 1;
4925
5375
  seen.set(id, n);
@@ -4975,16 +5425,16 @@ function mode(values) {
4975
5425
  }
4976
5426
 
4977
5427
  // src/pipeline/salvage.ts
4978
- import { readFile as readFile11 } from "fs/promises";
5428
+ import { readFile as readFile12 } from "fs/promises";
4979
5429
  import { existsSync as existsSync12 } from "fs";
4980
- import path14 from "path";
5430
+ import path15 from "path";
4981
5431
  async function loadClaims(lockPaths) {
4982
5432
  const claimed = /* @__PURE__ */ new Set();
4983
5433
  for (const p of lockPaths) {
4984
5434
  if (!existsSync12(p)) throw new Error(`Claim lockfile not found: ${p}`);
4985
5435
  let parsed;
4986
5436
  try {
4987
- parsed = parseLock(JSON.parse(await readFile11(p, "utf8")));
5437
+ parsed = parseLock(JSON.parse(await readFile12(p, "utf8")));
4988
5438
  } catch {
4989
5439
  throw new Error(`Claim lockfile is malformed: ${p}`);
4990
5440
  }
@@ -5031,20 +5481,20 @@ function matchOrphanStyle(prompt, manifest) {
5031
5481
  return matchStyleByPattern(prompt, manifest);
5032
5482
  }
5033
5483
  async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, claimPaths) {
5034
- const own = path14.resolve(ownManifestPath);
5484
+ const own = path15.resolve(ownManifestPath);
5035
5485
  const siblingManifestPaths = [
5036
5486
  .../* @__PURE__ */ new Set([
5037
5487
  ...workspaceManifestPaths,
5038
- ...claimPaths.map((c) => path14.join(path14.dirname(path14.resolve(c)), "pixelkiln.manifest.json"))
5488
+ ...claimPaths.map((c) => path15.join(path15.dirname(path15.resolve(c)), "pixelkiln.manifest.json"))
5039
5489
  ])
5040
5490
  ];
5041
5491
  const siblings = [];
5042
5492
  for (const siblingManifestPath of siblingManifestPaths) {
5043
- if (path14.resolve(siblingManifestPath) === own) continue;
5493
+ if (path15.resolve(siblingManifestPath) === own) continue;
5044
5494
  if (!existsSync12(siblingManifestPath)) continue;
5045
5495
  try {
5046
5496
  const { manifest } = await loadManifest(siblingManifestPath);
5047
- siblings.push({ label: path14.basename(path14.dirname(siblingManifestPath)), manifest });
5497
+ siblings.push({ label: path15.basename(path15.dirname(siblingManifestPath)), manifest });
5048
5498
  } catch {
5049
5499
  }
5050
5500
  }
@@ -5160,8 +5610,8 @@ async function applyTags(provider, decisions, existing, opts = {}) {
5160
5610
  }
5161
5611
 
5162
5612
  // src/pick/salvage-server.ts
5163
- import { mkdir as mkdir5, writeFile as writeFile7, readFile as readFile12 } from "fs/promises";
5164
- import path15 from "path";
5613
+ import { mkdir as mkdir5, writeFile as writeFile7, readFile as readFile13 } from "fs/promises";
5614
+ import path16 from "path";
5165
5615
 
5166
5616
  // src/pick/salvage-sheet.ts
5167
5617
  var escapeHtml2 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
@@ -5326,7 +5776,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
5326
5776
  });
5327
5777
  const html = renderSalvageSheet(orphans, {
5328
5778
  styleId: ctx.styleId,
5329
- importDir: path15.relative(process.cwd(), ctx.importDir) || "."
5779
+ importDir: path16.relative(process.cwd(), ctx.importDir) || "."
5330
5780
  });
5331
5781
  const byId = new Map(orphans.map((o) => [o.id, o]));
5332
5782
  const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
@@ -5355,9 +5805,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
5355
5805
  if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
5356
5806
  decodePng(buf);
5357
5807
  const assetId = idFromPrompt(orphan.prompt, taken);
5358
- const rel = path15.join("_salvaged", `${assetId}.png`);
5359
- const outFile = path15.resolve(ctx.importDir, rel);
5360
- await mkdir5(path15.dirname(outFile), { recursive: true });
5808
+ const rel = path16.join("_salvaged", `${assetId}.png`);
5809
+ const outFile = path16.resolve(ctx.importDir, rel);
5810
+ await mkdir5(path16.dirname(outFile), { recursive: true });
5361
5811
  await writeFile7(outFile, buf);
5362
5812
  ctx.manifest.assets[assetId] = {
5363
5813
  prompt: orphan.prompt,
@@ -5384,7 +5834,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
5384
5834
  error: null,
5385
5835
  sourceUrl: orphan.previewUrl,
5386
5836
  outputs: [{
5387
- path: portableOutputPath(outFile, path15.dirname(ctx.manifestPath)),
5837
+ path: portableOutputPath(outFile, path16.dirname(ctx.manifestPath)),
5388
5838
  sha256: sha256(buf)
5389
5839
  }],
5390
5840
  submittedAt: orphan.createdAt,
@@ -5408,7 +5858,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
5408
5858
  }
5409
5859
  }
5410
5860
  await applyTags(provider, decisions, existingTags, { onProgress: log });
5411
- const raw = JSON.parse(await readFile12(ctx.manifestPath, "utf8"));
5861
+ const raw = JSON.parse(await readFile13(ctx.manifestPath, "utf8"));
5412
5862
  for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
5413
5863
  await writeFile7(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
5414
5864
  await saveLock(ctx.lockPath, ctx.lock);
@@ -5418,9 +5868,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
5418
5868
  }
5419
5869
 
5420
5870
  // src/workspace.ts
5421
- import { mkdir as mkdir6, readFile as readFile13, rename as rename5, rm as rm6, writeFile as writeFile8 } from "fs/promises";
5871
+ import { mkdir as mkdir6, readFile as readFile14, rename as rename5, rm as rm6, writeFile as writeFile8 } from "fs/promises";
5422
5872
  import { existsSync as existsSync13 } from "fs";
5423
- import path16 from "path";
5873
+ import path17 from "path";
5424
5874
  import { z as z4 } from "zod";
5425
5875
  var WorkspaceProjectSchema = z4.object({
5426
5876
  id: z4.string().min(1),
@@ -5448,7 +5898,7 @@ async function loadWorkspace(workspacePath) {
5448
5898
  if (!existsSync13(workspacePath)) return { version: 1, projects: [] };
5449
5899
  let raw;
5450
5900
  try {
5451
- raw = JSON.parse(await readFile13(workspacePath, "utf8"));
5901
+ raw = JSON.parse(await readFile14(workspacePath, "utf8"));
5452
5902
  } catch (err) {
5453
5903
  throw new Error(
5454
5904
  `Workspace catalog at ${workspacePath} is malformed:
@@ -5462,7 +5912,7 @@ async function saveWorkspace(workspacePath, ws) {
5462
5912
  version: 1,
5463
5913
  projects: [...ws.projects].sort((a, b) => a.id.localeCompare(b.id))
5464
5914
  };
5465
- await mkdir6(path16.dirname(path16.resolve(workspacePath)), { recursive: true });
5915
+ await mkdir6(path17.dirname(path17.resolve(workspacePath)), { recursive: true });
5466
5916
  const tmp = `${workspacePath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
5467
5917
  try {
5468
5918
  await writeFile8(tmp, JSON.stringify(sorted, null, 2) + "\n");
@@ -5472,12 +5922,12 @@ async function saveWorkspace(workspacePath, ws) {
5472
5922
  }
5473
5923
  }
5474
5924
  function toPortablePath(dir, absolute) {
5475
- return path16.relative(dir, absolute).split(path16.sep).join("/");
5925
+ return path17.relative(dir, absolute).split(path17.sep).join("/");
5476
5926
  }
5477
5927
  function resolveProject(dir, project) {
5478
5928
  return {
5479
- manifestPath: path16.resolve(dir, project.manifest.split("/").join(path16.sep)),
5480
- lockPath: path16.resolve(dir, project.lock.split("/").join(path16.sep))
5929
+ manifestPath: path17.resolve(dir, project.manifest.split("/").join(path17.sep)),
5930
+ lockPath: path17.resolve(dir, project.lock.split("/").join(path17.sep))
5481
5931
  };
5482
5932
  }
5483
5933
  function validateWorkspace(ws, dir) {
@@ -5490,7 +5940,7 @@ function validateWorkspace(ws, dir) {
5490
5940
  const { manifestPath, lockPath } = resolveProject(dir, project);
5491
5941
  lockOwners.set(lockPath, [...lockOwners.get(lockPath) ?? [], project.id]);
5492
5942
  manifestOwners.set(manifestPath, [...manifestOwners.get(manifestPath) ?? [], project.id]);
5493
- if (path16.isAbsolute(project.manifest) || path16.isAbsolute(project.lock)) {
5943
+ if (path17.isAbsolute(project.manifest) || path17.isAbsolute(project.lock)) {
5494
5944
  diagnostics.push({
5495
5945
  id: "absolute-path",
5496
5946
  level: "warning",
@@ -5645,6 +6095,7 @@ async function workspaceStatus(ws, dir) {
5645
6095
  }
5646
6096
  export {
5647
6097
  AssetSchema,
6098
+ ComfyUIProvider,
5648
6099
  DEFAULT_RATE_LIMIT,
5649
6100
  FAKE_PNG,
5650
6101
  FakeProvider,