pixelkiln 0.6.0 → 0.7.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.cjs CHANGED
@@ -31,6 +31,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
33
  AssetSchema: () => AssetSchema,
34
+ ComfyUIProvider: () => ComfyUIProvider,
34
35
  DEFAULT_RATE_LIMIT: () => DEFAULT_RATE_LIMIT,
35
36
  FAKE_PNG: () => FAKE_PNG,
36
37
  FakeProvider: () => FakeProvider,
@@ -202,8 +203,8 @@ var StyleSchema = import_zod.z.object({
202
203
  // `map` is the default because it is 20-40x cheaper and correct for any
203
204
  // asset that is not going to be rotated or animated.
204
205
  generator: GeneratorSchema.default("map"),
205
- /** Square edge length for `1dir`. 32-256. */
206
- size: import_zod.z.number().int().min(32).max(256).optional(),
206
+ /** Square edge length. The active provider owns its exact limit. */
207
+ size: import_zod.z.number().int().min(16).max(8192).optional(),
207
208
  view: import_zod.z.string().optional(),
208
209
  /** Appended to every asset prompt in this style. Where the look is defined. */
209
210
  promptSuffix: import_zod.z.string().default(""),
@@ -319,8 +320,8 @@ var AssetSchema = import_zod.z.object({
319
320
  /** Subdirectory under the style's outDir. Optional. */
320
321
  category: import_zod.z.string().optional(),
321
322
  /** Overrides the style default. `map` generator only. */
322
- width: import_zod.z.number().int().min(16).max(400).optional(),
323
- height: import_zod.z.number().int().min(16).max(400).optional(),
323
+ width: import_zod.z.number().int().min(16).max(8192).optional(),
324
+ height: import_zod.z.number().int().min(16).max(8192).optional(),
324
325
  /** Overrides the style default. `1dir` generator only. */
325
326
  size: import_zod.z.number().int().min(32).max(256).optional(),
326
327
  /** Explicit output path relative to outDir. Media-aware providers may replace its extension. */
@@ -578,11 +579,11 @@ var PixelLabClient = class {
578
579
  * common than the failure mode of retrying (a duplicate object), and a
579
580
  * duplicate is visible and free to delete whereas a silent gap is neither.
580
581
  */
581
- async request(path17, init, attempt = 0) {
582
+ async request(path18, init, attempt = 0) {
582
583
  const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
583
584
  let res;
584
585
  try {
585
- res = await fetch(`${BASE}${path17}`, {
586
+ res = await fetch(`${BASE}${path18}`, {
586
587
  ...init,
587
588
  signal: init?.signal ?? AbortSignal.timeout(this.timeoutMs),
588
589
  headers: {
@@ -594,24 +595,24 @@ var PixelLabClient = class {
594
595
  } catch (err) {
595
596
  if (attempt < MAX_RETRIES) {
596
597
  await sleep(backoffMs(attempt));
597
- return this.request(path17, init, attempt + 1);
598
+ return this.request(path18, init, attempt + 1);
598
599
  }
599
600
  throw err;
600
601
  }
601
602
  if (!res.ok && shouldRetry(res.status) && attempt < MAX_RETRIES) {
602
603
  const waitMs = retryAfterMs(res.headers.get("retry-after")) ?? backoffMs(attempt);
603
604
  await sleep(waitMs);
604
- return this.request(path17, init, attempt + 1);
605
+ return this.request(path18, init, attempt + 1);
605
606
  }
606
607
  const text = await res.text();
607
608
  if (!res.ok) {
608
- throw new PixelLabError(`${init?.method ?? "GET"} ${path17} \u2192 ${res.status}`, res.status, text);
609
+ throw new PixelLabError(`${init?.method ?? "GET"} ${path18} \u2192 ${res.status}`, res.status, text);
609
610
  }
610
611
  if (!text) return {};
611
612
  try {
612
613
  return JSON.parse(text);
613
614
  } catch {
614
- throw new Error(`${init?.method ?? "GET"} ${path17} returned invalid JSON`);
615
+ throw new Error(`${init?.method ?? "GET"} ${path18} returned invalid JSON`);
615
616
  }
616
617
  }
617
618
  async balance() {
@@ -1375,6 +1376,12 @@ var PixelLabProvider = class _PixelLabProvider {
1375
1376
  };
1376
1377
  }
1377
1378
  validate(spec, styleImages) {
1379
+ if (spec.generator === "1dir" && (spec.width < 32 || spec.width > 256)) {
1380
+ throw new Error("PixelLab 1dir dimensions must be between 32 and 256 pixels");
1381
+ }
1382
+ if ((spec.generator === "map" || spec.generator === "pixflux") && (spec.width < 16 || spec.height < 16 || spec.width > 400 || spec.height > 400)) {
1383
+ throw new Error(`PixelLab ${spec.generator} dimensions must be between 16 and 400 pixels`);
1384
+ }
1378
1385
  if (spec.generator === "map") {
1379
1386
  requirePixelLabOption("view", spec.view, ["low top-down", "high top-down", "side"]);
1380
1387
  requirePixelLabOption("outline", spec.outline, [
@@ -1651,9 +1658,9 @@ var RetroDiffusionClient = class {
1651
1658
  }
1652
1659
  return balance;
1653
1660
  }
1654
- async call(path17, init = {}) {
1661
+ async call(path18, init = {}) {
1655
1662
  if (!this.token) throw new Error("RD_API_KEY is not set");
1656
- const response = await this.request(`${this.baseUrl}${path17}`, {
1663
+ const response = await this.request(`${this.baseUrl}${path18}`, {
1657
1664
  ...init,
1658
1665
  headers: {
1659
1666
  "Content-Type": "application/json",
@@ -1923,8 +1930,470 @@ function retroError(value) {
1923
1930
  return JSON.stringify(value);
1924
1931
  }
1925
1932
 
1926
- // src/providers/fake.ts
1933
+ // src/providers/comfyui.ts
1934
+ var import_node_crypto3 = require("crypto");
1935
+ var import_promises2 = require("fs/promises");
1936
+ var import_node_path2 = __toESM(require("path"), 1);
1937
+
1938
+ // src/hash.ts
1927
1939
  var import_node_crypto2 = require("crypto");
1940
+ var import_promises = require("fs/promises");
1941
+ function sha256(data) {
1942
+ return (0, import_node_crypto2.createHash)("sha256").update(data).digest("hex");
1943
+ }
1944
+ async function sha256File(path18) {
1945
+ return sha256(await (0, import_promises.readFile)(path18));
1946
+ }
1947
+ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.providerOptions) {
1948
+ return sha256(
1949
+ JSON.stringify({
1950
+ // Preserve every existing PixelLab hash while making a provider switch
1951
+ // invalidate the spec. Older manifests implicitly mean pixellab.
1952
+ provider: spec.provider === "pixellab" ? void 0 : spec.provider,
1953
+ providerOptions: providerOptionIdentity && (typeof providerOptionIdentity !== "object" || Object.keys(providerOptionIdentity).length > 0) ? providerOptionIdentity : void 0,
1954
+ generator: spec.generator,
1955
+ prompt: spec.prompt,
1956
+ width: spec.width,
1957
+ height: spec.height,
1958
+ view: spec.view,
1959
+ outline: spec.outline ?? null,
1960
+ shading: spec.shading ?? null,
1961
+ detail: spec.detail ?? null,
1962
+ seed: spec.seed ?? null,
1963
+ palette: spec.palette,
1964
+ // `noBackground` only reaches the wire for pixflux; the tile fields are
1965
+ // undefined for every other generator. `tileSize` is intentionally
1966
+ // absent — width/height are derived from it, so it is already covered.
1967
+ noBackground: spec.generator === "pixflux" || spec.provider !== "pixellab" ? spec.noBackground : void 0,
1968
+ tileType: spec.tileType,
1969
+ tileView: spec.tileView,
1970
+ tileFeature: spec.tileFeature,
1971
+ outlineMode: spec.outlineMode,
1972
+ styleImages: styleImageHashes
1973
+ })
1974
+ );
1975
+ }
1976
+
1977
+ // src/providers/comfyui.ts
1978
+ var DEFAULT_BASE_URL2 = "http://127.0.0.1:8188";
1979
+ var ComfyUIClient = class {
1980
+ constructor(baseUrl = process.env.COMFYUI_BASE_URL ?? DEFAULT_BASE_URL2, request = fetch) {
1981
+ this.request = request;
1982
+ this.baseUrl = normalizeBaseUrl(baseUrl);
1983
+ }
1984
+ request;
1985
+ baseUrl;
1986
+ async checkConnection() {
1987
+ const value = await this.json("system_stats");
1988
+ if (!isObject(value)) throw new Error("ComfyUI returned invalid system stats");
1989
+ }
1990
+ async submit(workflow) {
1991
+ const value = await this.json("prompt", {
1992
+ method: "POST",
1993
+ body: JSON.stringify({ prompt: workflow, client_id: (0, import_node_crypto3.randomUUID)() })
1994
+ });
1995
+ if (!isObject(value)) throw new Error("ComfyUI returned an invalid queue response");
1996
+ const promptId = value.prompt_id;
1997
+ if (typeof promptId !== "string" || !promptId) {
1998
+ const nodes = isObject(value.node_errors) ? Object.keys(value.node_errors) : [];
1999
+ throw new Error(
2000
+ "ComfyUI did not return a prompt id" + (nodes.length ? `; invalid workflow node(s): ${nodes.slice(0, 8).join(", ")}` : "")
2001
+ );
2002
+ }
2003
+ return promptId;
2004
+ }
2005
+ async history(promptId) {
2006
+ return this.json(`history/${encodeURIComponent(promptId)}`);
2007
+ }
2008
+ viewUrl(image) {
2009
+ const url = this.endpoint("view");
2010
+ url.searchParams.set("filename", image.filename);
2011
+ url.searchParams.set("subfolder", image.subfolder);
2012
+ url.searchParams.set("type", image.type);
2013
+ return url.toString();
2014
+ }
2015
+ async download(source) {
2016
+ let target = source;
2017
+ if (source.startsWith("comfyui://")) target = this.viewUrl(parseSource(source));
2018
+ const response = await this.request(target);
2019
+ if (!response.ok) throw new Error(`ComfyUI download failed (${response.status})`);
2020
+ return Buffer.from(await response.arrayBuffer());
2021
+ }
2022
+ endpoint(route) {
2023
+ return new URL(route.replace(/^\/+/, ""), `${this.baseUrl}/`);
2024
+ }
2025
+ async json(route, init = {}) {
2026
+ const url = this.endpoint(route);
2027
+ const response = await this.request(url, {
2028
+ ...init,
2029
+ headers: {
2030
+ ...init.body ? { "Content-Type": "application/json" } : {},
2031
+ ...init.headers
2032
+ }
2033
+ });
2034
+ const text = await response.text();
2035
+ let value = null;
2036
+ try {
2037
+ value = text ? JSON.parse(text) : null;
2038
+ } catch {
2039
+ if (response.ok) throw new Error(`ComfyUI returned invalid JSON from ${url.pathname}`);
2040
+ value = null;
2041
+ }
2042
+ if (!response.ok) {
2043
+ const detail = apiError(value);
2044
+ throw new Error(
2045
+ `ComfyUI request failed (${response.status}) at ${url.pathname}` + (detail ? `: ${detail}` : "")
2046
+ );
2047
+ }
2048
+ return value;
2049
+ }
2050
+ };
2051
+ var ComfyUIProvider = class _ComfyUIProvider {
2052
+ constructor(client = new ComfyUIClient()) {
2053
+ this.client = client;
2054
+ }
2055
+ client;
2056
+ id = "comfyui";
2057
+ static fromEnv() {
2058
+ return new _ComfyUIProvider();
2059
+ }
2060
+ static forOffline() {
2061
+ return new _ComfyUIProvider(new ComfyUIClient(DEFAULT_BASE_URL2));
2062
+ }
2063
+ static forDownloads() {
2064
+ return new _ComfyUIProvider();
2065
+ }
2066
+ async resolveOptions(value, context) {
2067
+ const options = parseOptions(value);
2068
+ const workflowPath = import_node_path2.default.resolve(context.root, options.workflowFile);
2069
+ let raw;
2070
+ try {
2071
+ raw = await (0, import_promises2.readFile)(workflowPath, "utf8");
2072
+ } catch (err) {
2073
+ throw new Error(
2074
+ `ComfyUI workflow for style "${context.styleId}" could not be read at ${workflowPath}: ${err instanceof Error ? err.message : String(err)}`
2075
+ );
2076
+ }
2077
+ let parsed;
2078
+ try {
2079
+ parsed = JSON.parse(raw);
2080
+ } catch {
2081
+ throw new Error(`ComfyUI workflow for style "${context.styleId}" is not valid JSON: ${workflowPath}`);
2082
+ }
2083
+ const workflow = parseWorkflow(parsed, workflowPath);
2084
+ validateWorkflowBindings(workflow, options);
2085
+ const workflowSha256 = sha256(JSON.stringify(canonical(workflow)));
2086
+ const resolved = { ...options, workflow, workflowSha256 };
2087
+ return {
2088
+ options: resolved,
2089
+ identity: {
2090
+ outputNodeId: options.outputNodeId,
2091
+ numImages: options.numImages,
2092
+ bindings: options.bindings,
2093
+ workflowSha256
2094
+ }
2095
+ };
2096
+ }
2097
+ supports(generator) {
2098
+ return generator === "map";
2099
+ }
2100
+ estimate(spec) {
2101
+ return { unit: "free", amount: 0, candidates: resolvedOptions(spec).numImages };
2102
+ }
2103
+ validate(spec, styleImages) {
2104
+ const options = resolvedOptions(spec);
2105
+ if (spec.width < 16 || spec.height < 16 || spec.width > 4096 || spec.height > 4096) {
2106
+ throw new Error("ComfyUI output dimensions must be between 16 and 4096 pixels");
2107
+ }
2108
+ if (styleImages.length) {
2109
+ throw new Error("ComfyUI styleImages are not supported yet; keep references inside the workflow");
2110
+ }
2111
+ if (spec.palette.length) {
2112
+ throw new Error("ComfyUI does not map PixelKiln palette values yet; encode palette control in the workflow");
2113
+ }
2114
+ if (spec.seed != null && !options.bindings.seed) {
2115
+ throw new Error("ComfyUI requires bindings.seed when the style declares a seed");
2116
+ }
2117
+ validateWorkflowBindings(options.workflow, options);
2118
+ }
2119
+ rateLimit() {
2120
+ return { spacingMs: 0, maxInFlight: 1 };
2121
+ }
2122
+ async submit(spec, styleImages) {
2123
+ this.validate(spec, styleImages);
2124
+ const options = resolvedOptions(spec);
2125
+ const workflow = structuredClone(options.workflow);
2126
+ setBinding(workflow, options.bindings.prompt, spec.prompt);
2127
+ setBinding(workflow, options.bindings.width, spec.width);
2128
+ setBinding(workflow, options.bindings.height, spec.height);
2129
+ setBinding(workflow, options.bindings.batchSize, options.numImages);
2130
+ if (spec.seed != null && options.bindings.seed) {
2131
+ setBinding(workflow, options.bindings.seed, spec.seed);
2132
+ }
2133
+ const promptId = await this.client.submit(workflow);
2134
+ return { jobId: encodeJob(promptId, options.outputNodeId) };
2135
+ }
2136
+ async poll(jobId, _generator, context) {
2137
+ const { promptId, outputNodeId } = decodeJob(jobId);
2138
+ const history = await this.client.history(promptId);
2139
+ const entry = historyEntry(history, promptId);
2140
+ if (!entry) return { status: "processing" };
2141
+ const status = isObject(entry.status) ? entry.status : {};
2142
+ const statusText = typeof status.status_str === "string" ? status.status_str : "";
2143
+ if (statusText === "error") {
2144
+ return { status: "failed", error: historyError(status) };
2145
+ }
2146
+ if (status.completed === false) return { status: "processing" };
2147
+ const images = outputImages(entry, outputNodeId);
2148
+ if (images instanceof Error) return { status: "failed", error: images.message };
2149
+ const expected = context?.spec ? resolvedOptions(context.spec).numImages : null;
2150
+ if (expected != null && images.length !== expected) {
2151
+ return {
2152
+ status: "failed",
2153
+ error: `ComfyUI output node "${outputNodeId}" returned ${images.length} image(s), expected ${expected}`
2154
+ };
2155
+ }
2156
+ const metadata = context?.spec ? comfyMetadata(context.spec, promptId, outputNodeId, images) : {
2157
+ promptId,
2158
+ outputNodeId,
2159
+ outputCount: images.length
2160
+ };
2161
+ if (images.length > 1) {
2162
+ return {
2163
+ status: "review",
2164
+ candidateUrls: images.map((image) => this.client.viewUrl(image)),
2165
+ metadata
2166
+ };
2167
+ }
2168
+ const sourceUrl = sourceRef(images[0]);
2169
+ return {
2170
+ status: "ready",
2171
+ objectId: `${promptId}#${outputNodeId}#0`,
2172
+ sourceUrl,
2173
+ sources: [{ url: sourceUrl, mediaType: MediaType.PNG }],
2174
+ metadata
2175
+ };
2176
+ }
2177
+ async selectCandidate(jobId, index) {
2178
+ const { promptId, outputNodeId } = decodeJob(jobId);
2179
+ const history = await this.client.history(promptId);
2180
+ const entry = historyEntry(history, promptId);
2181
+ if (!entry) throw new Error(`ComfyUI prompt ${promptId} is not complete`);
2182
+ const images = outputImages(entry, outputNodeId);
2183
+ if (images instanceof Error) throw images;
2184
+ const image = images[index];
2185
+ if (!image) throw new Error(`ComfyUI prompt ${promptId} has no candidate at index ${index}`);
2186
+ return {
2187
+ objectId: `${promptId}#${outputNodeId}#${index}`,
2188
+ sourceUrl: sourceRef(image)
2189
+ };
2190
+ }
2191
+ async download(url) {
2192
+ return this.client.download(url);
2193
+ }
2194
+ async checkConnection() {
2195
+ await this.client.checkConnection();
2196
+ }
2197
+ };
2198
+ function parseOptions(value) {
2199
+ const allowed = /* @__PURE__ */ new Set(["workflowFile", "outputNodeId", "numImages", "bindings", "workflow", "workflowSha256"]);
2200
+ const extra = Object.keys(value).filter((key) => !allowed.has(key));
2201
+ if (extra.length) throw new Error(`Unknown ComfyUI option(s): ${extra.join(", ")}`);
2202
+ const workflowFile = requiredString(value.workflowFile, "workflowFile");
2203
+ const outputNodeId = requiredString(value.outputNodeId, "outputNodeId");
2204
+ const numImages = value.numImages ?? 1;
2205
+ if (!Number.isInteger(numImages) || Number(numImages) < 1 || Number(numImages) > 16) {
2206
+ throw new Error("ComfyUI numImages must be a whole number from 1 to 16");
2207
+ }
2208
+ if (!isObject(value.bindings)) throw new Error("ComfyUI bindings must be an object");
2209
+ const bindingKeys = /* @__PURE__ */ new Set(["prompt", "width", "height", "batchSize", "seed"]);
2210
+ const extraBindings = Object.keys(value.bindings).filter((key) => !bindingKeys.has(key));
2211
+ if (extraBindings.length) throw new Error(`Unknown ComfyUI binding(s): ${extraBindings.join(", ")}`);
2212
+ const bindings = {
2213
+ prompt: parseBinding(value.bindings.prompt, "prompt"),
2214
+ width: parseBinding(value.bindings.width, "width"),
2215
+ height: parseBinding(value.bindings.height, "height"),
2216
+ batchSize: parseBinding(value.bindings.batchSize, "batchSize"),
2217
+ ...value.bindings.seed == null ? {} : { seed: parseBinding(value.bindings.seed, "seed") }
2218
+ };
2219
+ const workflow = value.workflow == null ? void 0 : parseWorkflow(value.workflow, workflowFile);
2220
+ const workflowSha256 = value.workflowSha256 == null ? void 0 : requiredString(value.workflowSha256, "workflowSha256");
2221
+ return {
2222
+ workflowFile,
2223
+ outputNodeId,
2224
+ numImages: Number(numImages),
2225
+ bindings,
2226
+ ...workflow ? { workflow } : {},
2227
+ ...workflowSha256 ? { workflowSha256 } : {}
2228
+ };
2229
+ }
2230
+ function resolvedOptions(spec) {
2231
+ const options = parseOptions(spec.providerOptions);
2232
+ if (!options.workflow || !options.workflowSha256) {
2233
+ throw new Error("ComfyUI workflow options were not resolved from workflowFile");
2234
+ }
2235
+ return options;
2236
+ }
2237
+ function parseBinding(value, name) {
2238
+ if (!isObject(value)) throw new Error(`ComfyUI bindings.${name} must be an object`);
2239
+ const extra = Object.keys(value).filter((key) => key !== "nodeId" && key !== "input");
2240
+ if (extra.length) throw new Error(`Unknown ComfyUI bindings.${name} field(s): ${extra.join(", ")}`);
2241
+ return {
2242
+ nodeId: requiredString(value.nodeId, `bindings.${name}.nodeId`),
2243
+ input: requiredString(value.input, `bindings.${name}.input`)
2244
+ };
2245
+ }
2246
+ function parseWorkflow(value, label) {
2247
+ if (!isObject(value) || !Object.keys(value).length) {
2248
+ throw new Error(`ComfyUI workflow is not a non-empty API-format object: ${label}`);
2249
+ }
2250
+ for (const [nodeId, node] of Object.entries(value)) {
2251
+ if (!isObject(node) || typeof node.class_type !== "string" || !isObject(node.inputs)) {
2252
+ throw new Error(`ComfyUI workflow node "${nodeId}" must contain class_type and inputs`);
2253
+ }
2254
+ }
2255
+ return value;
2256
+ }
2257
+ function validateWorkflowBindings(workflow, options) {
2258
+ for (const [name, binding] of Object.entries(options.bindings)) {
2259
+ if (!binding) continue;
2260
+ const node = workflow[binding.nodeId];
2261
+ if (!node) throw new Error(`ComfyUI ${name} binding refers to missing node "${binding.nodeId}"`);
2262
+ if (!Object.hasOwn(node.inputs, binding.input)) {
2263
+ throw new Error(
2264
+ `ComfyUI ${name} binding refers to missing input "${binding.input}" on node "${binding.nodeId}"`
2265
+ );
2266
+ }
2267
+ }
2268
+ if (!workflow[options.outputNodeId]) {
2269
+ throw new Error(`ComfyUI outputNodeId refers to missing node "${options.outputNodeId}"`);
2270
+ }
2271
+ }
2272
+ function setBinding(workflow, binding, value) {
2273
+ const node = workflow[binding.nodeId];
2274
+ if (!node || !Object.hasOwn(node.inputs, binding.input)) {
2275
+ throw new Error(`ComfyUI binding ${binding.nodeId}.${binding.input} is unavailable`);
2276
+ }
2277
+ node.inputs[binding.input] = value;
2278
+ }
2279
+ function historyEntry(history, promptId) {
2280
+ if (!isObject(history)) throw new Error("ComfyUI returned invalid history JSON");
2281
+ const entry = history[promptId];
2282
+ if (entry == null) return null;
2283
+ if (!isObject(entry)) throw new Error(`ComfyUI returned invalid history for prompt ${promptId}`);
2284
+ return entry;
2285
+ }
2286
+ function outputImages(entry, outputNodeId) {
2287
+ if (!isObject(entry.outputs)) return new Error("ComfyUI completed without workflow outputs");
2288
+ const output = entry.outputs[outputNodeId];
2289
+ if (!isObject(output) || !Array.isArray(output.images)) {
2290
+ return new Error(`ComfyUI output node "${outputNodeId}" returned no images`);
2291
+ }
2292
+ const images = [];
2293
+ for (const value of output.images) {
2294
+ if (!isObject(value) || typeof value.filename !== "string" || typeof value.subfolder !== "string" || value.type !== "output") {
2295
+ return new Error(`ComfyUI output node "${outputNodeId}" returned an invalid image record`);
2296
+ }
2297
+ images.push({ filename: value.filename, subfolder: value.subfolder, type: "output" });
2298
+ }
2299
+ if (!images.length) return new Error(`ComfyUI output node "${outputNodeId}" returned no images`);
2300
+ return images;
2301
+ }
2302
+ function comfyMetadata(spec, promptId, outputNodeId, images) {
2303
+ const options = resolvedOptions(spec);
2304
+ return {
2305
+ promptId,
2306
+ outputNodeId,
2307
+ outputCount: images.length,
2308
+ workflowFile: options.workflowFile,
2309
+ workflowSha256: options.workflowSha256,
2310
+ files: images.map((image) => ({ ...image }))
2311
+ };
2312
+ }
2313
+ function encodeJob(promptId, outputNodeId) {
2314
+ return `${promptId}#${encodeURIComponent(outputNodeId)}`;
2315
+ }
2316
+ function decodeJob(jobId) {
2317
+ const split = jobId.lastIndexOf("#");
2318
+ if (split < 1 || split === jobId.length - 1) throw new Error(`Invalid ComfyUI job id "${jobId}"`);
2319
+ return {
2320
+ promptId: jobId.slice(0, split),
2321
+ outputNodeId: decodeURIComponent(jobId.slice(split + 1))
2322
+ };
2323
+ }
2324
+ function sourceRef(image) {
2325
+ const url = new URL("comfyui://output");
2326
+ url.searchParams.set("filename", image.filename);
2327
+ url.searchParams.set("subfolder", image.subfolder);
2328
+ url.searchParams.set("type", image.type);
2329
+ return url.toString();
2330
+ }
2331
+ function parseSource(source) {
2332
+ let url;
2333
+ try {
2334
+ url = new URL(source);
2335
+ } catch {
2336
+ throw new Error("Invalid ComfyUI output reference");
2337
+ }
2338
+ const filename = url.searchParams.get("filename");
2339
+ const subfolder = url.searchParams.get("subfolder");
2340
+ const type = url.searchParams.get("type");
2341
+ if (url.protocol !== "comfyui:" || url.hostname !== "output" || !filename || subfolder == null || type !== "output") {
2342
+ throw new Error("Invalid ComfyUI output reference");
2343
+ }
2344
+ return { filename, subfolder, type };
2345
+ }
2346
+ function normalizeBaseUrl(value) {
2347
+ let url;
2348
+ try {
2349
+ url = new URL(value);
2350
+ } catch {
2351
+ throw new Error("COMFYUI_BASE_URL must be an absolute HTTP(S) URL");
2352
+ }
2353
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
2354
+ throw new Error("COMFYUI_BASE_URL must use HTTP or HTTPS");
2355
+ }
2356
+ url.hash = "";
2357
+ url.search = "";
2358
+ return url.toString().replace(/\/$/, "");
2359
+ }
2360
+ function historyError(status) {
2361
+ const messages = Array.isArray(status.messages) ? status.messages : [];
2362
+ for (const message2 of messages) {
2363
+ if (!Array.isArray(message2) || message2[0] !== "execution_error" || !isObject(message2[1])) continue;
2364
+ const detail = message2[1].exception_message;
2365
+ if (typeof detail === "string" && detail.trim()) {
2366
+ return `ComfyUI execution failed: ${detail.trim().slice(0, 300)}`;
2367
+ }
2368
+ }
2369
+ return "ComfyUI execution failed";
2370
+ }
2371
+ function apiError(value) {
2372
+ if (!isObject(value)) return null;
2373
+ 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;
2374
+ const nodes = isObject(value.node_errors) ? Object.keys(value.node_errors) : [];
2375
+ if (nodes.length) {
2376
+ return `${raw ? `${raw}; ` : ""}invalid workflow node(s): ${nodes.slice(0, 8).join(", ")}`;
2377
+ }
2378
+ return raw?.trim().slice(0, 300) || null;
2379
+ }
2380
+ function requiredString(value, name) {
2381
+ if (typeof value !== "string" || !value.trim()) throw new Error(`ComfyUI ${name} must be a non-empty string`);
2382
+ return value.trim();
2383
+ }
2384
+ function isObject(value) {
2385
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
2386
+ }
2387
+ function canonical(value) {
2388
+ if (Array.isArray(value)) return value.map(canonical);
2389
+ if (!isObject(value)) return value;
2390
+ return Object.fromEntries(
2391
+ Object.keys(value).sort().map((key) => [key, canonical(value[key])])
2392
+ );
2393
+ }
2394
+
2395
+ // src/providers/fake.ts
2396
+ var import_node_crypto4 = require("crypto");
1928
2397
  var FAKE_PNG = Buffer.from(
1929
2398
  "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
1930
2399
  "base64"
@@ -2008,7 +2477,7 @@ var FakeProvider = class {
2008
2477
  }
2009
2478
  async download(url) {
2010
2479
  if (!url.startsWith("fake://")) throw new Error(`fake provider cannot download ${url}`);
2011
- const salt = (0, import_node_crypto2.createHash)("sha256").update(url).digest();
2480
+ const salt = (0, import_node_crypto4.createHash)("sha256").update(url).digest();
2012
2481
  return Buffer.concat([FAKE_PNG, salt.subarray(0, 4)]);
2013
2482
  }
2014
2483
  async balance() {
@@ -2107,56 +2576,23 @@ registerProvider({
2107
2576
  return RetroDiffusionProvider.forOffline();
2108
2577
  }
2109
2578
  });
2579
+ registerProvider({
2580
+ id: "comfyui",
2581
+ create(mode2) {
2582
+ if (mode2 === "online") return ComfyUIProvider.fromEnv();
2583
+ if (mode2 === "downloads") return ComfyUIProvider.forDownloads();
2584
+ return ComfyUIProvider.forOffline();
2585
+ }
2586
+ });
2110
2587
 
2111
2588
  // src/manifest.ts
2112
- var import_promises2 = require("fs/promises");
2589
+ var import_promises3 = require("fs/promises");
2113
2590
  var import_node_fs2 = require("fs");
2114
- var import_node_path2 = __toESM(require("path"), 1);
2115
-
2116
- // src/hash.ts
2117
- var import_node_crypto3 = require("crypto");
2118
- var import_promises = require("fs/promises");
2119
- function sha256(data) {
2120
- return (0, import_node_crypto3.createHash)("sha256").update(data).digest("hex");
2121
- }
2122
- async function sha256File(path17) {
2123
- return sha256(await (0, import_promises.readFile)(path17));
2124
- }
2125
- function specHash(spec, styleImageHashes) {
2126
- return sha256(
2127
- JSON.stringify({
2128
- // Preserve every existing PixelLab hash while making a provider switch
2129
- // invalidate the spec. Older manifests implicitly mean pixellab.
2130
- provider: spec.provider === "pixellab" ? void 0 : spec.provider,
2131
- providerOptions: Object.keys(spec.providerOptions).length > 0 ? spec.providerOptions : void 0,
2132
- generator: spec.generator,
2133
- prompt: spec.prompt,
2134
- width: spec.width,
2135
- height: spec.height,
2136
- view: spec.view,
2137
- outline: spec.outline ?? null,
2138
- shading: spec.shading ?? null,
2139
- detail: spec.detail ?? null,
2140
- seed: spec.seed ?? null,
2141
- palette: spec.palette,
2142
- // `noBackground` only reaches the wire for pixflux; the tile fields are
2143
- // undefined for every other generator. `tileSize` is intentionally
2144
- // absent — width/height are derived from it, so it is already covered.
2145
- noBackground: spec.generator === "pixflux" || spec.provider !== "pixellab" ? spec.noBackground : void 0,
2146
- tileType: spec.tileType,
2147
- tileView: spec.tileView,
2148
- tileFeature: spec.tileFeature,
2149
- outlineMode: spec.outlineMode,
2150
- styleImages: styleImageHashes
2151
- })
2152
- );
2153
- }
2154
-
2155
- // src/manifest.ts
2591
+ var import_node_path3 = __toESM(require("path"), 1);
2156
2592
  async function loadManifest(manifestPath) {
2157
- const abs = import_node_path2.default.resolve(manifestPath);
2593
+ const abs = import_node_path3.default.resolve(manifestPath);
2158
2594
  if (!(0, import_node_fs2.existsSync)(abs)) throw new Error(`No manifest at ${abs}`);
2159
- const parsed = ManifestSchema.safeParse(JSON.parse(await (0, import_promises2.readFile)(abs, "utf8")));
2595
+ const parsed = ManifestSchema.safeParse(JSON.parse(await (0, import_promises3.readFile)(abs, "utf8")));
2160
2596
  if (!parsed.success) {
2161
2597
  const issues = parsed.error.issues.map((i) => ` ${i.path.join(".")}: ${i.message}`).join("\n");
2162
2598
  throw new Error(`Manifest at ${abs} is invalid:
@@ -2178,7 +2614,7 @@ ${issues}`);
2178
2614
  throw new Error(`Manifest at ${abs} is invalid:
2179
2615
  ${unknownReferences.map((i) => ` ${i}`).join("\n")}`);
2180
2616
  }
2181
- return { manifest: parsed.data, root: import_node_path2.default.dirname(abs), path: abs };
2617
+ return { manifest: parsed.data, root: import_node_path3.default.dirname(abs), path: abs };
2182
2618
  }
2183
2619
  async function resolveSpecs(loaded, filter) {
2184
2620
  const { manifest, root } = loaded;
@@ -2201,11 +2637,11 @@ async function resolveSpecs(loaded, filter) {
2201
2637
  }
2202
2638
  const styleImageCache = /* @__PURE__ */ new Map();
2203
2639
  async function loadStyleImage(rel) {
2204
- const abs = import_node_path2.default.resolve(root, rel);
2640
+ const abs = import_node_path3.default.resolve(root, rel);
2205
2641
  let hit = styleImageCache.get(abs);
2206
2642
  if (!hit) {
2207
2643
  if (!(0, import_node_fs2.existsSync)(abs)) throw new Error(`Style image not found: ${abs}`);
2208
- const buf = await (0, import_promises2.readFile)(abs);
2644
+ const buf = await (0, import_promises3.readFile)(abs);
2209
2645
  const metadata = imageMetadata(buf);
2210
2646
  if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${abs}`);
2211
2647
  if (metadata.width < 1 || metadata.height < 1) {
@@ -2218,6 +2654,10 @@ async function resolveSpecs(loaded, filter) {
2218
2654
  }
2219
2655
  for (const styleId of styleIds) {
2220
2656
  const style = manifest.styles[styleId];
2657
+ const rawProviderOptions = style.providerOptions[activeProvider.id] ?? {};
2658
+ const optionResolution = activeProvider.resolveOptions ? await activeProvider.resolveOptions(rawProviderOptions, { root, styleId }) : { options: rawProviderOptions };
2659
+ const providerOptions = optionResolution.options;
2660
+ const providerOptionIdentity = optionResolution.identity ?? providerOptions;
2221
2661
  const styleImageHashes = [];
2222
2662
  const styleImageDimensions = [];
2223
2663
  for (const img of style.styleImages) {
@@ -2250,15 +2690,15 @@ async function resolveSpecs(loaded, filter) {
2250
2690
  }
2251
2691
  const subject = asset.promptByStyle[styleId] ?? asset.prompt;
2252
2692
  const prompt = [style.promptPrefix, subject, style.promptSuffix].map((p) => p.trim()).filter(Boolean).join(", ");
2253
- const relFile = asset.file ?? import_node_path2.default.join(asset.category ?? "", `${assetId}.png`);
2254
- const outFile = import_node_path2.default.resolve(root, style.outDir, relFile);
2693
+ const relFile = asset.file ?? import_node_path3.default.join(asset.category ?? "", `${assetId}.png`);
2694
+ const outFile = import_node_path3.default.resolve(root, style.outDir, relFile);
2255
2695
  const tileSize = generator === "tiles" ? size : style.tileSize ?? 32;
2256
2696
  const tileVariations = tileFeatureOutputCount(generator === "tiles" ? style.tileFeature : void 0) ?? tileVariationCount(countNumberedDescriptions(prompt));
2257
2697
  const base = {
2258
2698
  styleId,
2259
2699
  assetId,
2260
2700
  provider: activeProvider.id,
2261
- providerOptions: style.providerOptions[activeProvider.id] ?? {},
2701
+ providerOptions,
2262
2702
  generator,
2263
2703
  prompt,
2264
2704
  width,
@@ -2296,10 +2736,10 @@ async function resolveSpecs(loaded, filter) {
2296
2736
  outFile,
2297
2737
  tags,
2298
2738
  source: asset.source,
2299
- specHash: specHash(base, styleImageHashes)
2739
+ specHash: specHash(base, styleImageHashes, providerOptionIdentity)
2300
2740
  };
2301
2741
  const resolvedImages = style.styleImages.map((image) => {
2302
- const hit = styleImageCache.get(import_node_path2.default.resolve(root, image.path));
2742
+ const hit = styleImageCache.get(import_node_path3.default.resolve(root, image.path));
2303
2743
  return { base64: hit.base64, width: hit.width, height: hit.height, format: hit.format };
2304
2744
  });
2305
2745
  activeProvider.validate?.(resolved, resolvedImages);
@@ -2326,8 +2766,8 @@ async function resolveStyleImages(loaded, styleId) {
2326
2766
  if (!style) throw new Error(`Unknown style "${styleId}"`);
2327
2767
  const out = [];
2328
2768
  for (const img of style.styleImages) {
2329
- const file = import_node_path2.default.resolve(loaded.root, img.path);
2330
- const buf = await (0, import_promises2.readFile)(file);
2769
+ const file = import_node_path3.default.resolve(loaded.root, img.path);
2770
+ const buf = await (0, import_promises3.readFile)(file);
2331
2771
  const metadata = imageMetadata(buf);
2332
2772
  if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${file}`);
2333
2773
  if (metadata.width < 1 || metadata.height < 1) {
@@ -2370,13 +2810,13 @@ function imageMetadata(buf) {
2370
2810
  }
2371
2811
 
2372
2812
  // src/lock.ts
2373
- var import_promises3 = require("fs/promises");
2813
+ var import_promises4 = require("fs/promises");
2374
2814
  var import_node_fs3 = require("fs");
2375
- var import_node_path3 = __toESM(require("path"), 1);
2815
+ var import_node_path4 = __toESM(require("path"), 1);
2376
2816
  async function loadLock(lockPath) {
2377
2817
  if (!(0, import_node_fs3.existsSync)(lockPath)) return { version: 2, entries: {} };
2378
2818
  try {
2379
- return parseLock(JSON.parse(await (0, import_promises3.readFile)(lockPath, "utf8")));
2819
+ return parseLock(JSON.parse(await (0, import_promises4.readFile)(lockPath, "utf8")));
2380
2820
  } catch (err) {
2381
2821
  throw new Error(
2382
2822
  `Lockfile at ${lockPath} is malformed:
@@ -2388,7 +2828,7 @@ var saveQueues = /* @__PURE__ */ new Map();
2388
2828
  var dirtyPatches = /* @__PURE__ */ new WeakMap();
2389
2829
  var dirtyDeletes = /* @__PURE__ */ new WeakMap();
2390
2830
  function saveLock(lockPath, lock) {
2391
- const queueKey = import_node_path3.default.resolve(lockPath);
2831
+ const queueKey = import_node_path4.default.resolve(lockPath);
2392
2832
  const previous = saveQueues.get(queueKey) ?? Promise.resolve();
2393
2833
  const next = previous.catch(() => {
2394
2834
  }).then(() => writeLockNow(lockPath, lock));
@@ -2400,7 +2840,7 @@ function saveLock(lockPath, lock) {
2400
2840
  return next;
2401
2841
  }
2402
2842
  async function writeLockNow(lockPath, lock) {
2403
- await (0, import_promises3.mkdir)(import_node_path3.default.dirname(import_node_path3.default.resolve(lockPath)), { recursive: true });
2843
+ await (0, import_promises4.mkdir)(import_node_path4.default.dirname(import_node_path4.default.resolve(lockPath)), { recursive: true });
2404
2844
  const release = await acquireFileLock(lockPath);
2405
2845
  try {
2406
2846
  await writeLockWhileHeld(lockPath, lock);
@@ -2412,7 +2852,7 @@ async function writeLockWhileHeld(file, lock) {
2412
2852
  let disk = { version: 2, entries: {} };
2413
2853
  if ((0, import_node_fs3.existsSync)(file)) {
2414
2854
  try {
2415
- disk = parseLock(JSON.parse(await (0, import_promises3.readFile)(file, "utf8")));
2855
+ disk = parseLock(JSON.parse(await (0, import_promises4.readFile)(file, "utf8")));
2416
2856
  } catch (err) {
2417
2857
  throw new Error(
2418
2858
  `Refusing to overwrite malformed lockfile at ${file}: ${err instanceof Error ? err.message : String(err)}`
@@ -2440,10 +2880,10 @@ async function writeLockWhileHeld(file, lock) {
2440
2880
  }
2441
2881
  const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
2442
2882
  try {
2443
- await (0, import_promises3.writeFile)(tmp, JSON.stringify({ version: 2, entries: sorted }, null, 2) + "\n");
2444
- await (0, import_promises3.rename)(tmp, file);
2883
+ await (0, import_promises4.writeFile)(tmp, JSON.stringify({ version: 2, entries: sorted }, null, 2) + "\n");
2884
+ await (0, import_promises4.rename)(tmp, file);
2445
2885
  } finally {
2446
- await (0, import_promises3.rm)(tmp, { force: true });
2886
+ await (0, import_promises4.rm)(tmp, { force: true });
2447
2887
  }
2448
2888
  const currentDirty = dirtyPatches.get(lock);
2449
2889
  if (currentDirty) {
@@ -2506,18 +2946,18 @@ async function acquireFileLock(file) {
2506
2946
  const deadline = Date.now() + FILE_LOCK_TIMEOUT_MS;
2507
2947
  for (; ; ) {
2508
2948
  try {
2509
- await (0, import_promises3.mkdir)(lockDir);
2949
+ await (0, import_promises4.mkdir)(lockDir);
2510
2950
  return async () => {
2511
- await (0, import_promises3.rmdir)(lockDir).catch(() => {
2951
+ await (0, import_promises4.rmdir)(lockDir).catch(() => {
2512
2952
  });
2513
2953
  };
2514
2954
  } catch (err) {
2515
2955
  const code = err.code;
2516
2956
  if (code !== "EEXIST") throw err;
2517
2957
  try {
2518
- const age = Date.now() - (await (0, import_promises3.stat)(lockDir)).mtimeMs;
2958
+ const age = Date.now() - (await (0, import_promises4.stat)(lockDir)).mtimeMs;
2519
2959
  if (age > STALE_FILE_LOCK_MS) {
2520
- await (0, import_promises3.rmdir)(lockDir);
2960
+ await (0, import_promises4.rmdir)(lockDir);
2521
2961
  continue;
2522
2962
  }
2523
2963
  } catch (lockErr) {
@@ -2543,21 +2983,21 @@ function totalSpend(lock, unit = "generations") {
2543
2983
  }
2544
2984
 
2545
2985
  // src/outputs.ts
2546
- var import_node_path4 = __toESM(require("path"), 1);
2986
+ var import_node_path5 = __toESM(require("path"), 1);
2547
2987
  function portableOutputPath(file, manifestDir) {
2548
- const root = import_node_path4.default.resolve(manifestDir);
2549
- const absolute = import_node_path4.default.isAbsolute(file) ? import_node_path4.default.normalize(file) : import_node_path4.default.resolve(root, file);
2550
- const relative = import_node_path4.default.relative(root, absolute);
2551
- const value = import_node_path4.default.isAbsolute(relative) ? absolute : relative;
2552
- return value.split(import_node_path4.default.sep).join("/");
2988
+ const root = import_node_path5.default.resolve(manifestDir);
2989
+ const absolute = import_node_path5.default.isAbsolute(file) ? import_node_path5.default.normalize(file) : import_node_path5.default.resolve(root, file);
2990
+ const relative = import_node_path5.default.relative(root, absolute);
2991
+ const value = import_node_path5.default.isAbsolute(relative) ? absolute : relative;
2992
+ return value.split(import_node_path5.default.sep).join("/");
2553
2993
  }
2554
2994
  function resolveOutputPath(recordedPath, manifestDir) {
2555
- if (import_node_path4.default.isAbsolute(recordedPath)) return import_node_path4.default.normalize(recordedPath);
2556
- if (import_node_path4.default.win32.isAbsolute(recordedPath)) return recordedPath;
2557
- return import_node_path4.default.resolve(manifestDir, recordedPath.split(/[\\/]/).join(import_node_path4.default.sep));
2995
+ if (import_node_path5.default.isAbsolute(recordedPath)) return import_node_path5.default.normalize(recordedPath);
2996
+ if (import_node_path5.default.win32.isAbsolute(recordedPath)) return recordedPath;
2997
+ return import_node_path5.default.resolve(manifestDir, recordedPath.split(/[\\/]/).join(import_node_path5.default.sep));
2558
2998
  }
2559
2999
  function expectedOutputPath(spec, role, index, total, mediaType) {
2560
- const originalExt = import_node_path4.default.extname(spec.outFile);
3000
+ const originalExt = import_node_path5.default.extname(spec.outFile);
2561
3001
  const ext = mediaType ? mediaExtension(mediaType) : originalExt || ".png";
2562
3002
  const stem = originalExt ? spec.outFile.slice(0, -originalExt.length) : spec.outFile;
2563
3003
  if (total === 1) return `${stem}${ext}`;
@@ -2646,44 +3086,44 @@ function resolveSpecOutputs(spec, lock, manifestDir) {
2646
3086
  id: spec.assetId,
2647
3087
  index: 0,
2648
3088
  path: spec.outFile,
2649
- absolutePath: import_node_path4.default.resolve(spec.outFile),
3089
+ absolutePath: import_node_path5.default.resolve(spec.outFile),
2650
3090
  sha256: ""
2651
3091
  }];
2652
3092
  }
2653
3093
 
2654
3094
  // src/artifacts.ts
2655
- var import_node_path5 = __toESM(require("path"), 1);
2656
- var import_node_crypto4 = require("crypto");
2657
- var import_promises4 = require("fs/promises");
3095
+ var import_node_path6 = __toESM(require("path"), 1);
3096
+ var import_node_crypto5 = require("crypto");
3097
+ var import_promises5 = require("fs/promises");
2658
3098
  var activeTransactions = /* @__PURE__ */ new Set();
2659
3099
  function message(error) {
2660
3100
  return error instanceof Error ? error.message : String(error);
2661
3101
  }
2662
3102
  function digest(data) {
2663
- return (0, import_node_crypto4.createHash)("sha256").update(data).digest("hex");
3103
+ return (0, import_node_crypto5.createHash)("sha256").update(data).digest("hex");
2664
3104
  }
2665
3105
  function portableRelative(from, to) {
2666
- return import_node_path5.default.relative(from, import_node_path5.default.resolve(to)).split(import_node_path5.default.sep).join("/") || ".";
3106
+ return import_node_path6.default.relative(from, import_node_path6.default.resolve(to)).split(import_node_path6.default.sep).join("/") || ".";
2667
3107
  }
2668
- function canonical(value) {
3108
+ function canonical2(value) {
2669
3109
  if (value === null || typeof value === "string" || typeof value === "boolean") return value;
2670
3110
  if (typeof value === "number") {
2671
3111
  if (!Number.isFinite(value)) throw new Error("Artifact provenance numbers must be finite.");
2672
3112
  return value;
2673
3113
  }
2674
- if (Array.isArray(value)) return value.map(canonical);
3114
+ if (Array.isArray(value)) return value.map(canonical2);
2675
3115
  if (typeof value === "object") {
2676
3116
  const sorted = {};
2677
3117
  for (const key of Object.keys(value).sort()) {
2678
3118
  const item = value[key];
2679
- if (item !== void 0) sorted[key] = canonical(item);
3119
+ if (item !== void 0) sorted[key] = canonical2(item);
2680
3120
  }
2681
3121
  return sorted;
2682
3122
  }
2683
3123
  throw new Error(`Artifact provenance cannot contain ${typeof value} values.`);
2684
3124
  }
2685
3125
  function fingerprint(provenance) {
2686
- return digest(JSON.stringify(canonical({
3126
+ return digest(JSON.stringify(canonical2({
2687
3127
  kind: provenance.kind,
2688
3128
  sources: provenance.sources,
2689
3129
  options: provenance.options,
@@ -2714,7 +3154,7 @@ function parseArtifactManifest(absolute, data) {
2714
3154
  }
2715
3155
  async function readOptional(file) {
2716
3156
  try {
2717
- return await (0, import_promises4.readFile)(file);
3157
+ return await (0, import_promises5.readFile)(file);
2718
3158
  } catch (error) {
2719
3159
  if (isCode(error, "ENOENT")) return null;
2720
3160
  throw error;
@@ -2733,8 +3173,8 @@ function processIsAlive(pid) {
2733
3173
  }
2734
3174
  }
2735
3175
  function validTemporaryPath(candidate, destination, type) {
2736
- return import_node_path5.default.dirname(candidate) === import_node_path5.default.dirname(destination) && import_node_path5.default.basename(candidate).startsWith(
2737
- `.${import_node_path5.default.basename(destination)}.pixelkiln-${type}-`
3176
+ return import_node_path6.default.dirname(candidate) === import_node_path6.default.dirname(destination) && import_node_path6.default.basename(candidate).startsWith(
3177
+ `.${import_node_path6.default.basename(destination)}.pixelkiln-${type}-`
2738
3178
  );
2739
3179
  }
2740
3180
  function parseTransaction(journal, data) {
@@ -2750,14 +3190,14 @@ function parseTransaction(journal, data) {
2750
3190
  return raw;
2751
3191
  }
2752
3192
  async function removeTransactionFiles(journal) {
2753
- await (0, import_promises4.rm)(journal, { force: true });
2754
- await (0, import_promises4.rm)(transactionMarker(journal), { force: true });
3193
+ await (0, import_promises5.rm)(journal, { force: true });
3194
+ await (0, import_promises5.rm)(transactionMarker(journal), { force: true });
2755
3195
  }
2756
3196
  async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
2757
- const journal = import_node_path5.default.resolve(recoveryFile);
3197
+ const journal = import_node_path6.default.resolve(recoveryFile);
2758
3198
  const bytes = await readOptional(journal);
2759
3199
  if (!bytes) {
2760
- await (0, import_promises4.rm)(transactionMarker(journal), { force: true });
3200
+ await (0, import_promises5.rm)(transactionMarker(journal), { force: true });
2761
3201
  return;
2762
3202
  }
2763
3203
  if (activeTransactions.has(journal)) {
@@ -2770,7 +3210,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
2770
3210
  );
2771
3211
  }
2772
3212
  for (const entry of transaction.entries) {
2773
- if (typeof entry.destination !== "string" || typeof entry.stage !== "string" || typeof entry.sha256 !== "string" || entry.backup !== void 0 && typeof entry.backup !== "string" || !allowedDestinations.has(import_node_path5.default.resolve(entry.destination)) || !validTemporaryPath(entry.stage, entry.destination, "stage") || entry.backup !== void 0 && !validTemporaryPath(entry.backup, entry.destination, "backup")) {
3213
+ if (typeof entry.destination !== "string" || typeof entry.stage !== "string" || typeof entry.sha256 !== "string" || entry.backup !== void 0 && typeof entry.backup !== "string" || !allowedDestinations.has(import_node_path6.default.resolve(entry.destination)) || !validTemporaryPath(entry.stage, entry.destination, "stage") || entry.backup !== void 0 && !validTemporaryPath(entry.backup, entry.destination, "backup")) {
2774
3214
  throw new Error(
2775
3215
  `Refusing unsafe artifact recovery from ${journal}; its destinations or temporary paths do not match the current bundle.`
2776
3216
  );
@@ -2796,7 +3236,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
2796
3236
  }
2797
3237
  await remove2(entry.destination, errors);
2798
3238
  try {
2799
- await (0, import_promises4.rename)(entry.backup, entry.destination);
3239
+ await (0, import_promises5.rename)(entry.backup, entry.destination);
2800
3240
  } catch (error) {
2801
3241
  errors.push(
2802
3242
  `could not restore ${entry.destination}; previous file remains at ${entry.backup}: ${message(error)}`
@@ -2816,7 +3256,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
2816
3256
  await removeTransactionFiles(journal);
2817
3257
  }
2818
3258
  function createArtifactBundleManifest(manifestPath, outputs, provenance) {
2819
- const root = import_node_path5.default.dirname(import_node_path5.default.resolve(manifestPath));
3259
+ const root = import_node_path6.default.dirname(import_node_path6.default.resolve(manifestPath));
2820
3260
  const sources = provenance.sources.map((source) => ({
2821
3261
  ...source,
2822
3262
  path: portableRelative(root, source.path)
@@ -2824,7 +3264,7 @@ function createArtifactBundleManifest(manifestPath, outputs, provenance) {
2824
3264
  const normalized = {
2825
3265
  kind: provenance.kind,
2826
3266
  sources,
2827
- options: canonical(provenance.options)
3267
+ options: canonical2(provenance.options)
2828
3268
  };
2829
3269
  const manifestOutputs = outputs.map((output) => ({
2830
3270
  path: portableRelative(root, output.path),
@@ -2846,11 +3286,11 @@ function withArtifactManifest(manifestPath, outputs, provenance) {
2846
3286
  ];
2847
3287
  }
2848
3288
  async function writeManagedArtifactBundle(manifestPath, outputs, provenance, options = {}) {
2849
- const absoluteManifest = import_node_path5.default.resolve(manifestPath);
3289
+ const absoluteManifest = import_node_path6.default.resolve(manifestPath);
2850
3290
  const recoveryFile = `${absoluteManifest}.transaction`;
2851
3291
  const allowedDestinations = /* @__PURE__ */ new Set([
2852
3292
  absoluteManifest,
2853
- ...outputs.map((output) => import_node_path5.default.resolve(output.path))
3293
+ ...outputs.map((output) => import_node_path6.default.resolve(output.path))
2854
3294
  ]);
2855
3295
  await recoverArtifactTransaction(recoveryFile, allowedDestinations);
2856
3296
  const existingManifestBytes = await readOptional(absoluteManifest);
@@ -2868,13 +3308,13 @@ async function writeManagedArtifactBundle(manifestPath, outputs, provenance, opt
2868
3308
  }
2869
3309
  }
2870
3310
  if (!options.force) {
2871
- const root = import_node_path5.default.dirname(absoluteManifest);
3311
+ const root = import_node_path6.default.dirname(absoluteManifest);
2872
3312
  const recorded = new Map(
2873
- previous?.outputs.map((output) => [import_node_path5.default.resolve(root, output.path), output.sha256]) ?? []
3313
+ previous?.outputs.map((output) => [import_node_path6.default.resolve(root, output.path), output.sha256]) ?? []
2874
3314
  );
2875
3315
  const conflicts = [];
2876
3316
  for (const output of outputs) {
2877
- const destination = import_node_path5.default.resolve(output.path);
3317
+ const destination = import_node_path6.default.resolve(output.path);
2878
3318
  const current = await readOptional(destination);
2879
3319
  if (!current || digest(current) === digest(output.data)) continue;
2880
3320
  const expected = recorded.get(destination);
@@ -2895,15 +3335,15 @@ Pass { force: true } (CLI: --force) to take ownership and replace the complete b
2895
3335
  );
2896
3336
  }
2897
3337
  async function verifyArtifactBundle(manifestPath) {
2898
- const absolute = import_node_path5.default.resolve(manifestPath);
2899
- const raw = parseArtifactManifest(absolute, await (0, import_promises4.readFile)(absolute, "utf8"));
2900
- const root = import_node_path5.default.dirname(absolute);
3338
+ const absolute = import_node_path6.default.resolve(manifestPath);
3339
+ const raw = parseArtifactManifest(absolute, await (0, import_promises5.readFile)(absolute, "utf8"));
3340
+ const root = import_node_path6.default.dirname(absolute);
2901
3341
  const changedSources = [];
2902
3342
  const changedOutputs = [];
2903
3343
  for (const source of raw.sources) {
2904
3344
  let actual = null;
2905
3345
  try {
2906
- actual = digest(await (0, import_promises4.readFile)(import_node_path5.default.resolve(root, source.path)));
3346
+ actual = digest(await (0, import_promises5.readFile)(import_node_path6.default.resolve(root, source.path)));
2907
3347
  } catch (error) {
2908
3348
  if (!isCode(error, "ENOENT")) throw error;
2909
3349
  }
@@ -2911,7 +3351,7 @@ async function verifyArtifactBundle(manifestPath) {
2911
3351
  }
2912
3352
  for (const output of raw.outputs) {
2913
3353
  try {
2914
- if (digest(await (0, import_promises4.readFile)(import_node_path5.default.resolve(root, output.path))) !== output.sha256) {
3354
+ if (digest(await (0, import_promises5.readFile)(import_node_path6.default.resolve(root, output.path))) !== output.sha256) {
2915
3355
  changedOutputs.push(output.path);
2916
3356
  }
2917
3357
  } catch (error) {
@@ -2937,7 +3377,7 @@ function isCode(error, code) {
2937
3377
  }
2938
3378
  async function remove2(file, errors) {
2939
3379
  try {
2940
- await (0, import_promises4.rm)(file, { force: true });
3380
+ await (0, import_promises5.rm)(file, { force: true });
2941
3381
  } catch (error) {
2942
3382
  errors?.push(`could not remove ${file}: ${message(error)}`);
2943
3383
  }
@@ -2950,7 +3390,7 @@ async function rollback(prepared) {
2950
3390
  for (const artifact of [...prepared].reverse()) {
2951
3391
  if (!artifact.backup || !artifact.backedUp) continue;
2952
3392
  try {
2953
- await (0, import_promises4.rename)(artifact.backup, artifact.destination);
3393
+ await (0, import_promises5.rename)(artifact.backup, artifact.destination);
2954
3394
  artifact.backedUp = false;
2955
3395
  } catch (error) {
2956
3396
  errors.push(
@@ -2967,7 +3407,7 @@ async function writeArtifactBundle(files, options = {}) {
2967
3407
  if (!files.length) throw new Error("An artifact bundle must contain at least one file.");
2968
3408
  const normalized = files.map((file) => {
2969
3409
  if (!file.path.trim()) throw new Error("Artifact paths cannot be empty.");
2970
- return { destination: import_node_path5.default.resolve(file.path), data: Buffer.from(file.data) };
3410
+ return { destination: import_node_path6.default.resolve(file.path), data: Buffer.from(file.data) };
2971
3411
  });
2972
3412
  const destinations = /* @__PURE__ */ new Set();
2973
3413
  for (const artifact of normalized) {
@@ -2983,7 +3423,7 @@ async function writeArtifactBundle(files, options = {}) {
2983
3423
  const unchanged = [];
2984
3424
  for (const artifact of normalized) {
2985
3425
  try {
2986
- const current = await (0, import_promises4.readFile)(artifact.destination);
3426
+ const current = await (0, import_promises5.readFile)(artifact.destination);
2987
3427
  if (current.equals(artifact.data)) {
2988
3428
  unchanged.push(artifact.destination);
2989
3429
  } else {
@@ -2995,23 +3435,23 @@ async function writeArtifactBundle(files, options = {}) {
2995
3435
  }
2996
3436
  }
2997
3437
  if (!changed.length) return { changed: [], unchanged };
2998
- const token = (0, import_node_crypto4.randomUUID)();
3438
+ const token = (0, import_node_crypto5.randomUUID)();
2999
3439
  for (const [index, artifact] of changed.entries()) {
3000
- const basename = import_node_path5.default.basename(artifact.destination);
3001
- artifact.stage = import_node_path5.default.join(
3002
- import_node_path5.default.dirname(artifact.destination),
3440
+ const basename = import_node_path6.default.basename(artifact.destination);
3441
+ artifact.stage = import_node_path6.default.join(
3442
+ import_node_path6.default.dirname(artifact.destination),
3003
3443
  `.${basename}.pixelkiln-stage-${token}-${index}`
3004
3444
  );
3005
3445
  if (artifact.existed) {
3006
- artifact.backup = import_node_path5.default.join(
3007
- import_node_path5.default.dirname(artifact.destination),
3446
+ artifact.backup = import_node_path6.default.join(
3447
+ import_node_path6.default.dirname(artifact.destination),
3008
3448
  `.${basename}.pixelkiln-backup-${token}-${index}`
3009
3449
  );
3010
3450
  }
3011
3451
  }
3012
- const journal = options.recoveryFile ? import_node_path5.default.resolve(options.recoveryFile) : null;
3452
+ const journal = options.recoveryFile ? import_node_path6.default.resolve(options.recoveryFile) : null;
3013
3453
  if (journal) {
3014
- await (0, import_promises4.mkdir)(import_node_path5.default.dirname(journal), { recursive: true });
3454
+ await (0, import_promises5.mkdir)(import_node_path6.default.dirname(journal), { recursive: true });
3015
3455
  const transaction = {
3016
3456
  format: "pixelkiln-artifact-transaction",
3017
3457
  version: 1,
@@ -3025,7 +3465,7 @@ async function writeArtifactBundle(files, options = {}) {
3025
3465
  }))
3026
3466
  };
3027
3467
  try {
3028
- await (0, import_promises4.writeFile)(journal, JSON.stringify(transaction, null, 2) + "\n", { flag: "wx" });
3468
+ await (0, import_promises5.writeFile)(journal, JSON.stringify(transaction, null, 2) + "\n", { flag: "wx" });
3029
3469
  activeTransactions.add(journal);
3030
3470
  } catch (error) {
3031
3471
  throw new Error(
@@ -3036,9 +3476,9 @@ async function writeArtifactBundle(files, options = {}) {
3036
3476
  }
3037
3477
  try {
3038
3478
  for (const [index, artifact] of changed.entries()) {
3039
- await (0, import_promises4.mkdir)(import_node_path5.default.dirname(artifact.destination), { recursive: true });
3479
+ await (0, import_promises5.mkdir)(import_node_path6.default.dirname(artifact.destination), { recursive: true });
3040
3480
  await options.beforeStage?.(artifact.destination, index);
3041
- await (0, import_promises4.writeFile)(artifact.stage, artifact.data, { flag: "wx" });
3481
+ await (0, import_promises5.writeFile)(artifact.stage, artifact.data, { flag: "wx" });
3042
3482
  }
3043
3483
  } catch (error) {
3044
3484
  const cleanupErrors2 = await rollback(changed);
@@ -3051,17 +3491,17 @@ async function writeArtifactBundle(files, options = {}) {
3051
3491
  try {
3052
3492
  for (const artifact of changed) {
3053
3493
  if (!artifact.existed) continue;
3054
- await (0, import_promises4.rename)(artifact.destination, artifact.backup);
3494
+ await (0, import_promises5.rename)(artifact.destination, artifact.backup);
3055
3495
  artifact.backedUp = true;
3056
3496
  }
3057
3497
  for (const [index, artifact] of changed.entries()) {
3058
3498
  await options.beforePromote?.(artifact.destination, index);
3059
- await (0, import_promises4.rename)(artifact.stage, artifact.destination);
3499
+ await (0, import_promises5.rename)(artifact.stage, artifact.destination);
3060
3500
  artifact.stage = void 0;
3061
3501
  artifact.promoted = true;
3062
3502
  }
3063
3503
  if (journal) {
3064
- await (0, import_promises4.writeFile)(transactionMarker(journal), `${token}
3504
+ await (0, import_promises5.writeFile)(transactionMarker(journal), `${token}
3065
3505
  `, { flag: "wx" });
3066
3506
  durableCommit = true;
3067
3507
  await options.afterCommit?.();
@@ -3105,7 +3545,7 @@ async function writeArtifactBundle(files, options = {}) {
3105
3545
 
3106
3546
  // src/pipeline/plan.ts
3107
3547
  var import_node_fs4 = require("fs");
3108
- var import_node_path6 = __toESM(require("path"), 1);
3548
+ var import_node_path7 = __toESM(require("path"), 1);
3109
3549
  async function anyOutputModified(entry, spec) {
3110
3550
  for (let index = 0; index < entry.outputs.length; index++) {
3111
3551
  const output = entry.outputs[index];
@@ -3124,7 +3564,7 @@ async function buildPlan(specs, lock, opts = {}) {
3124
3564
  state = "missing";
3125
3565
  reason = "--force";
3126
3566
  } else if (!entry && spec.source) {
3127
- if ((0, import_node_fs4.existsSync)(import_node_path6.default.resolve(spec.root, spec.source))) {
3567
+ if ((0, import_node_fs4.existsSync)(import_node_path7.default.resolve(spec.root, spec.source))) {
3128
3568
  state = "ok";
3129
3569
  reason = `placed from ${spec.source}; not generated`;
3130
3570
  } else {
@@ -3190,9 +3630,9 @@ function summarize(plan) {
3190
3630
  }
3191
3631
 
3192
3632
  // src/pipeline/audit.ts
3193
- var import_promises5 = require("fs/promises");
3633
+ var import_promises6 = require("fs/promises");
3194
3634
  var import_node_fs5 = require("fs");
3195
- var import_node_path7 = __toESM(require("path"), 1);
3635
+ var import_node_path8 = __toESM(require("path"), 1);
3196
3636
  function colorDistance(a, b) {
3197
3637
  const rmean = (a.r + b.r) / 2;
3198
3638
  const dr = a.r - b.r;
@@ -3242,7 +3682,7 @@ async function auditStyle(loaded, specs, styleId, lock) {
3242
3682
  continue;
3243
3683
  }
3244
3684
  try {
3245
- const png = decodePng(await (0, import_promises5.readFile)(output.absolutePath));
3685
+ const png = decodePng(await (0, import_promises6.readFile)(output.absolutePath));
3246
3686
  const palette = extractPalette(png, 12);
3247
3687
  assets.push({
3248
3688
  assetId: spec.assetId,
@@ -3266,10 +3706,10 @@ async function auditStyle(loaded, specs, styleId, lock) {
3266
3706
  let referenceFromStyleImages = false;
3267
3707
  const refPalettes = [];
3268
3708
  for (const img of style.styleImages) {
3269
- const abs = import_node_path7.default.resolve(loaded.root, img.path);
3709
+ const abs = import_node_path8.default.resolve(loaded.root, img.path);
3270
3710
  if (!(0, import_node_fs5.existsSync)(abs)) continue;
3271
3711
  try {
3272
- refPalettes.push(extractPalette(decodePng(await (0, import_promises5.readFile)(abs)), 12));
3712
+ refPalettes.push(extractPalette(decodePng(await (0, import_promises6.readFile)(abs)), 12));
3273
3713
  } catch {
3274
3714
  }
3275
3715
  }
@@ -3346,13 +3786,13 @@ function hex(c) {
3346
3786
 
3347
3787
  // src/pipeline/cache-health.ts
3348
3788
  var import_node_fs7 = require("fs");
3349
- var import_promises7 = require("fs/promises");
3350
- var import_node_path9 = __toESM(require("path"), 1);
3789
+ var import_promises8 = require("fs/promises");
3790
+ var import_node_path10 = __toESM(require("path"), 1);
3351
3791
 
3352
3792
  // src/cache.ts
3353
- var import_promises6 = require("fs/promises");
3793
+ var import_promises7 = require("fs/promises");
3354
3794
  var import_node_fs6 = require("fs");
3355
- var import_node_path8 = __toESM(require("path"), 1);
3795
+ var import_node_path9 = __toESM(require("path"), 1);
3356
3796
  var import_zod3 = require("zod");
3357
3797
  var HashCacheSchema = import_zod3.z.object({
3358
3798
  version: import_zod3.z.literal(1),
@@ -3366,10 +3806,10 @@ function isSha256Hash(value) {
3366
3806
  function parseCache(value) {
3367
3807
  return HashCacheSchema.parse(value);
3368
3808
  }
3369
- async function loadCache(path17) {
3370
- if (!(0, import_node_fs6.existsSync)(path17)) return { version: 1, hashes: {} };
3809
+ async function loadCache(path18) {
3810
+ if (!(0, import_node_fs6.existsSync)(path18)) return { version: 1, hashes: {} };
3371
3811
  try {
3372
- const parsed = HashCacheSchema.safeParse(JSON.parse(await (0, import_promises6.readFile)(path17, "utf8")));
3812
+ const parsed = HashCacheSchema.safeParse(JSON.parse(await (0, import_promises7.readFile)(path18, "utf8")));
3373
3813
  if (!parsed.success) return { version: 1, hashes: {} };
3374
3814
  return {
3375
3815
  version: 1,
@@ -3381,20 +3821,20 @@ async function loadCache(path17) {
3381
3821
  return { version: 1, hashes: {} };
3382
3822
  }
3383
3823
  }
3384
- async function saveCache(path17, cache) {
3824
+ async function saveCache(path18, cache) {
3385
3825
  const sorted = {};
3386
3826
  for (const key of Object.keys(cache.hashes).sort()) {
3387
3827
  const hash = cache.hashes[key];
3388
3828
  if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
3389
3829
  sorted[key] = hash;
3390
3830
  }
3391
- await (0, import_promises6.mkdir)(import_node_path8.default.dirname(import_node_path8.default.resolve(path17)), { recursive: true });
3392
- const tmp = `${path17}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
3831
+ await (0, import_promises7.mkdir)(import_node_path9.default.dirname(import_node_path9.default.resolve(path18)), { recursive: true });
3832
+ const tmp = `${path18}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
3393
3833
  try {
3394
- await (0, import_promises6.writeFile)(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
3395
- await (0, import_promises6.rename)(tmp, path17);
3834
+ await (0, import_promises7.writeFile)(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
3835
+ await (0, import_promises7.rename)(tmp, path18);
3396
3836
  } finally {
3397
- await (0, import_promises6.rm)(tmp, { force: true });
3837
+ await (0, import_promises7.rm)(tmp, { force: true });
3398
3838
  }
3399
3839
  }
3400
3840
  function pruneCache(cache, liveIds) {
@@ -3415,10 +3855,10 @@ function cachePathFor(lockPath) {
3415
3855
  // src/pipeline/cache-health.ts
3416
3856
  async function inspectCaches(lock, lockPath, options = {}) {
3417
3857
  if (options.prune && !(0, import_node_fs7.existsSync)(lockPath)) {
3418
- throw new Error(`Refusing to prune without an existing lockfile at ${import_node_path9.default.resolve(lockPath)}`);
3858
+ throw new Error(`Refusing to prune without an existing lockfile at ${import_node_path10.default.resolve(lockPath)}`);
3419
3859
  }
3420
- const contentDir = import_node_path9.default.resolve(import_node_path9.default.dirname(lockPath), ".pixelkiln", "cache");
3421
- const remotePath = import_node_path9.default.resolve(cachePathFor(lockPath));
3860
+ const contentDir = import_node_path10.default.resolve(import_node_path10.default.dirname(lockPath), ".pixelkiln", "cache");
3861
+ const remotePath = import_node_path10.default.resolve(cachePathFor(lockPath));
3422
3862
  const referenced = new Set(
3423
3863
  Object.values(lock.entries).flatMap((entry) => entry.outputs.map((output) => output.sha256))
3424
3864
  );
@@ -3432,7 +3872,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
3432
3872
  ]);
3433
3873
  for (const name of names) {
3434
3874
  try {
3435
- await (0, import_promises7.rm)(import_node_path9.default.join(contentDir, name), { force: true });
3875
+ await (0, import_promises8.rm)(import_node_path10.default.join(contentDir, name), { force: true });
3436
3876
  removed.contentFiles++;
3437
3877
  } catch {
3438
3878
  }
@@ -3441,7 +3881,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
3441
3881
  await saveCache(remotePath, { version: 1, hashes: {} });
3442
3882
  removed.resetRemoteHashCache = true;
3443
3883
  } else if (remoteHashes.invalidIds.length) {
3444
- const cache = parseCache(JSON.parse(await (0, import_promises7.readFile)(remotePath, "utf8")));
3884
+ const cache = parseCache(JSON.parse(await (0, import_promises8.readFile)(remotePath, "utf8")));
3445
3885
  for (const id of remoteHashes.invalidIds) delete cache.hashes[id];
3446
3886
  removed.remoteHashEntries = remoteHashes.invalidIds.length;
3447
3887
  await saveCache(remotePath, cache);
@@ -3473,19 +3913,19 @@ async function inspectContentCache(contentDir, referenced) {
3473
3913
  return report;
3474
3914
  }
3475
3915
  const present = /* @__PURE__ */ new Set();
3476
- const entries = await (0, import_promises7.readdir)(contentDir, { withFileTypes: true });
3916
+ const entries = await (0, import_promises8.readdir)(contentDir, { withFileTypes: true });
3477
3917
  for (const entry of entries) {
3478
3918
  if (!entry.isFile()) {
3479
3919
  report.invalid.push({ name: entry.name, reason: "not a regular file" });
3480
3920
  continue;
3481
3921
  }
3482
3922
  report.files++;
3483
- const file = import_node_path9.default.join(contentDir, entry.name);
3923
+ const file = import_node_path10.default.join(contentDir, entry.name);
3484
3924
  const mediaType = mediaTypeFromExtension(entry.name);
3485
3925
  const expected = mediaType ? entry.name.slice(0, -4) : "";
3486
3926
  let bytes;
3487
3927
  try {
3488
- bytes = await (0, import_promises7.readFile)(file);
3928
+ bytes = await (0, import_promises8.readFile)(file);
3489
3929
  report.bytes += bytes.length;
3490
3930
  } catch (err) {
3491
3931
  report.invalid.push({
@@ -3532,7 +3972,7 @@ async function inspectRemoteHashCache(remotePath) {
3532
3972
  if (!report.exists) return report;
3533
3973
  let cache;
3534
3974
  try {
3535
- cache = parseCache(JSON.parse(await (0, import_promises7.readFile)(remotePath, "utf8")));
3975
+ cache = parseCache(JSON.parse(await (0, import_promises8.readFile)(remotePath, "utf8")));
3536
3976
  } catch (err) {
3537
3977
  report.error = err instanceof Error ? err.message : String(err);
3538
3978
  return report;
@@ -3548,18 +3988,18 @@ async function inspectRemoteHashCache(remotePath) {
3548
3988
 
3549
3989
  // src/pipeline/pack.ts
3550
3990
  var import_node_fs8 = require("fs");
3551
- var import_node_path10 = __toESM(require("path"), 1);
3991
+ var import_node_path11 = __toESM(require("path"), 1);
3552
3992
  function resolvePackInputs(raw, inputsFilePath) {
3553
3993
  if (!Array.isArray(raw) || !raw.length) {
3554
3994
  throw new Error("--inputs must be a non-empty JSON array of { id, path }");
3555
3995
  }
3556
- const baseDir = import_node_path10.default.dirname(import_node_path10.default.resolve(inputsFilePath));
3996
+ const baseDir = import_node_path11.default.dirname(import_node_path11.default.resolve(inputsFilePath));
3557
3997
  return raw.map((entry, i) => {
3558
3998
  const e = entry;
3559
3999
  if (typeof e.id !== "string" || typeof e.path !== "string") {
3560
4000
  throw new Error(`--inputs[${i}] needs string "id" and "path"`);
3561
4001
  }
3562
- return { id: e.id, path: import_node_path10.default.resolve(baseDir, e.path) };
4002
+ return { id: e.id, path: import_node_path11.default.resolve(baseDir, e.path) };
3563
4003
  });
3564
4004
  }
3565
4005
  function packSprites(inputs, options = {}) {
@@ -3756,7 +4196,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
3756
4196
  const cell = cells[id];
3757
4197
  const source = sources[id];
3758
4198
  if (source) {
3759
- placements.push({ id, path: import_node_path10.default.resolve(manifestDir, source), cell });
4199
+ placements.push({ id, path: import_node_path11.default.resolve(manifestDir, source), cell });
3760
4200
  continue;
3761
4201
  }
3762
4202
  const entry = lock.entries[`${styleId}/${id}`];
@@ -3780,7 +4220,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
3780
4220
  `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}`
3781
4221
  );
3782
4222
  }
3783
- const basePng = mount.base ? (0, import_node_fs8.readFileSync)(import_node_path10.default.resolve(manifestDir, mount.base)) : void 0;
4223
+ const basePng = mount.base ? (0, import_node_fs8.readFileSync)(import_node_path11.default.resolve(manifestDir, mount.base)) : void 0;
3784
4224
  const mounted = mountSprites(placements, {
3785
4225
  cellWidth: mount.cellWidth,
3786
4226
  cellHeight: mount.cellHeight,
@@ -3790,7 +4230,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
3790
4230
  ...mounted,
3791
4231
  sources: mount.base ? [{
3792
4232
  id: "$base",
3793
- path: import_node_path10.default.resolve(manifestDir, mount.base),
4233
+ path: import_node_path11.default.resolve(manifestDir, mount.base),
3794
4234
  sha256: sha256(basePng),
3795
4235
  included: true
3796
4236
  }, ...mounted.sources] : mounted.sources,
@@ -4202,7 +4642,11 @@ async function poll(provider, lock, lockPath, opts = {}) {
4202
4642
  spec: currentSpec
4203
4643
  });
4204
4644
  if (state.status === "review") {
4205
- upsert(lock, key, { status: "review", reviewObjectId: entry.jobId });
4645
+ upsert(lock, key, {
4646
+ status: "review",
4647
+ reviewObjectId: entry.jobId,
4648
+ providerMetadata: state.metadata ? { ...entry.providerMetadata, [provider.id]: state.metadata } : entry.providerMetadata
4649
+ });
4206
4650
  result.review++;
4207
4651
  log(` review ${key} (${state.candidateUrls.length} candidates)`);
4208
4652
  } else if (state.status === "ready") {
@@ -4235,15 +4679,15 @@ async function poll(provider, lock, lockPath, opts = {}) {
4235
4679
 
4236
4680
  // src/pipeline/fetch.ts
4237
4681
  var import_node_fs9 = require("fs");
4238
- var import_promises8 = require("fs/promises");
4239
- var import_node_path11 = __toESM(require("path"), 1);
4682
+ var import_promises9 = require("fs/promises");
4683
+ var import_node_path12 = __toESM(require("path"), 1);
4240
4684
  async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
4241
4685
  const log = opts.onProgress ?? (() => {
4242
4686
  });
4243
4687
  const result = { downloaded: 0, skipped: 0, failed: 0 };
4244
4688
  const specByKey = new Map(specs.map((s) => [lockKey(s.styleId, s.assetId), s]));
4245
4689
  normalizeLockOutputPaths(lock, specs);
4246
- const cacheDir = opts.cacheDir === false ? null : import_node_path11.default.resolve(opts.cacheDir ?? import_node_path11.default.join(import_node_path11.default.dirname(lockPath), ".pixelkiln", "cache"));
4690
+ const cacheDir = opts.cacheDir === false ? null : import_node_path12.default.resolve(opts.cacheDir ?? import_node_path12.default.join(import_node_path12.default.dirname(lockPath), ".pixelkiln", "cache"));
4247
4691
  const pending = Object.entries(lock.entries).filter(([key, e]) => {
4248
4692
  if (e.status === "selected" || e.status === "download-failed") return true;
4249
4693
  if (!opts.repair || e.status !== "downloaded") return false;
@@ -4295,7 +4739,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
4295
4739
  if (cacheDir) {
4296
4740
  await cacheMedia(
4297
4741
  cacheDir,
4298
- await (0, import_promises8.readFile)(target),
4742
+ await (0, import_promises9.readFile)(target),
4299
4743
  recorded.mediaType ?? MediaType.PNG,
4300
4744
  recorded.sha256
4301
4745
  );
@@ -4306,7 +4750,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
4306
4750
  const expectedMediaType = source.mediaType ?? recorded?.mediaType ?? MediaType.PNG;
4307
4751
  let buf = recorded && cacheDir ? await readCachedMedia(cacheDir, recorded.sha256, expectedMediaType) : null;
4308
4752
  if (buf) {
4309
- log(` cached ${import_node_path11.default.relative(process.cwd(), target)}`);
4753
+ log(` cached ${import_node_path12.default.relative(process.cwd(), target)}`);
4310
4754
  } else {
4311
4755
  if (!source.url) {
4312
4756
  throw new Error(`no source URL or cached bytes remain for ${source.role ?? "asset"}`);
@@ -4324,9 +4768,9 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
4324
4768
  );
4325
4769
  }
4326
4770
  if (cacheDir) await cacheMedia(cacheDir, buf, mediaType);
4327
- await (0, import_promises8.mkdir)(import_node_path11.default.dirname(target), { recursive: true });
4771
+ await (0, import_promises9.mkdir)(import_node_path12.default.dirname(target), { recursive: true });
4328
4772
  const tmp = `${target}.pixelkiln.tmp`;
4329
- await (0, import_promises8.writeFile)(tmp, buf);
4773
+ await (0, import_promises9.writeFile)(tmp, buf);
4330
4774
  outputs.push({
4331
4775
  path: portableOutputPath(target, spec.root),
4332
4776
  sha256: sha256(buf),
@@ -4335,8 +4779,8 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
4335
4779
  });
4336
4780
  upsert(lock, key, { outputs: mergeOutputs(entry.outputs, outputs) });
4337
4781
  await saveLock(lockPath, lock);
4338
- await (0, import_promises8.rename)(tmp, target);
4339
- log(` wrote ${import_node_path11.default.relative(process.cwd(), target)}`);
4782
+ await (0, import_promises9.rename)(tmp, target);
4783
+ log(` wrote ${import_node_path12.default.relative(process.cwd(), target)}`);
4340
4784
  }
4341
4785
  upsert(lock, key, {
4342
4786
  status: "downloaded",
@@ -4369,10 +4813,10 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
4369
4813
  return result;
4370
4814
  }
4371
4815
  async function readCachedMedia(cacheDir, hash, mediaType) {
4372
- const file = import_node_path11.default.join(cacheDir, cacheFileName(hash, mediaType));
4816
+ const file = import_node_path12.default.join(cacheDir, cacheFileName(hash, mediaType));
4373
4817
  if (!(0, import_node_fs9.existsSync)(file)) return null;
4374
4818
  try {
4375
- const buf = await (0, import_promises8.readFile)(file);
4819
+ const buf = await (0, import_promises9.readFile)(file);
4376
4820
  if (sha256(buf) !== hash) return null;
4377
4821
  validateMedia(buf, mediaType);
4378
4822
  return buf;
@@ -4383,17 +4827,17 @@ async function readCachedMedia(cacheDir, hash, mediaType) {
4383
4827
  async function cacheMedia(cacheDir, buf, mediaType, knownHash) {
4384
4828
  const hash = knownHash ?? sha256(buf);
4385
4829
  validateMedia(buf, mediaType);
4386
- const file = import_node_path11.default.join(cacheDir, cacheFileName(hash, mediaType));
4830
+ const file = import_node_path12.default.join(cacheDir, cacheFileName(hash, mediaType));
4387
4831
  if (await readCachedMedia(cacheDir, hash, mediaType)) return;
4388
- await (0, import_promises8.mkdir)(cacheDir, { recursive: true });
4832
+ await (0, import_promises9.mkdir)(cacheDir, { recursive: true });
4389
4833
  const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
4390
- await (0, import_promises8.writeFile)(tmp, buf);
4834
+ await (0, import_promises9.writeFile)(tmp, buf);
4391
4835
  try {
4392
- await (0, import_promises8.rename)(tmp, file);
4836
+ await (0, import_promises9.rename)(tmp, file);
4393
4837
  } catch (err) {
4394
4838
  if (!(0, import_node_fs9.existsSync)(file)) throw err;
4395
4839
  } finally {
4396
- await (0, import_promises8.rm)(tmp, { force: true });
4840
+ await (0, import_promises9.rm)(tmp, { force: true });
4397
4841
  }
4398
4842
  }
4399
4843
  function mergeOutputs(previous, current) {
@@ -4425,8 +4869,8 @@ async function pushTags(provider, specs, lock, opts = {}) {
4425
4869
 
4426
4870
  // src/pipeline/doctor.ts
4427
4871
  var import_node_fs10 = require("fs");
4428
- var import_promises9 = require("fs/promises");
4429
- var import_node_path12 = __toESM(require("path"), 1);
4872
+ var import_promises10 = require("fs/promises");
4873
+ var import_node_path13 = __toESM(require("path"), 1);
4430
4874
  async function doctor(loaded, specs, lock, lockPath, opts = {}) {
4431
4875
  const checks = [];
4432
4876
  const add = (id, level, message2) => checks.push({ id, level, message: message2 });
@@ -4436,12 +4880,12 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
4436
4880
  `${Object.keys(loaded.manifest.styles).length} style(s), ${Object.keys(loaded.manifest.assets).length} asset(s), ${specs.length} resolved spec(s)`
4437
4881
  );
4438
4882
  add("lock", "ok", `${Object.keys(lock.entries).length} valid v2 lock entr(ies)`);
4439
- const dirs = [...new Set(specs.map((s) => import_node_path12.default.dirname(s.outFile)))];
4883
+ const dirs = [...new Set(specs.map((s) => import_node_path13.default.dirname(s.outFile)))];
4440
4884
  const unwritable = [];
4441
4885
  for (const dir of dirs) {
4442
4886
  const ancestor = await nearestExistingDirectory(dir);
4443
4887
  try {
4444
- await (0, import_promises9.access)(ancestor, import_node_fs10.constants.W_OK);
4888
+ await (0, import_promises10.access)(ancestor, import_node_fs10.constants.W_OK);
4445
4889
  } catch {
4446
4890
  unwritable.push(dir);
4447
4891
  }
@@ -4458,7 +4902,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
4458
4902
  for (let index = 0; index < entry.outputs.length; index++) {
4459
4903
  const output = entry.outputs[index];
4460
4904
  const spec = specByKey.get(key);
4461
- const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, import_node_path12.default.dirname(lockPath));
4905
+ const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, import_node_path13.default.dirname(lockPath));
4462
4906
  const owner = outputOwners.get(absolute);
4463
4907
  if (owner && owner !== key) duplicateOutputs.push(`${output.path} (${owner}, ${key})`);
4464
4908
  outputOwners.set(absolute, key);
@@ -4473,7 +4917,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
4473
4917
  } else {
4474
4918
  add("lock-outputs", "ok", `${outputOwners.size} recorded output path(s) are uniquely owned`);
4475
4919
  }
4476
- const cacheDir = import_node_path12.default.join(import_node_path12.default.dirname(lockPath), ".pixelkiln", "cache");
4920
+ const cacheDir = import_node_path13.default.join(import_node_path13.default.dirname(lockPath), ".pixelkiln", "cache");
4477
4921
  const stranded = [];
4478
4922
  const invalidState = [];
4479
4923
  const staleMapUrls = [];
@@ -4489,7 +4933,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
4489
4933
  const hasSource = Boolean(entry.sourceUrl || entry.sourceUrls?.length);
4490
4934
  let hasCache = false;
4491
4935
  for (const output of entry.outputs) {
4492
- const cached = import_node_path12.default.join(
4936
+ const cached = import_node_path13.default.join(
4493
4937
  cacheDir,
4494
4938
  cacheFileName(output.sha256, output.mediaType ?? MediaType.PNG)
4495
4939
  );
@@ -4524,8 +4968,15 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
4524
4968
  "error",
4525
4969
  opts.apiKeyPresent === false ? `${opts.credentialEnv ?? "PIXELLAB_API_KEY"} is not configured` : "provider is not configured"
4526
4970
  );
4971
+ } else if (!opts.provider.balance && !opts.provider.checkConnection) {
4972
+ add("provider", "ok", `${opts.provider.id} configured; connectivity check unavailable`);
4527
4973
  } else if (!opts.provider.balance) {
4528
- add("provider", "ok", `${opts.provider.id} configured; balance reporting unavailable`);
4974
+ try {
4975
+ await opts.provider.checkConnection();
4976
+ add("provider", "ok", `${opts.provider.id} reachable; balance reporting unavailable`);
4977
+ } catch (err) {
4978
+ add("provider", "error", `provider connectivity failed: ${err instanceof Error ? err.message : String(err)}`);
4979
+ }
4529
4980
  } else {
4530
4981
  const balanceFn = opts.provider.balance.bind(opts.provider);
4531
4982
  try {
@@ -4541,10 +4992,10 @@ async function nearestExistingDirectory(start) {
4541
4992
  let current = start;
4542
4993
  for (; ; ) {
4543
4994
  try {
4544
- if ((await (0, import_promises9.stat)(current)).isDirectory()) return current;
4995
+ if ((await (0, import_promises10.stat)(current)).isDirectory()) return current;
4545
4996
  } catch {
4546
4997
  }
4547
- const parent = import_node_path12.default.dirname(current);
4998
+ const parent = import_node_path13.default.dirname(current);
4548
4999
  if (parent === current) return current;
4549
5000
  current = parent;
4550
5001
  }
@@ -4552,8 +5003,8 @@ async function nearestExistingDirectory(start) {
4552
5003
 
4553
5004
  // src/pipeline/adopt.ts
4554
5005
  var import_node_fs11 = require("fs");
4555
- var import_promises10 = require("fs/promises");
4556
- var import_node_path13 = __toESM(require("path"), 1);
5006
+ var import_promises11 = require("fs/promises");
5007
+ var import_node_path14 = __toESM(require("path"), 1);
4557
5008
  async function adopt(provider, specs, lock, lockPath, opts = {}) {
4558
5009
  const log = opts.onProgress ?? (() => {
4559
5010
  });
@@ -4568,7 +5019,7 @@ async function adopt(provider, specs, lock, lockPath, opts = {}) {
4568
5019
  unmatchedLocal.push(`${spec.styleId}/${spec.assetId} (no file at ${spec.outFile})`);
4569
5020
  continue;
4570
5021
  }
4571
- const bytes = await (0, import_promises10.readFile)(spec.outFile);
5022
+ const bytes = await (0, import_promises11.readFile)(spec.outFile);
4572
5023
  try {
4573
5024
  decodePng(bytes);
4574
5025
  } catch (err) {
@@ -5044,9 +5495,9 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
5044
5495
  }
5045
5496
 
5046
5497
  // src/pipeline/init.ts
5047
- var import_promises11 = require("fs/promises");
5498
+ var import_promises12 = require("fs/promises");
5048
5499
  var import_node_fs12 = require("fs");
5049
- var import_node_path14 = __toESM(require("path"), 1);
5500
+ var import_node_path15 = __toESM(require("path"), 1);
5050
5501
  function pngSize(buf) {
5051
5502
  if (buf.length < 24) return null;
5052
5503
  const isPng = buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834;
@@ -5059,8 +5510,8 @@ function slugify(value) {
5059
5510
  }
5060
5511
  async function walk(dir, exclude) {
5061
5512
  const out = [];
5062
- for (const entry of await (0, import_promises11.readdir)(dir, { withFileTypes: true })) {
5063
- const full = import_node_path14.default.join(dir, entry.name);
5513
+ for (const entry of await (0, import_promises12.readdir)(dir, { withFileTypes: true })) {
5514
+ const full = import_node_path15.default.join(dir, entry.name);
5064
5515
  if (exclude.some((x) => full.includes(x))) continue;
5065
5516
  if (entry.isDirectory()) out.push(...await walk(full, exclude));
5066
5517
  else if (entry.name.toLowerCase().endsWith(".png")) out.push(full);
@@ -5074,14 +5525,14 @@ async function scanAssets(root, opts = {}) {
5074
5525
  const skipped = [];
5075
5526
  const seen = /* @__PURE__ */ new Map();
5076
5527
  for (const file of files.sort()) {
5077
- const size = pngSize(await (0, import_promises11.readFile)(file));
5528
+ const size = pngSize(await (0, import_promises12.readFile)(file));
5078
5529
  if (!size) {
5079
5530
  skipped.push(`${file} (not a readable PNG)`);
5080
5531
  continue;
5081
5532
  }
5082
- const rel = import_node_path14.default.relative(root, file);
5083
- const category = import_node_path14.default.dirname(rel) === "." ? "" : import_node_path14.default.dirname(rel);
5084
- let id = slugify(import_node_path14.default.basename(rel));
5533
+ const rel = import_node_path15.default.relative(root, file);
5534
+ const category = import_node_path15.default.dirname(rel) === "." ? "" : import_node_path15.default.dirname(rel);
5535
+ let id = slugify(import_node_path15.default.basename(rel));
5085
5536
  if (seen.has(id)) {
5086
5537
  const n = seen.get(id) + 1;
5087
5538
  seen.set(id, n);
@@ -5137,16 +5588,16 @@ function mode(values) {
5137
5588
  }
5138
5589
 
5139
5590
  // src/pipeline/salvage.ts
5140
- var import_promises12 = require("fs/promises");
5591
+ var import_promises13 = require("fs/promises");
5141
5592
  var import_node_fs13 = require("fs");
5142
- var import_node_path15 = __toESM(require("path"), 1);
5593
+ var import_node_path16 = __toESM(require("path"), 1);
5143
5594
  async function loadClaims(lockPaths) {
5144
5595
  const claimed = /* @__PURE__ */ new Set();
5145
5596
  for (const p of lockPaths) {
5146
5597
  if (!(0, import_node_fs13.existsSync)(p)) throw new Error(`Claim lockfile not found: ${p}`);
5147
5598
  let parsed;
5148
5599
  try {
5149
- parsed = parseLock(JSON.parse(await (0, import_promises12.readFile)(p, "utf8")));
5600
+ parsed = parseLock(JSON.parse(await (0, import_promises13.readFile)(p, "utf8")));
5150
5601
  } catch {
5151
5602
  throw new Error(`Claim lockfile is malformed: ${p}`);
5152
5603
  }
@@ -5193,20 +5644,20 @@ function matchOrphanStyle(prompt, manifest) {
5193
5644
  return matchStyleByPattern(prompt, manifest);
5194
5645
  }
5195
5646
  async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, claimPaths) {
5196
- const own = import_node_path15.default.resolve(ownManifestPath);
5647
+ const own = import_node_path16.default.resolve(ownManifestPath);
5197
5648
  const siblingManifestPaths = [
5198
5649
  .../* @__PURE__ */ new Set([
5199
5650
  ...workspaceManifestPaths,
5200
- ...claimPaths.map((c) => import_node_path15.default.join(import_node_path15.default.dirname(import_node_path15.default.resolve(c)), "pixelkiln.manifest.json"))
5651
+ ...claimPaths.map((c) => import_node_path16.default.join(import_node_path16.default.dirname(import_node_path16.default.resolve(c)), "pixelkiln.manifest.json"))
5201
5652
  ])
5202
5653
  ];
5203
5654
  const siblings = [];
5204
5655
  for (const siblingManifestPath of siblingManifestPaths) {
5205
- if (import_node_path15.default.resolve(siblingManifestPath) === own) continue;
5656
+ if (import_node_path16.default.resolve(siblingManifestPath) === own) continue;
5206
5657
  if (!(0, import_node_fs13.existsSync)(siblingManifestPath)) continue;
5207
5658
  try {
5208
5659
  const { manifest } = await loadManifest(siblingManifestPath);
5209
- siblings.push({ label: import_node_path15.default.basename(import_node_path15.default.dirname(siblingManifestPath)), manifest });
5660
+ siblings.push({ label: import_node_path16.default.basename(import_node_path16.default.dirname(siblingManifestPath)), manifest });
5210
5661
  } catch {
5211
5662
  }
5212
5663
  }
@@ -5322,8 +5773,8 @@ async function applyTags(provider, decisions, existing, opts = {}) {
5322
5773
  }
5323
5774
 
5324
5775
  // src/pick/salvage-server.ts
5325
- var import_promises13 = require("fs/promises");
5326
- var import_node_path16 = __toESM(require("path"), 1);
5776
+ var import_promises14 = require("fs/promises");
5777
+ var import_node_path17 = __toESM(require("path"), 1);
5327
5778
 
5328
5779
  // src/pick/salvage-sheet.ts
5329
5780
  var escapeHtml2 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
@@ -5488,7 +5939,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
5488
5939
  });
5489
5940
  const html = renderSalvageSheet(orphans, {
5490
5941
  styleId: ctx.styleId,
5491
- importDir: import_node_path16.default.relative(process.cwd(), ctx.importDir) || "."
5942
+ importDir: import_node_path17.default.relative(process.cwd(), ctx.importDir) || "."
5492
5943
  });
5493
5944
  const byId = new Map(orphans.map((o) => [o.id, o]));
5494
5945
  const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
@@ -5517,10 +5968,10 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
5517
5968
  if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
5518
5969
  decodePng(buf);
5519
5970
  const assetId = idFromPrompt(orphan.prompt, taken);
5520
- const rel = import_node_path16.default.join("_salvaged", `${assetId}.png`);
5521
- const outFile = import_node_path16.default.resolve(ctx.importDir, rel);
5522
- await (0, import_promises13.mkdir)(import_node_path16.default.dirname(outFile), { recursive: true });
5523
- await (0, import_promises13.writeFile)(outFile, buf);
5971
+ const rel = import_node_path17.default.join("_salvaged", `${assetId}.png`);
5972
+ const outFile = import_node_path17.default.resolve(ctx.importDir, rel);
5973
+ await (0, import_promises14.mkdir)(import_node_path17.default.dirname(outFile), { recursive: true });
5974
+ await (0, import_promises14.writeFile)(outFile, buf);
5524
5975
  ctx.manifest.assets[assetId] = {
5525
5976
  prompt: orphan.prompt,
5526
5977
  promptByStyle: {},
@@ -5546,7 +5997,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
5546
5997
  error: null,
5547
5998
  sourceUrl: orphan.previewUrl,
5548
5999
  outputs: [{
5549
- path: portableOutputPath(outFile, import_node_path16.default.dirname(ctx.manifestPath)),
6000
+ path: portableOutputPath(outFile, import_node_path17.default.dirname(ctx.manifestPath)),
5550
6001
  sha256: sha256(buf)
5551
6002
  }],
5552
6003
  submittedAt: orphan.createdAt,
@@ -5570,9 +6021,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
5570
6021
  }
5571
6022
  }
5572
6023
  await applyTags(provider, decisions, existingTags, { onProgress: log });
5573
- const raw = JSON.parse(await (0, import_promises13.readFile)(ctx.manifestPath, "utf8"));
6024
+ const raw = JSON.parse(await (0, import_promises14.readFile)(ctx.manifestPath, "utf8"));
5574
6025
  for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
5575
- await (0, import_promises13.writeFile)(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
6026
+ await (0, import_promises14.writeFile)(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
5576
6027
  await saveLock(ctx.lockPath, ctx.lock);
5577
6028
  return result;
5578
6029
  }
@@ -5580,9 +6031,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
5580
6031
  }
5581
6032
 
5582
6033
  // src/workspace.ts
5583
- var import_promises14 = require("fs/promises");
6034
+ var import_promises15 = require("fs/promises");
5584
6035
  var import_node_fs14 = require("fs");
5585
- var import_node_path17 = __toESM(require("path"), 1);
6036
+ var import_node_path18 = __toESM(require("path"), 1);
5586
6037
  var import_zod4 = require("zod");
5587
6038
  var WorkspaceProjectSchema = import_zod4.z.object({
5588
6039
  id: import_zod4.z.string().min(1),
@@ -5610,7 +6061,7 @@ async function loadWorkspace(workspacePath) {
5610
6061
  if (!(0, import_node_fs14.existsSync)(workspacePath)) return { version: 1, projects: [] };
5611
6062
  let raw;
5612
6063
  try {
5613
- raw = JSON.parse(await (0, import_promises14.readFile)(workspacePath, "utf8"));
6064
+ raw = JSON.parse(await (0, import_promises15.readFile)(workspacePath, "utf8"));
5614
6065
  } catch (err) {
5615
6066
  throw new Error(
5616
6067
  `Workspace catalog at ${workspacePath} is malformed:
@@ -5624,22 +6075,22 @@ async function saveWorkspace(workspacePath, ws) {
5624
6075
  version: 1,
5625
6076
  projects: [...ws.projects].sort((a, b) => a.id.localeCompare(b.id))
5626
6077
  };
5627
- await (0, import_promises14.mkdir)(import_node_path17.default.dirname(import_node_path17.default.resolve(workspacePath)), { recursive: true });
6078
+ await (0, import_promises15.mkdir)(import_node_path18.default.dirname(import_node_path18.default.resolve(workspacePath)), { recursive: true });
5628
6079
  const tmp = `${workspacePath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
5629
6080
  try {
5630
- await (0, import_promises14.writeFile)(tmp, JSON.stringify(sorted, null, 2) + "\n");
5631
- await (0, import_promises14.rename)(tmp, workspacePath);
6081
+ await (0, import_promises15.writeFile)(tmp, JSON.stringify(sorted, null, 2) + "\n");
6082
+ await (0, import_promises15.rename)(tmp, workspacePath);
5632
6083
  } finally {
5633
- await (0, import_promises14.rm)(tmp, { force: true });
6084
+ await (0, import_promises15.rm)(tmp, { force: true });
5634
6085
  }
5635
6086
  }
5636
6087
  function toPortablePath(dir, absolute) {
5637
- return import_node_path17.default.relative(dir, absolute).split(import_node_path17.default.sep).join("/");
6088
+ return import_node_path18.default.relative(dir, absolute).split(import_node_path18.default.sep).join("/");
5638
6089
  }
5639
6090
  function resolveProject(dir, project) {
5640
6091
  return {
5641
- manifestPath: import_node_path17.default.resolve(dir, project.manifest.split("/").join(import_node_path17.default.sep)),
5642
- lockPath: import_node_path17.default.resolve(dir, project.lock.split("/").join(import_node_path17.default.sep))
6092
+ manifestPath: import_node_path18.default.resolve(dir, project.manifest.split("/").join(import_node_path18.default.sep)),
6093
+ lockPath: import_node_path18.default.resolve(dir, project.lock.split("/").join(import_node_path18.default.sep))
5643
6094
  };
5644
6095
  }
5645
6096
  function validateWorkspace(ws, dir) {
@@ -5652,7 +6103,7 @@ function validateWorkspace(ws, dir) {
5652
6103
  const { manifestPath, lockPath } = resolveProject(dir, project);
5653
6104
  lockOwners.set(lockPath, [...lockOwners.get(lockPath) ?? [], project.id]);
5654
6105
  manifestOwners.set(manifestPath, [...manifestOwners.get(manifestPath) ?? [], project.id]);
5655
- if (import_node_path17.default.isAbsolute(project.manifest) || import_node_path17.default.isAbsolute(project.lock)) {
6106
+ if (import_node_path18.default.isAbsolute(project.manifest) || import_node_path18.default.isAbsolute(project.lock)) {
5656
6107
  diagnostics.push({
5657
6108
  id: "absolute-path",
5658
6109
  level: "warning",
@@ -5808,6 +6259,7 @@ async function workspaceStatus(ws, dir) {
5808
6259
  // Annotate the CommonJS export names for ESM import in node:
5809
6260
  0 && (module.exports = {
5810
6261
  AssetSchema,
6262
+ ComfyUIProvider,
5811
6263
  DEFAULT_RATE_LIMIT,
5812
6264
  FAKE_PNG,
5813
6265
  FakeProvider,