pixelkiln 0.5.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/cli.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import path18 from "path";
4
+ import path19 from "path";
5
5
  import { existsSync as existsSync15 } from "fs";
6
- import { readFile as readFile14 } from "fs/promises";
6
+ import { readFile as readFile15 } from "fs/promises";
7
7
 
8
8
  // src/env.ts
9
9
  import { readFileSync, existsSync } from "fs";
@@ -103,532 +103,225 @@ function formatCost(unit, amount) {
103
103
  return `${amount} ${unit}`;
104
104
  }
105
105
 
106
- // src/providers/pixellab.ts
107
- import { mkdirSync, existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
106
+ // src/providers/comfyui.ts
108
107
  import { randomUUID } from "crypto";
109
- import os from "os";
108
+ import { readFile as readFile2 } from "fs/promises";
110
109
  import path2 from "path";
111
110
 
112
- // src/client.ts
113
- import { z } from "zod";
114
- var BASE = process.env.PIXELLAB_API_BASE ?? "https://api.pixellab.ai/v2";
115
- var MAX_RETRIES = 4;
116
- var MAX_DOWNLOAD_BYTES = 64 * 1024 * 1024;
117
- var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
118
- function shouldRetry(status) {
119
- return status === 429 || status === 408 || status >= 500;
120
- }
121
- function backoffMs(attempt) {
122
- const base = Math.min(1e3 * 2 ** attempt, 16e3);
123
- return base + Math.floor(Math.random() * 400);
111
+ // src/hash.ts
112
+ import { createHash } from "crypto";
113
+ import { readFile } from "fs/promises";
114
+ function sha256(data) {
115
+ return createHash("sha256").update(data).digest("hex");
124
116
  }
125
- function retryAfterMs(value, now = Date.now()) {
126
- if (!value) return null;
127
- const seconds = Number(value);
128
- if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1e3;
129
- const date = Date.parse(value);
130
- if (!Number.isFinite(date)) return null;
131
- return Math.max(0, date - now);
117
+ async function sha256File(path20) {
118
+ return sha256(await readFile(path20));
132
119
  }
133
- var BalanceResponseSchema = z.object({
134
- credits: z.object({ usd: z.number() }).passthrough(),
135
- subscription: z.object({
136
- generations: z.number(),
137
- total: z.number(),
138
- plan: z.string().nullable().optional()
139
- }).passthrough()
140
- }).passthrough();
141
- var ObjectSubmitSchema = z.object({
142
- background_job_id: z.string().min(1),
143
- object_id: z.string().min(1),
144
- status: z.string().default("queued"),
145
- n_frames: z.number().int().min(0)
146
- }).passthrough();
147
- var MapSubmitSchema = z.object({
148
- background_job_id: z.string().min(1),
149
- object_id: z.string().min(1),
150
- status: z.string().default("processing")
151
- }).passthrough();
152
- var TilesSubmitSchema = z.object({
153
- tile_id: z.string().min(1),
154
- background_job_id: z.string().min(1),
155
- status: z.literal("processing").default("processing")
156
- }).passthrough();
157
- var TilesProSchema = z.object({
158
- storage_urls: z.record(z.string()),
159
- kind: z.string().nullable().default(null),
160
- tile_rules: z.record(z.unknown()).nullable().optional()
161
- }).passthrough();
162
- var PixelLabObjectSchema = z.object({
163
- id: z.string().min(1),
164
- name: z.string().nullable().default(null),
165
- prompt: z.string().default(""),
166
- size: z.object({ width: z.number().int().positive(), height: z.number().int().positive() }),
167
- directions: z.number().default(0),
168
- created_at: z.string(),
169
- view: z.string().nullable().default(null),
170
- preview_url: z.string().nullable().optional(),
171
- rotation_urls: z.record(z.string().nullable()).nullable().optional(),
172
- frame_urls: z.array(z.string()).nullable().optional(),
173
- tags: z.array(z.string()).default([]),
174
- status: z.string().nullable().default(null),
175
- progress_percent: z.number().nullable().optional(),
176
- eta_seconds: z.number().nullable().optional()
177
- }).passthrough();
178
- var MapObjectSchema = z.object({
179
- object_id: z.string().min(1),
180
- status: z.string(),
181
- description: z.string().nullable().default(null),
182
- width: z.number().nullable().default(null),
183
- height: z.number().nullable().default(null),
184
- download_url: z.string().nullable().default(null)
185
- }).passthrough();
186
- var ObjectListSchema = z.object({ objects: z.array(PixelLabObjectSchema), total: z.number().int().min(0) }).passthrough();
187
- var PixfluxResponseSchema = z.object({ image: z.object({ base64: z.string().min(1) }).passthrough(), usage: z.unknown().optional() }).passthrough();
188
- var SelectFramesSchema = z.object({ created_object_ids: z.array(z.string()) }).passthrough();
189
- function validateResponse(schema, raw, operation) {
190
- const parsed = schema.safeParse(raw);
191
- if (parsed.success) return parsed.data;
192
- const issues = parsed.error.issues.slice(0, 4).map((i) => `${i.path.join(".") || "response"}: ${i.message}`).join("; ");
193
- throw new Error(`Invalid PixelLab response for ${operation}: ${issues}`);
120
+ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.providerOptions) {
121
+ return sha256(
122
+ JSON.stringify({
123
+ // Preserve every existing PixelLab hash while making a provider switch
124
+ // invalidate the spec. Older manifests implicitly mean pixellab.
125
+ provider: spec.provider === "pixellab" ? void 0 : spec.provider,
126
+ providerOptions: providerOptionIdentity && (typeof providerOptionIdentity !== "object" || Object.keys(providerOptionIdentity).length > 0) ? providerOptionIdentity : void 0,
127
+ generator: spec.generator,
128
+ prompt: spec.prompt,
129
+ width: spec.width,
130
+ height: spec.height,
131
+ view: spec.view,
132
+ outline: spec.outline ?? null,
133
+ shading: spec.shading ?? null,
134
+ detail: spec.detail ?? null,
135
+ seed: spec.seed ?? null,
136
+ palette: spec.palette,
137
+ // `noBackground` only reaches the wire for pixflux; the tile fields are
138
+ // undefined for every other generator. `tileSize` is intentionally
139
+ // absent — width/height are derived from it, so it is already covered.
140
+ noBackground: spec.generator === "pixflux" || spec.provider !== "pixellab" ? spec.noBackground : void 0,
141
+ tileType: spec.tileType,
142
+ tileView: spec.tileView,
143
+ tileFeature: spec.tileFeature,
144
+ outlineMode: spec.outlineMode,
145
+ styleImages: styleImageHashes
146
+ })
147
+ );
194
148
  }
195
- var PixelLabError = class extends Error {
196
- constructor(message2, status, body) {
197
- super(message2);
198
- this.status = status;
199
- this.body = body;
200
- this.name = "PixelLabError";
201
- }
202
- status;
203
- body;
204
- };
205
- var PixelLabClient = class {
206
- constructor(apiKey, timeoutMs = 12e4) {
207
- this.apiKey = apiKey;
208
- this.timeoutMs = timeoutMs;
209
- if (!apiKey) throw new Error("PIXELLAB_API_KEY is required");
149
+
150
+ // src/png.ts
151
+ import { deflateSync, inflateSync } from "zlib";
152
+ var SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
153
+ var MAX_DECODED_BYTES = 256 * 1024 * 1024;
154
+ var CHANNELS = /* @__PURE__ */ new Map([
155
+ [0, 1],
156
+ // greyscale
157
+ [2, 3],
158
+ // RGB
159
+ [3, 1],
160
+ // indexed colour
161
+ [4, 2],
162
+ // greyscale + alpha
163
+ [6, 4]
164
+ // RGBA
165
+ ]);
166
+ var LEGAL_DEPTHS = /* @__PURE__ */ new Map([
167
+ [0, /* @__PURE__ */ new Set([1, 2, 4, 8, 16])],
168
+ [2, /* @__PURE__ */ new Set([8, 16])],
169
+ [3, /* @__PURE__ */ new Set([1, 2, 4, 8])],
170
+ [4, /* @__PURE__ */ new Set([8, 16])],
171
+ [6, /* @__PURE__ */ new Set([8, 16])]
172
+ ]);
173
+ function decodePng(buf) {
174
+ if (buf.length < 8 || !buf.subarray(0, 8).equals(SIGNATURE)) {
175
+ throw new Error("not a PNG");
210
176
  }
211
- apiKey;
212
- timeoutMs;
213
- /**
214
- * Retries only what is safe to retry: transport failures, 429, and 5xx.
215
- * A 4xx other than 429 is a bad request and retrying it just wastes time.
216
- *
217
- * POSTs that create objects are included, which is a deliberate trade: the
218
- * failure mode of not retrying (a dropped asset in a 65-item run) is more
219
- * common than the failure mode of retrying (a duplicate object), and a
220
- * duplicate is visible and free to delete whereas a silent gap is neither.
221
- */
222
- async request(path19, init, attempt = 0) {
223
- const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
224
- let res;
225
- try {
226
- res = await fetch(`${BASE}${path19}`, {
227
- ...init,
228
- signal: init?.signal ?? AbortSignal.timeout(this.timeoutMs),
229
- headers: {
230
- Authorization: auth,
231
- "Content-Type": "application/json",
232
- ...init?.headers ?? {}
233
- }
234
- });
235
- } catch (err) {
236
- if (attempt < MAX_RETRIES) {
237
- await sleep(backoffMs(attempt));
238
- return this.request(path19, init, attempt + 1);
239
- }
240
- throw err;
241
- }
242
- if (!res.ok && shouldRetry(res.status) && attempt < MAX_RETRIES) {
243
- const waitMs = retryAfterMs(res.headers.get("retry-after")) ?? backoffMs(attempt);
244
- await sleep(waitMs);
245
- return this.request(path19, init, attempt + 1);
177
+ let header = null;
178
+ let palette = null;
179
+ let transparency = null;
180
+ let sawImageData = false;
181
+ let sawEnd = false;
182
+ const idat = [];
183
+ let offset = 8;
184
+ while (offset < buf.length) {
185
+ if (offset + 12 > buf.length) throw new Error("truncated PNG chunk header");
186
+ const length = buf.readUInt32BE(offset);
187
+ const type = buf.toString("ascii", offset + 4, offset + 8);
188
+ if (!/^[A-Za-z]{4}$/.test(type) || /[a-z]/.test(type[2])) {
189
+ throw new Error(`PNG has an invalid chunk type ${JSON.stringify(type)}`);
246
190
  }
247
- const text = await res.text();
248
- if (!res.ok) {
249
- throw new PixelLabError(`${init?.method ?? "GET"} ${path19} \u2192 ${res.status}`, res.status, text);
191
+ const end = offset + 12 + length;
192
+ if (!Number.isSafeInteger(end) || end > buf.length) {
193
+ throw new Error(`truncated PNG chunk ${type || "(unknown)"}`);
250
194
  }
251
- if (!text) return {};
252
- try {
253
- return JSON.parse(text);
254
- } catch {
255
- throw new Error(`${init?.method ?? "GET"} ${path19} returned invalid JSON`);
195
+ const data = buf.subarray(offset + 8, offset + 8 + length);
196
+ const expectedCrc = buf.readUInt32BE(offset + 8 + length);
197
+ const actualCrc = crc32(buf.subarray(offset + 4, offset + 8 + length)) >>> 0;
198
+ if (actualCrc !== expectedCrc) throw new Error(`PNG chunk ${type} has an invalid checksum`);
199
+ if (!header && type !== "IHDR") throw new Error("PNG is missing its leading IHDR chunk");
200
+ if (type === "IHDR") {
201
+ if (header) throw new Error("PNG has more than one IHDR chunk");
202
+ if (data.length !== 13) throw new Error("PNG has an invalid IHDR chunk");
203
+ const width = data.readUInt32BE(0);
204
+ const height = data.readUInt32BE(4);
205
+ const bitDepth = data[8];
206
+ const colorType = data[9];
207
+ const channels = CHANNELS.get(colorType);
208
+ if (!width || !height) throw new Error("PNG dimensions must be positive");
209
+ if (!channels || !LEGAL_DEPTHS.get(colorType)?.has(bitDepth)) {
210
+ throw new Error(`unsupported PNG: bitDepth=${bitDepth} colorType=${colorType}`);
211
+ }
212
+ if (data[10] !== 0) throw new Error(`unsupported PNG compression method ${data[10]}`);
213
+ if (data[11] !== 0) throw new Error(`unsupported PNG filter method ${data[11]}`);
214
+ if (data[12] !== 0) throw new Error("unsupported PNG: interlaced");
215
+ header = { width, height, bitDepth, colorType, channels };
216
+ } else if (type === "PLTE") {
217
+ if (sawImageData) throw new Error("PNG palette appears after image data");
218
+ if (palette) throw new Error("PNG has more than one palette");
219
+ if (!data.length || data.length % 3 !== 0 || data.length > 256 * 3) {
220
+ throw new Error("PNG has an invalid palette");
221
+ }
222
+ palette = Buffer.from(data);
223
+ } else if (type === "tRNS") {
224
+ if (sawImageData) throw new Error("PNG transparency appears after image data");
225
+ if (transparency) throw new Error("PNG has more than one transparency chunk");
226
+ transparency = Buffer.from(data);
227
+ } else if (type === "IDAT") {
228
+ sawImageData = true;
229
+ idat.push(data);
230
+ } else if (type === "IEND") {
231
+ if (data.length !== 0) throw new Error("PNG has an invalid IEND chunk");
232
+ sawEnd = true;
233
+ break;
234
+ } else if (/^[A-Z]/.test(type)) {
235
+ throw new Error(`unsupported critical PNG chunk ${type}`);
256
236
  }
237
+ offset = end;
257
238
  }
258
- async balance() {
259
- const raw = validateResponse(BalanceResponseSchema, await this.request("/balance"), "balance");
260
- return {
261
- usd: raw.credits?.usd ?? 0,
262
- generations: raw.subscription?.generations ?? 0,
263
- total: raw.subscription?.total ?? 0,
264
- plan: raw.subscription?.plan ?? "unknown"
265
- };
266
- }
267
- /**
268
- * Square objects that persist indefinitely.
269
- *
270
- * `size` and `styleImages` are mutually exclusive at the API level: when style
271
- * images are supplied the largest one dictates the output size. So style
272
- * references must already be at the target resolution — a 128px reference
273
- * silently produces 128px output and a different candidate count.
274
- */
275
- async create1Direction(args) {
276
- const body = { description: args.description };
277
- if (args.styleImages?.length) {
278
- body.style_images = args.styleImages.map(({ base64, format }) => ({
279
- type: "base64",
280
- base64,
281
- format
282
- }));
283
- } else if (args.size != null) {
284
- body.size = args.size;
285
- }
286
- if (args.view) body.view = args.view;
287
- if (args.itemDescriptions?.length) body.item_descriptions = args.itemDescriptions;
288
- const raw = await this.request("/create-1-direction-object", {
289
- method: "POST",
290
- body: JSON.stringify(body)
291
- });
292
- const parsed = validateResponse(ObjectSubmitSchema, raw, "create-1-direction-object");
293
- return parsed;
294
- }
295
- /**
296
- * Arbitrary width x height. Returns a single result — no selection step.
297
- *
298
- * These AUTO-DELETE AFTER 8 HOURS, so `fetch` must run in the same session as
299
- * `submit`. The pipeline warns when a map-object entry is older than that.
300
- */
301
- async createMapObject(args) {
302
- const body = {
303
- description: args.description,
304
- image_size: { width: args.width, height: args.height }
305
- };
306
- if (args.view) body.view = args.view;
307
- if (args.outline) body.outline = args.outline;
308
- if (args.shading) body.shading = args.shading;
309
- if (args.detail) body.detail = args.detail;
310
- if (args.seed != null) body.seed = args.seed;
311
- return validateResponse(
312
- MapSubmitSchema,
313
- await this.request("/map-objects", { method: "POST", body: JSON.stringify(body) }),
314
- "create map object"
315
- );
316
- }
317
- /**
318
- * Draws a whole tile set in one call — many variations, or a connectable
319
- * set when `tileFeature` is given.
320
- *
321
- * `styleImages` here is NOT the shape `create-1-direction-object` uses.
322
- * TilesProStyleImage is flat — `{base64, width, height}`, all three
323
- * required — where 1dir wants `{type, base64, format}`. Confirmed against
324
- * the OpenAPI schema; sending 1dir's shape is rejected as an extra field.
325
- *
326
- * Passing style images also makes the API ignore `tileType` and `tileView`
327
- * and copy the reference's tile geometry instead.
328
- */
329
- async createTilesPro(args) {
330
- const body = { description: args.description };
331
- if (args.tileSize != null) body.tile_size = args.tileSize;
332
- if (args.tileType) body.tile_type = args.tileType;
333
- if (args.tileView) body.tile_view = args.tileView;
334
- if (args.tileFeature) body.tile_feature = args.tileFeature;
335
- if (args.outlineMode) body.outline_mode = args.outlineMode;
336
- if (args.seed != null) body.seed = args.seed;
337
- if (args.styleImages?.length) body.style_images = args.styleImages;
338
- return validateResponse(
339
- TilesSubmitSchema,
340
- await this.request("/create-tiles-pro", { method: "POST", body: JSON.stringify(body) }),
341
- "create tiles"
342
- );
343
- }
344
- /** Throws PixelLabError(423) while the set is still drawing — see TilesPro. */
345
- async getTilesPro(tileId) {
346
- return validateResponse(
347
- TilesProSchema,
348
- await this.request(`/tiles-pro/${tileId}`),
349
- "get tiles"
350
- );
239
+ if (!header) throw new Error("PNG has no image header");
240
+ if (!sawEnd) throw new Error("PNG has no IEND chunk");
241
+ if (!idat.length) throw new Error("PNG has no image data");
242
+ validatePalette(header, palette);
243
+ validateTransparency(header.colorType, transparency, palette);
244
+ const bitsPerPixel = header.channels * header.bitDepth;
245
+ const stride = Math.ceil(header.width * bitsPerPixel / 8);
246
+ const rawLength = (stride + 1) * header.height;
247
+ const rgbaLength = header.width * header.height * 4;
248
+ if (!Number.isSafeInteger(rawLength) || !Number.isSafeInteger(rgbaLength) || rawLength > MAX_DECODED_BYTES || rgbaLength > MAX_DECODED_BYTES) {
249
+ throw new Error("PNG dimensions exceed the 256 MiB decoded-image limit");
351
250
  }
352
- /**
353
- * Synchronous single-image generation. Returns the PNG inline rather than a
354
- * job id, and is the only endpoint that honours a forced palette —
355
- * `color_image` on /map-objects returns a 500 whatever the payload shape.
356
- */
357
- async createImagePixflux(args) {
358
- const body = {
359
- description: args.description,
360
- image_size: { width: args.width, height: args.height },
361
- no_background: args.noBackground ?? true
362
- };
363
- if (args.paletteSwatchBase64) {
364
- body.color_image = { type: "base64", base64: args.paletteSwatchBase64, format: "png" };
365
- }
366
- if (args.seed != null) body.seed = args.seed;
367
- const res = validateResponse(
368
- PixfluxResponseSchema,
369
- await this.request("/create-image-pixflux", {
370
- method: "POST",
371
- body: JSON.stringify(body)
372
- }),
373
- "create pixflux image"
374
- );
375
- const b64 = res.image.base64;
376
- return { png: Buffer.from(b64, "base64"), usage: res.usage };
251
+ let raw;
252
+ try {
253
+ raw = inflateSync(Buffer.concat(idat), { maxOutputLength: rawLength });
254
+ } catch (err) {
255
+ throw new Error(`invalid PNG image data: ${err instanceof Error ? err.message : String(err)}`);
377
256
  }
378
- async getObject(objectId) {
379
- return validateResponse(
380
- PixelLabObjectSchema,
381
- await this.request(`/objects/${objectId}`),
382
- "get object"
383
- );
257
+ if (raw.length !== rawLength) {
258
+ throw new Error(`invalid PNG image data length: expected ${rawLength}, got ${raw.length}`);
384
259
  }
385
- async getMapObject(objectId) {
386
- return validateResponse(
387
- MapObjectSchema,
388
- await this.request(`/map-objects/${objectId}`),
389
- "get map object"
390
- );
260
+ const filterBytes = Math.max(1, Math.ceil(bitsPerPixel / 8));
261
+ const samples = unfilter(raw, stride, header.height, filterBytes);
262
+ return {
263
+ width: header.width,
264
+ height: header.height,
265
+ pixels: toRgba(samples, header, stride, palette, transparency)
266
+ };
267
+ }
268
+ function validatePalette(header, palette) {
269
+ if (header.colorType === 3 && !palette) throw new Error("indexed PNG has no palette");
270
+ if ((header.colorType === 0 || header.colorType === 4) && palette) {
271
+ throw new Error("greyscale PNG cannot contain a palette");
391
272
  }
392
- async listObjects(limit = 50, offset = 0) {
393
- return validateResponse(
394
- ObjectListSchema,
395
- await this.request(`/objects?limit=${limit}&offset=${offset}`),
396
- "list objects"
397
- );
273
+ if (header.colorType === 3 && palette && palette.length / 3 > 2 ** header.bitDepth) {
274
+ throw new Error("indexed PNG palette has more entries than its bit depth permits");
398
275
  }
399
- /** Walks the whole account. Used by `adopt` to reconcile orphaned objects. */
400
- async *iterateObjects(pageSize = 100) {
401
- let offset = 0;
402
- for (; ; ) {
403
- const page = await this.listObjects(pageSize, offset);
404
- for (const obj of page.objects) yield obj;
405
- offset += page.objects.length;
406
- if (page.objects.length === 0 || offset >= page.total) return;
276
+ }
277
+ function unfilter(raw, stride, height, bpp) {
278
+ const out = Buffer.alloc(stride * height);
279
+ for (let y = 0; y < height; y++) {
280
+ const filter = raw[y * (stride + 1)];
281
+ const src = raw.subarray(y * (stride + 1) + 1, y * (stride + 1) + 1 + stride);
282
+ const dst = out.subarray(y * stride, (y + 1) * stride);
283
+ const prev = y > 0 ? out.subarray((y - 1) * stride, y * stride) : null;
284
+ for (let x = 0; x < stride; x++) {
285
+ const a = x >= bpp ? dst[x - bpp] : 0;
286
+ const b = prev ? prev[x] : 0;
287
+ const c = prev && x >= bpp ? prev[x - bpp] : 0;
288
+ const v = src[x];
289
+ switch (filter) {
290
+ case 0:
291
+ dst[x] = v;
292
+ break;
293
+ case 1:
294
+ dst[x] = v + a & 255;
295
+ break;
296
+ case 2:
297
+ dst[x] = v + b & 255;
298
+ break;
299
+ case 3:
300
+ dst[x] = v + (a + b >> 1) & 255;
301
+ break;
302
+ case 4:
303
+ dst[x] = v + paeth(a, b, c) & 255;
304
+ break;
305
+ default:
306
+ throw new Error(`unknown PNG filter ${filter} on row ${y}`);
307
+ }
407
308
  }
408
309
  }
409
- /**
410
- * Promotes chosen candidates to standalone objects, each with its own id.
411
- * The review parent survives until nothing is left in it, so the returned
412
- * `created_object_ids` — not the parent id — is what should be recorded.
413
- */
414
- async selectFrames(objectId, indices, commonTag) {
415
- const raw = await this.request(`/objects/${objectId}/select-frames`, {
416
- method: "POST",
417
- body: JSON.stringify(commonTag ? { indices, common_tag: commonTag } : { indices })
418
- });
419
- return validateResponse(SelectFramesSchema, raw, "select frames");
420
- }
421
- /** Irreversible. Only reached via `purge`, behind an explicit confirmation. */
422
- deleteObject(objectId) {
423
- return this.request(`/objects/${objectId}`, { method: "DELETE" });
310
+ return out;
311
+ }
312
+ function validateTransparency(colorType, transparency, palette) {
313
+ if (!transparency) return;
314
+ if (colorType === 0 && transparency.length !== 2) {
315
+ throw new Error("greyscale PNG has an invalid transparency chunk");
424
316
  }
425
- dismissReview(objectId) {
426
- return this.request(`/objects/${objectId}/dismiss-review`, { method: "POST" });
317
+ if (colorType === 2 && transparency.length !== 6) {
318
+ throw new Error("RGB PNG has an invalid transparency chunk");
427
319
  }
428
- /** Free and synchronous. Replaces the full tag set — include tags you want to keep. */
429
- setTags(objectId, tags) {
430
- return this.request(`/objects/${objectId}/tags`, { method: "PATCH", body: JSON.stringify({ tags }) });
320
+ if (colorType === 3 && transparency.length > (palette?.length ?? 0) / 3) {
321
+ throw new Error("indexed PNG transparency exceeds its palette");
431
322
  }
432
- /** Storage URLs are public; no auth header, and sending one can break the CDN request. */
433
- async download(url) {
434
- const res = await fetch(url, { signal: AbortSignal.timeout(this.timeoutMs) });
435
- if (!res.ok) throw new PixelLabError(`download ${url} \u2192 ${res.status}`, res.status, "");
436
- const declared = Number(res.headers.get("content-length"));
437
- if (Number.isFinite(declared) && declared > MAX_DOWNLOAD_BYTES) {
438
- throw new Error(`download ${url} exceeds the ${MAX_DOWNLOAD_BYTES}-byte safety limit`);
439
- }
440
- const buf = Buffer.from(await res.arrayBuffer());
441
- if (buf.length > MAX_DOWNLOAD_BYTES) {
442
- throw new Error(`download ${url} exceeds the ${MAX_DOWNLOAD_BYTES}-byte safety limit`);
443
- }
444
- return buf;
445
- }
446
- };
447
- function clientFromEnv() {
448
- const key = process.env.PIXELLAB_API_KEY;
449
- if (!key) {
450
- throw new Error(
451
- "PIXELLAB_API_KEY is not set.\nLooked in the environment, and in .env.local / .env beside the manifest and in the current directory."
452
- );
453
- }
454
- return new PixelLabClient(key);
455
- }
456
-
457
- // src/png.ts
458
- import { deflateSync, inflateSync } from "zlib";
459
- var SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
460
- var MAX_DECODED_BYTES = 256 * 1024 * 1024;
461
- var CHANNELS = /* @__PURE__ */ new Map([
462
- [0, 1],
463
- // greyscale
464
- [2, 3],
465
- // RGB
466
- [3, 1],
467
- // indexed colour
468
- [4, 2],
469
- // greyscale + alpha
470
- [6, 4]
471
- // RGBA
472
- ]);
473
- var LEGAL_DEPTHS = /* @__PURE__ */ new Map([
474
- [0, /* @__PURE__ */ new Set([1, 2, 4, 8, 16])],
475
- [2, /* @__PURE__ */ new Set([8, 16])],
476
- [3, /* @__PURE__ */ new Set([1, 2, 4, 8])],
477
- [4, /* @__PURE__ */ new Set([8, 16])],
478
- [6, /* @__PURE__ */ new Set([8, 16])]
479
- ]);
480
- function decodePng(buf) {
481
- if (buf.length < 8 || !buf.subarray(0, 8).equals(SIGNATURE)) {
482
- throw new Error("not a PNG");
483
- }
484
- let header = null;
485
- let palette = null;
486
- let transparency = null;
487
- let sawImageData = false;
488
- let sawEnd = false;
489
- const idat = [];
490
- let offset = 8;
491
- while (offset < buf.length) {
492
- if (offset + 12 > buf.length) throw new Error("truncated PNG chunk header");
493
- const length = buf.readUInt32BE(offset);
494
- const type = buf.toString("ascii", offset + 4, offset + 8);
495
- if (!/^[A-Za-z]{4}$/.test(type) || /[a-z]/.test(type[2])) {
496
- throw new Error(`PNG has an invalid chunk type ${JSON.stringify(type)}`);
497
- }
498
- const end = offset + 12 + length;
499
- if (!Number.isSafeInteger(end) || end > buf.length) {
500
- throw new Error(`truncated PNG chunk ${type || "(unknown)"}`);
501
- }
502
- const data = buf.subarray(offset + 8, offset + 8 + length);
503
- const expectedCrc = buf.readUInt32BE(offset + 8 + length);
504
- const actualCrc = crc32(buf.subarray(offset + 4, offset + 8 + length)) >>> 0;
505
- if (actualCrc !== expectedCrc) throw new Error(`PNG chunk ${type} has an invalid checksum`);
506
- if (!header && type !== "IHDR") throw new Error("PNG is missing its leading IHDR chunk");
507
- if (type === "IHDR") {
508
- if (header) throw new Error("PNG has more than one IHDR chunk");
509
- if (data.length !== 13) throw new Error("PNG has an invalid IHDR chunk");
510
- const width = data.readUInt32BE(0);
511
- const height = data.readUInt32BE(4);
512
- const bitDepth = data[8];
513
- const colorType = data[9];
514
- const channels = CHANNELS.get(colorType);
515
- if (!width || !height) throw new Error("PNG dimensions must be positive");
516
- if (!channels || !LEGAL_DEPTHS.get(colorType)?.has(bitDepth)) {
517
- throw new Error(`unsupported PNG: bitDepth=${bitDepth} colorType=${colorType}`);
518
- }
519
- if (data[10] !== 0) throw new Error(`unsupported PNG compression method ${data[10]}`);
520
- if (data[11] !== 0) throw new Error(`unsupported PNG filter method ${data[11]}`);
521
- if (data[12] !== 0) throw new Error("unsupported PNG: interlaced");
522
- header = { width, height, bitDepth, colorType, channels };
523
- } else if (type === "PLTE") {
524
- if (sawImageData) throw new Error("PNG palette appears after image data");
525
- if (palette) throw new Error("PNG has more than one palette");
526
- if (!data.length || data.length % 3 !== 0 || data.length > 256 * 3) {
527
- throw new Error("PNG has an invalid palette");
528
- }
529
- palette = Buffer.from(data);
530
- } else if (type === "tRNS") {
531
- if (sawImageData) throw new Error("PNG transparency appears after image data");
532
- if (transparency) throw new Error("PNG has more than one transparency chunk");
533
- transparency = Buffer.from(data);
534
- } else if (type === "IDAT") {
535
- sawImageData = true;
536
- idat.push(data);
537
- } else if (type === "IEND") {
538
- if (data.length !== 0) throw new Error("PNG has an invalid IEND chunk");
539
- sawEnd = true;
540
- break;
541
- } else if (/^[A-Z]/.test(type)) {
542
- throw new Error(`unsupported critical PNG chunk ${type}`);
543
- }
544
- offset = end;
545
- }
546
- if (!header) throw new Error("PNG has no image header");
547
- if (!sawEnd) throw new Error("PNG has no IEND chunk");
548
- if (!idat.length) throw new Error("PNG has no image data");
549
- validatePalette(header, palette);
550
- validateTransparency(header.colorType, transparency, palette);
551
- const bitsPerPixel = header.channels * header.bitDepth;
552
- const stride = Math.ceil(header.width * bitsPerPixel / 8);
553
- const rawLength = (stride + 1) * header.height;
554
- const rgbaLength = header.width * header.height * 4;
555
- if (!Number.isSafeInteger(rawLength) || !Number.isSafeInteger(rgbaLength) || rawLength > MAX_DECODED_BYTES || rgbaLength > MAX_DECODED_BYTES) {
556
- throw new Error("PNG dimensions exceed the 256 MiB decoded-image limit");
557
- }
558
- let raw;
559
- try {
560
- raw = inflateSync(Buffer.concat(idat), { maxOutputLength: rawLength });
561
- } catch (err) {
562
- throw new Error(`invalid PNG image data: ${err instanceof Error ? err.message : String(err)}`);
563
- }
564
- if (raw.length !== rawLength) {
565
- throw new Error(`invalid PNG image data length: expected ${rawLength}, got ${raw.length}`);
566
- }
567
- const filterBytes = Math.max(1, Math.ceil(bitsPerPixel / 8));
568
- const samples = unfilter(raw, stride, header.height, filterBytes);
569
- return {
570
- width: header.width,
571
- height: header.height,
572
- pixels: toRgba(samples, header, stride, palette, transparency)
573
- };
574
- }
575
- function validatePalette(header, palette) {
576
- if (header.colorType === 3 && !palette) throw new Error("indexed PNG has no palette");
577
- if ((header.colorType === 0 || header.colorType === 4) && palette) {
578
- throw new Error("greyscale PNG cannot contain a palette");
579
- }
580
- if (header.colorType === 3 && palette && palette.length / 3 > 2 ** header.bitDepth) {
581
- throw new Error("indexed PNG palette has more entries than its bit depth permits");
582
- }
583
- }
584
- function unfilter(raw, stride, height, bpp) {
585
- const out = Buffer.alloc(stride * height);
586
- for (let y = 0; y < height; y++) {
587
- const filter = raw[y * (stride + 1)];
588
- const src = raw.subarray(y * (stride + 1) + 1, y * (stride + 1) + 1 + stride);
589
- const dst = out.subarray(y * stride, (y + 1) * stride);
590
- const prev = y > 0 ? out.subarray((y - 1) * stride, y * stride) : null;
591
- for (let x = 0; x < stride; x++) {
592
- const a = x >= bpp ? dst[x - bpp] : 0;
593
- const b = prev ? prev[x] : 0;
594
- const c = prev && x >= bpp ? prev[x - bpp] : 0;
595
- const v = src[x];
596
- switch (filter) {
597
- case 0:
598
- dst[x] = v;
599
- break;
600
- case 1:
601
- dst[x] = v + a & 255;
602
- break;
603
- case 2:
604
- dst[x] = v + b & 255;
605
- break;
606
- case 3:
607
- dst[x] = v + (a + b >> 1) & 255;
608
- break;
609
- case 4:
610
- dst[x] = v + paeth(a, b, c) & 255;
611
- break;
612
- default:
613
- throw new Error(`unknown PNG filter ${filter} on row ${y}`);
614
- }
615
- }
616
- }
617
- return out;
618
- }
619
- function validateTransparency(colorType, transparency, palette) {
620
- if (!transparency) return;
621
- if (colorType === 0 && transparency.length !== 2) {
622
- throw new Error("greyscale PNG has an invalid transparency chunk");
623
- }
624
- if (colorType === 2 && transparency.length !== 6) {
625
- throw new Error("RGB PNG has an invalid transparency chunk");
626
- }
627
- if (colorType === 3 && transparency.length > (palette?.length ?? 0) / 3) {
628
- throw new Error("indexed PNG transparency exceeds its palette");
629
- }
630
- if (colorType === 4 || colorType === 6) {
631
- throw new Error("PNG with an alpha channel cannot also contain tRNS");
323
+ if (colorType === 4 || colorType === 6) {
324
+ throw new Error("PNG with an alpha channel cannot also contain tRNS");
632
325
  }
633
326
  }
634
327
  function toRgba(samples, header, stride, palette, transparency) {
@@ -748,67 +441,921 @@ function encodeRgbPng(width, height, rgb) {
748
441
  if (rgb.length !== width * height * 3) {
749
442
  throw new Error(`expected ${width * height * 3} bytes of RGB, got ${rgb.length}`);
750
443
  }
751
- const stride = width * 3;
752
- const raw = Buffer.alloc((stride + 1) * height);
753
- for (let y = 0; y < height; y++) {
754
- raw[y * (stride + 1)] = 0;
755
- rgb.copy(raw, y * (stride + 1) + 1, y * stride, (y + 1) * stride);
444
+ const stride = width * 3;
445
+ const raw = Buffer.alloc((stride + 1) * height);
446
+ for (let y = 0; y < height; y++) {
447
+ raw[y * (stride + 1)] = 0;
448
+ rgb.copy(raw, y * (stride + 1) + 1, y * stride, (y + 1) * stride);
449
+ }
450
+ const ihdr = Buffer.alloc(13);
451
+ ihdr.writeUInt32BE(width, 0);
452
+ ihdr.writeUInt32BE(height, 4);
453
+ ihdr[8] = 8;
454
+ ihdr[9] = 2;
455
+ return Buffer.concat([
456
+ Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]),
457
+ chunk("IHDR", ihdr),
458
+ chunk("IDAT", deflateSync(raw)),
459
+ chunk("IEND", Buffer.alloc(0))
460
+ ]);
461
+ }
462
+ function encodeRgbaPng(width, height, rgba) {
463
+ if (rgba.length !== width * height * 4) {
464
+ throw new Error(`expected ${width * height * 4} bytes of RGBA, got ${rgba.length}`);
465
+ }
466
+ const stride = width * 4;
467
+ const raw = Buffer.alloc((stride + 1) * height);
468
+ for (let y = 0; y < height; y++) {
469
+ raw[y * (stride + 1)] = 0;
470
+ rgba.copy(raw, y * (stride + 1) + 1, y * stride, (y + 1) * stride);
471
+ }
472
+ const ihdr = Buffer.alloc(13);
473
+ ihdr.writeUInt32BE(width, 0);
474
+ ihdr.writeUInt32BE(height, 4);
475
+ ihdr[8] = 8;
476
+ ihdr[9] = 6;
477
+ return Buffer.concat([
478
+ Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]),
479
+ chunk("IHDR", ihdr),
480
+ chunk("IDAT", deflateSync(raw)),
481
+ chunk("IEND", Buffer.alloc(0))
482
+ ]);
483
+ }
484
+ function paletteSwatch(hexes, size = 64) {
485
+ const colours = hexes.map(parseHex);
486
+ if (!colours.length) throw new Error("palette is empty");
487
+ const rgb = Buffer.alloc(size * size * 3);
488
+ const band = Math.max(1, Math.floor(size / colours.length));
489
+ for (let y = 0; y < size; y++) {
490
+ for (let x = 0; x < size; x++) {
491
+ const c = colours[Math.min(colours.length - 1, Math.floor(x / band))];
492
+ const o = (y * size + x) * 3;
493
+ rgb[o] = c.r;
494
+ rgb[o + 1] = c.g;
495
+ rgb[o + 2] = c.b;
496
+ }
497
+ }
498
+ return encodeRgbPng(size, size, rgb);
499
+ }
500
+ function parseHex(hex2) {
501
+ const m = /^#?([0-9a-f]{6})$/i.exec(hex2.trim());
502
+ if (!m) throw new Error(`invalid hex colour "${hex2}" \u2014 expected #rrggbb`);
503
+ const n = parseInt(m[1], 16);
504
+ return { r: n >> 16 & 255, g: n >> 8 & 255, b: n & 255 };
505
+ }
506
+
507
+ // src/media.ts
508
+ var MediaType = {
509
+ PNG: "image/png",
510
+ GIF: "image/gif"
511
+ };
512
+ var PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
513
+ function mediaExtension(mediaType) {
514
+ return mediaType === MediaType.GIF ? ".gif" : ".png";
515
+ }
516
+ function mediaTypeFromExtension(file) {
517
+ const lower = file.toLowerCase();
518
+ if (lower.endsWith(".png")) return MediaType.PNG;
519
+ if (lower.endsWith(".gif")) return MediaType.GIF;
520
+ return null;
521
+ }
522
+ function detectMediaType(bytes) {
523
+ if (bytes.subarray(0, PNG_SIGNATURE.length).equals(PNG_SIGNATURE)) return MediaType.PNG;
524
+ const header = bytes.subarray(0, 6).toString("ascii");
525
+ if (header === "GIF87a" || header === "GIF89a") return MediaType.GIF;
526
+ return null;
527
+ }
528
+ function validateMedia(bytes, expected) {
529
+ const actual = detectMediaType(bytes);
530
+ if (!actual) throw new Error(`response was not a supported PNG or GIF (${bytes.length} bytes)`);
531
+ if (expected && actual !== expected) {
532
+ throw new Error(`response was ${actual}, expected ${expected}`);
533
+ }
534
+ if (actual === MediaType.PNG) {
535
+ decodePng(bytes);
536
+ } else {
537
+ validateGif(bytes);
538
+ }
539
+ return actual;
540
+ }
541
+ function validateGif(bytes) {
542
+ if (bytes.length < 14) throw new Error("invalid GIF: truncated logical screen descriptor");
543
+ const width = bytes.readUInt16LE(6);
544
+ const height = bytes.readUInt16LE(8);
545
+ if (!width || !height) throw new Error("invalid GIF: zero-sized logical screen");
546
+ const packed = bytes[10];
547
+ let offset = 13;
548
+ if (packed & 128) offset += 3 * 2 ** ((packed & 7) + 1);
549
+ if (offset > bytes.length) throw new Error("invalid GIF: truncated global color table");
550
+ let sawImage = false;
551
+ while (offset < bytes.length) {
552
+ const marker = bytes[offset];
553
+ if (marker === 59) {
554
+ if (!sawImage) throw new Error("invalid GIF: contains no image frame");
555
+ return;
556
+ }
557
+ if (marker === 44) {
558
+ if (offset + 10 > bytes.length) throw new Error("invalid GIF: truncated image descriptor");
559
+ const imagePacked = bytes[offset + 9];
560
+ offset += 10;
561
+ if (imagePacked & 128) offset += 3 * 2 ** ((imagePacked & 7) + 1);
562
+ if (offset >= bytes.length) throw new Error("invalid GIF: missing image data");
563
+ offset++;
564
+ offset = skipSubBlocks(bytes, offset);
565
+ sawImage = true;
566
+ continue;
567
+ }
568
+ if (marker === 33) {
569
+ if (offset + 2 > bytes.length) throw new Error("invalid GIF: truncated extension");
570
+ offset = skipSubBlocks(bytes, offset + 2);
571
+ continue;
572
+ }
573
+ throw new Error(`invalid GIF: unexpected block marker 0x${marker.toString(16)}`);
574
+ }
575
+ throw new Error("invalid GIF: missing trailer");
576
+ }
577
+ function skipSubBlocks(bytes, start) {
578
+ let offset = start;
579
+ for (; ; ) {
580
+ if (offset >= bytes.length) throw new Error("invalid GIF: truncated data blocks");
581
+ const size = bytes[offset];
582
+ offset++;
583
+ if (size === 0) return offset;
584
+ offset += size;
585
+ if (offset > bytes.length) throw new Error("invalid GIF: truncated data block");
586
+ }
587
+ }
588
+ function cacheFileName(hash, mediaType = MediaType.PNG) {
589
+ return `${hash}${mediaExtension(mediaType)}`;
590
+ }
591
+
592
+ // src/providers/comfyui.ts
593
+ var DEFAULT_BASE_URL = "http://127.0.0.1:8188";
594
+ var ComfyUIClient = class {
595
+ constructor(baseUrl = process.env.COMFYUI_BASE_URL ?? DEFAULT_BASE_URL, request = fetch) {
596
+ this.request = request;
597
+ this.baseUrl = normalizeBaseUrl(baseUrl);
598
+ }
599
+ request;
600
+ baseUrl;
601
+ async checkConnection() {
602
+ const value = await this.json("system_stats");
603
+ if (!isObject(value)) throw new Error("ComfyUI returned invalid system stats");
604
+ }
605
+ async submit(workflow) {
606
+ const value = await this.json("prompt", {
607
+ method: "POST",
608
+ body: JSON.stringify({ prompt: workflow, client_id: randomUUID() })
609
+ });
610
+ if (!isObject(value)) throw new Error("ComfyUI returned an invalid queue response");
611
+ const promptId = value.prompt_id;
612
+ if (typeof promptId !== "string" || !promptId) {
613
+ const nodes = isObject(value.node_errors) ? Object.keys(value.node_errors) : [];
614
+ throw new Error(
615
+ "ComfyUI did not return a prompt id" + (nodes.length ? `; invalid workflow node(s): ${nodes.slice(0, 8).join(", ")}` : "")
616
+ );
617
+ }
618
+ return promptId;
619
+ }
620
+ async history(promptId) {
621
+ return this.json(`history/${encodeURIComponent(promptId)}`);
622
+ }
623
+ viewUrl(image) {
624
+ const url = this.endpoint("view");
625
+ url.searchParams.set("filename", image.filename);
626
+ url.searchParams.set("subfolder", image.subfolder);
627
+ url.searchParams.set("type", image.type);
628
+ return url.toString();
629
+ }
630
+ async download(source) {
631
+ let target = source;
632
+ if (source.startsWith("comfyui://")) target = this.viewUrl(parseSource(source));
633
+ const response = await this.request(target);
634
+ if (!response.ok) throw new Error(`ComfyUI download failed (${response.status})`);
635
+ return Buffer.from(await response.arrayBuffer());
636
+ }
637
+ endpoint(route) {
638
+ return new URL(route.replace(/^\/+/, ""), `${this.baseUrl}/`);
639
+ }
640
+ async json(route, init = {}) {
641
+ const url = this.endpoint(route);
642
+ const response = await this.request(url, {
643
+ ...init,
644
+ headers: {
645
+ ...init.body ? { "Content-Type": "application/json" } : {},
646
+ ...init.headers
647
+ }
648
+ });
649
+ const text = await response.text();
650
+ let value = null;
651
+ try {
652
+ value = text ? JSON.parse(text) : null;
653
+ } catch {
654
+ if (response.ok) throw new Error(`ComfyUI returned invalid JSON from ${url.pathname}`);
655
+ value = null;
656
+ }
657
+ if (!response.ok) {
658
+ const detail = apiError(value);
659
+ throw new Error(
660
+ `ComfyUI request failed (${response.status}) at ${url.pathname}` + (detail ? `: ${detail}` : "")
661
+ );
662
+ }
663
+ return value;
664
+ }
665
+ };
666
+ var ComfyUIProvider = class _ComfyUIProvider {
667
+ constructor(client = new ComfyUIClient()) {
668
+ this.client = client;
669
+ }
670
+ client;
671
+ id = "comfyui";
672
+ static fromEnv() {
673
+ return new _ComfyUIProvider();
674
+ }
675
+ static forOffline() {
676
+ return new _ComfyUIProvider(new ComfyUIClient(DEFAULT_BASE_URL));
677
+ }
678
+ static forDownloads() {
679
+ return new _ComfyUIProvider();
680
+ }
681
+ async resolveOptions(value, context) {
682
+ const options = parseOptions(value);
683
+ const workflowPath = path2.resolve(context.root, options.workflowFile);
684
+ let raw;
685
+ try {
686
+ raw = await readFile2(workflowPath, "utf8");
687
+ } catch (err) {
688
+ throw new Error(
689
+ `ComfyUI workflow for style "${context.styleId}" could not be read at ${workflowPath}: ${err instanceof Error ? err.message : String(err)}`
690
+ );
691
+ }
692
+ let parsed;
693
+ try {
694
+ parsed = JSON.parse(raw);
695
+ } catch {
696
+ throw new Error(`ComfyUI workflow for style "${context.styleId}" is not valid JSON: ${workflowPath}`);
697
+ }
698
+ const workflow = parseWorkflow(parsed, workflowPath);
699
+ validateWorkflowBindings(workflow, options);
700
+ const workflowSha256 = sha256(JSON.stringify(canonical(workflow)));
701
+ const resolved = { ...options, workflow, workflowSha256 };
702
+ return {
703
+ options: resolved,
704
+ identity: {
705
+ outputNodeId: options.outputNodeId,
706
+ numImages: options.numImages,
707
+ bindings: options.bindings,
708
+ workflowSha256
709
+ }
710
+ };
711
+ }
712
+ supports(generator) {
713
+ return generator === "map";
714
+ }
715
+ estimate(spec) {
716
+ return { unit: "free", amount: 0, candidates: resolvedOptions(spec).numImages };
717
+ }
718
+ validate(spec, styleImages) {
719
+ const options = resolvedOptions(spec);
720
+ if (spec.width < 16 || spec.height < 16 || spec.width > 4096 || spec.height > 4096) {
721
+ throw new Error("ComfyUI output dimensions must be between 16 and 4096 pixels");
722
+ }
723
+ if (styleImages.length) {
724
+ throw new Error("ComfyUI styleImages are not supported yet; keep references inside the workflow");
725
+ }
726
+ if (spec.palette.length) {
727
+ throw new Error("ComfyUI does not map PixelKiln palette values yet; encode palette control in the workflow");
728
+ }
729
+ if (spec.seed != null && !options.bindings.seed) {
730
+ throw new Error("ComfyUI requires bindings.seed when the style declares a seed");
731
+ }
732
+ validateWorkflowBindings(options.workflow, options);
733
+ }
734
+ rateLimit() {
735
+ return { spacingMs: 0, maxInFlight: 1 };
736
+ }
737
+ async submit(spec, styleImages) {
738
+ this.validate(spec, styleImages);
739
+ const options = resolvedOptions(spec);
740
+ const workflow = structuredClone(options.workflow);
741
+ setBinding(workflow, options.bindings.prompt, spec.prompt);
742
+ setBinding(workflow, options.bindings.width, spec.width);
743
+ setBinding(workflow, options.bindings.height, spec.height);
744
+ setBinding(workflow, options.bindings.batchSize, options.numImages);
745
+ if (spec.seed != null && options.bindings.seed) {
746
+ setBinding(workflow, options.bindings.seed, spec.seed);
747
+ }
748
+ const promptId = await this.client.submit(workflow);
749
+ return { jobId: encodeJob(promptId, options.outputNodeId) };
750
+ }
751
+ async poll(jobId, _generator, context) {
752
+ const { promptId, outputNodeId } = decodeJob(jobId);
753
+ const history = await this.client.history(promptId);
754
+ const entry = historyEntry(history, promptId);
755
+ if (!entry) return { status: "processing" };
756
+ const status = isObject(entry.status) ? entry.status : {};
757
+ const statusText = typeof status.status_str === "string" ? status.status_str : "";
758
+ if (statusText === "error") {
759
+ return { status: "failed", error: historyError(status) };
760
+ }
761
+ if (status.completed === false) return { status: "processing" };
762
+ const images = outputImages(entry, outputNodeId);
763
+ if (images instanceof Error) return { status: "failed", error: images.message };
764
+ const expected = context?.spec ? resolvedOptions(context.spec).numImages : null;
765
+ if (expected != null && images.length !== expected) {
766
+ return {
767
+ status: "failed",
768
+ error: `ComfyUI output node "${outputNodeId}" returned ${images.length} image(s), expected ${expected}`
769
+ };
770
+ }
771
+ const metadata = context?.spec ? comfyMetadata(context.spec, promptId, outputNodeId, images) : {
772
+ promptId,
773
+ outputNodeId,
774
+ outputCount: images.length
775
+ };
776
+ if (images.length > 1) {
777
+ return {
778
+ status: "review",
779
+ candidateUrls: images.map((image) => this.client.viewUrl(image)),
780
+ metadata
781
+ };
782
+ }
783
+ const sourceUrl = sourceRef(images[0]);
784
+ return {
785
+ status: "ready",
786
+ objectId: `${promptId}#${outputNodeId}#0`,
787
+ sourceUrl,
788
+ sources: [{ url: sourceUrl, mediaType: MediaType.PNG }],
789
+ metadata
790
+ };
791
+ }
792
+ async selectCandidate(jobId, index) {
793
+ const { promptId, outputNodeId } = decodeJob(jobId);
794
+ const history = await this.client.history(promptId);
795
+ const entry = historyEntry(history, promptId);
796
+ if (!entry) throw new Error(`ComfyUI prompt ${promptId} is not complete`);
797
+ const images = outputImages(entry, outputNodeId);
798
+ if (images instanceof Error) throw images;
799
+ const image = images[index];
800
+ if (!image) throw new Error(`ComfyUI prompt ${promptId} has no candidate at index ${index}`);
801
+ return {
802
+ objectId: `${promptId}#${outputNodeId}#${index}`,
803
+ sourceUrl: sourceRef(image)
804
+ };
805
+ }
806
+ async download(url) {
807
+ return this.client.download(url);
808
+ }
809
+ async checkConnection() {
810
+ await this.client.checkConnection();
811
+ }
812
+ };
813
+ function parseOptions(value) {
814
+ const allowed = /* @__PURE__ */ new Set(["workflowFile", "outputNodeId", "numImages", "bindings", "workflow", "workflowSha256"]);
815
+ const extra = Object.keys(value).filter((key) => !allowed.has(key));
816
+ if (extra.length) throw new Error(`Unknown ComfyUI option(s): ${extra.join(", ")}`);
817
+ const workflowFile = requiredString(value.workflowFile, "workflowFile");
818
+ const outputNodeId = requiredString(value.outputNodeId, "outputNodeId");
819
+ const numImages = value.numImages ?? 1;
820
+ if (!Number.isInteger(numImages) || Number(numImages) < 1 || Number(numImages) > 16) {
821
+ throw new Error("ComfyUI numImages must be a whole number from 1 to 16");
822
+ }
823
+ if (!isObject(value.bindings)) throw new Error("ComfyUI bindings must be an object");
824
+ const bindingKeys = /* @__PURE__ */ new Set(["prompt", "width", "height", "batchSize", "seed"]);
825
+ const extraBindings = Object.keys(value.bindings).filter((key) => !bindingKeys.has(key));
826
+ if (extraBindings.length) throw new Error(`Unknown ComfyUI binding(s): ${extraBindings.join(", ")}`);
827
+ const bindings = {
828
+ prompt: parseBinding(value.bindings.prompt, "prompt"),
829
+ width: parseBinding(value.bindings.width, "width"),
830
+ height: parseBinding(value.bindings.height, "height"),
831
+ batchSize: parseBinding(value.bindings.batchSize, "batchSize"),
832
+ ...value.bindings.seed == null ? {} : { seed: parseBinding(value.bindings.seed, "seed") }
833
+ };
834
+ const workflow = value.workflow == null ? void 0 : parseWorkflow(value.workflow, workflowFile);
835
+ const workflowSha256 = value.workflowSha256 == null ? void 0 : requiredString(value.workflowSha256, "workflowSha256");
836
+ return {
837
+ workflowFile,
838
+ outputNodeId,
839
+ numImages: Number(numImages),
840
+ bindings,
841
+ ...workflow ? { workflow } : {},
842
+ ...workflowSha256 ? { workflowSha256 } : {}
843
+ };
844
+ }
845
+ function resolvedOptions(spec) {
846
+ const options = parseOptions(spec.providerOptions);
847
+ if (!options.workflow || !options.workflowSha256) {
848
+ throw new Error("ComfyUI workflow options were not resolved from workflowFile");
849
+ }
850
+ return options;
851
+ }
852
+ function parseBinding(value, name) {
853
+ if (!isObject(value)) throw new Error(`ComfyUI bindings.${name} must be an object`);
854
+ const extra = Object.keys(value).filter((key) => key !== "nodeId" && key !== "input");
855
+ if (extra.length) throw new Error(`Unknown ComfyUI bindings.${name} field(s): ${extra.join(", ")}`);
856
+ return {
857
+ nodeId: requiredString(value.nodeId, `bindings.${name}.nodeId`),
858
+ input: requiredString(value.input, `bindings.${name}.input`)
859
+ };
860
+ }
861
+ function parseWorkflow(value, label) {
862
+ if (!isObject(value) || !Object.keys(value).length) {
863
+ throw new Error(`ComfyUI workflow is not a non-empty API-format object: ${label}`);
864
+ }
865
+ for (const [nodeId, node] of Object.entries(value)) {
866
+ if (!isObject(node) || typeof node.class_type !== "string" || !isObject(node.inputs)) {
867
+ throw new Error(`ComfyUI workflow node "${nodeId}" must contain class_type and inputs`);
868
+ }
869
+ }
870
+ return value;
871
+ }
872
+ function validateWorkflowBindings(workflow, options) {
873
+ for (const [name, binding] of Object.entries(options.bindings)) {
874
+ if (!binding) continue;
875
+ const node = workflow[binding.nodeId];
876
+ if (!node) throw new Error(`ComfyUI ${name} binding refers to missing node "${binding.nodeId}"`);
877
+ if (!Object.hasOwn(node.inputs, binding.input)) {
878
+ throw new Error(
879
+ `ComfyUI ${name} binding refers to missing input "${binding.input}" on node "${binding.nodeId}"`
880
+ );
881
+ }
882
+ }
883
+ if (!workflow[options.outputNodeId]) {
884
+ throw new Error(`ComfyUI outputNodeId refers to missing node "${options.outputNodeId}"`);
885
+ }
886
+ }
887
+ function setBinding(workflow, binding, value) {
888
+ const node = workflow[binding.nodeId];
889
+ if (!node || !Object.hasOwn(node.inputs, binding.input)) {
890
+ throw new Error(`ComfyUI binding ${binding.nodeId}.${binding.input} is unavailable`);
891
+ }
892
+ node.inputs[binding.input] = value;
893
+ }
894
+ function historyEntry(history, promptId) {
895
+ if (!isObject(history)) throw new Error("ComfyUI returned invalid history JSON");
896
+ const entry = history[promptId];
897
+ if (entry == null) return null;
898
+ if (!isObject(entry)) throw new Error(`ComfyUI returned invalid history for prompt ${promptId}`);
899
+ return entry;
900
+ }
901
+ function outputImages(entry, outputNodeId) {
902
+ if (!isObject(entry.outputs)) return new Error("ComfyUI completed without workflow outputs");
903
+ const output = entry.outputs[outputNodeId];
904
+ if (!isObject(output) || !Array.isArray(output.images)) {
905
+ return new Error(`ComfyUI output node "${outputNodeId}" returned no images`);
906
+ }
907
+ const images = [];
908
+ for (const value of output.images) {
909
+ if (!isObject(value) || typeof value.filename !== "string" || typeof value.subfolder !== "string" || value.type !== "output") {
910
+ return new Error(`ComfyUI output node "${outputNodeId}" returned an invalid image record`);
911
+ }
912
+ images.push({ filename: value.filename, subfolder: value.subfolder, type: "output" });
913
+ }
914
+ if (!images.length) return new Error(`ComfyUI output node "${outputNodeId}" returned no images`);
915
+ return images;
916
+ }
917
+ function comfyMetadata(spec, promptId, outputNodeId, images) {
918
+ const options = resolvedOptions(spec);
919
+ return {
920
+ promptId,
921
+ outputNodeId,
922
+ outputCount: images.length,
923
+ workflowFile: options.workflowFile,
924
+ workflowSha256: options.workflowSha256,
925
+ files: images.map((image) => ({ ...image }))
926
+ };
927
+ }
928
+ function encodeJob(promptId, outputNodeId) {
929
+ return `${promptId}#${encodeURIComponent(outputNodeId)}`;
930
+ }
931
+ function decodeJob(jobId) {
932
+ const split = jobId.lastIndexOf("#");
933
+ if (split < 1 || split === jobId.length - 1) throw new Error(`Invalid ComfyUI job id "${jobId}"`);
934
+ return {
935
+ promptId: jobId.slice(0, split),
936
+ outputNodeId: decodeURIComponent(jobId.slice(split + 1))
937
+ };
938
+ }
939
+ function sourceRef(image) {
940
+ const url = new URL("comfyui://output");
941
+ url.searchParams.set("filename", image.filename);
942
+ url.searchParams.set("subfolder", image.subfolder);
943
+ url.searchParams.set("type", image.type);
944
+ return url.toString();
945
+ }
946
+ function parseSource(source) {
947
+ let url;
948
+ try {
949
+ url = new URL(source);
950
+ } catch {
951
+ throw new Error("Invalid ComfyUI output reference");
952
+ }
953
+ const filename = url.searchParams.get("filename");
954
+ const subfolder = url.searchParams.get("subfolder");
955
+ const type = url.searchParams.get("type");
956
+ if (url.protocol !== "comfyui:" || url.hostname !== "output" || !filename || subfolder == null || type !== "output") {
957
+ throw new Error("Invalid ComfyUI output reference");
958
+ }
959
+ return { filename, subfolder, type };
960
+ }
961
+ function normalizeBaseUrl(value) {
962
+ let url;
963
+ try {
964
+ url = new URL(value);
965
+ } catch {
966
+ throw new Error("COMFYUI_BASE_URL must be an absolute HTTP(S) URL");
967
+ }
968
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
969
+ throw new Error("COMFYUI_BASE_URL must use HTTP or HTTPS");
970
+ }
971
+ url.hash = "";
972
+ url.search = "";
973
+ return url.toString().replace(/\/$/, "");
974
+ }
975
+ function historyError(status) {
976
+ const messages = Array.isArray(status.messages) ? status.messages : [];
977
+ for (const message2 of messages) {
978
+ if (!Array.isArray(message2) || message2[0] !== "execution_error" || !isObject(message2[1])) continue;
979
+ const detail = message2[1].exception_message;
980
+ if (typeof detail === "string" && detail.trim()) {
981
+ return `ComfyUI execution failed: ${detail.trim().slice(0, 300)}`;
982
+ }
983
+ }
984
+ return "ComfyUI execution failed";
985
+ }
986
+ function apiError(value) {
987
+ if (!isObject(value)) return null;
988
+ 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;
989
+ const nodes = isObject(value.node_errors) ? Object.keys(value.node_errors) : [];
990
+ if (nodes.length) {
991
+ return `${raw ? `${raw}; ` : ""}invalid workflow node(s): ${nodes.slice(0, 8).join(", ")}`;
992
+ }
993
+ return raw?.trim().slice(0, 300) || null;
994
+ }
995
+ function requiredString(value, name) {
996
+ if (typeof value !== "string" || !value.trim()) throw new Error(`ComfyUI ${name} must be a non-empty string`);
997
+ return value.trim();
998
+ }
999
+ function isObject(value) {
1000
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
1001
+ }
1002
+ function canonical(value) {
1003
+ if (Array.isArray(value)) return value.map(canonical);
1004
+ if (!isObject(value)) return value;
1005
+ return Object.fromEntries(
1006
+ Object.keys(value).sort().map((key) => [key, canonical(value[key])])
1007
+ );
1008
+ }
1009
+
1010
+ // src/providers/pixellab.ts
1011
+ import { mkdirSync, existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
1012
+ import { randomUUID as randomUUID2 } from "crypto";
1013
+ import os from "os";
1014
+ import path3 from "path";
1015
+
1016
+ // src/client.ts
1017
+ import { z } from "zod";
1018
+ var BASE = process.env.PIXELLAB_API_BASE ?? "https://api.pixellab.ai/v2";
1019
+ var MAX_RETRIES = 4;
1020
+ var MAX_DOWNLOAD_BYTES = 64 * 1024 * 1024;
1021
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
1022
+ function shouldRetry(status) {
1023
+ return status === 429 || status === 408 || status >= 500;
1024
+ }
1025
+ function backoffMs(attempt) {
1026
+ const base = Math.min(1e3 * 2 ** attempt, 16e3);
1027
+ return base + Math.floor(Math.random() * 400);
1028
+ }
1029
+ function retryAfterMs(value, now = Date.now()) {
1030
+ if (!value) return null;
1031
+ const seconds = Number(value);
1032
+ if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1e3;
1033
+ const date = Date.parse(value);
1034
+ if (!Number.isFinite(date)) return null;
1035
+ return Math.max(0, date - now);
1036
+ }
1037
+ var BalanceResponseSchema = z.object({
1038
+ credits: z.object({ usd: z.number() }).passthrough(),
1039
+ subscription: z.object({
1040
+ generations: z.number(),
1041
+ total: z.number(),
1042
+ plan: z.string().nullable().optional()
1043
+ }).passthrough()
1044
+ }).passthrough();
1045
+ var ObjectSubmitSchema = z.object({
1046
+ background_job_id: z.string().min(1),
1047
+ object_id: z.string().min(1),
1048
+ status: z.string().default("queued"),
1049
+ n_frames: z.number().int().min(0)
1050
+ }).passthrough();
1051
+ var MapSubmitSchema = z.object({
1052
+ background_job_id: z.string().min(1),
1053
+ object_id: z.string().min(1),
1054
+ status: z.string().default("processing")
1055
+ }).passthrough();
1056
+ var TilesSubmitSchema = z.object({
1057
+ tile_id: z.string().min(1),
1058
+ background_job_id: z.string().min(1),
1059
+ status: z.literal("processing").default("processing")
1060
+ }).passthrough();
1061
+ var TilesProSchema = z.object({
1062
+ storage_urls: z.record(z.string()),
1063
+ kind: z.string().nullable().default(null),
1064
+ tile_rules: z.record(z.unknown()).nullable().optional()
1065
+ }).passthrough();
1066
+ var PixelLabObjectSchema = z.object({
1067
+ id: z.string().min(1),
1068
+ name: z.string().nullable().default(null),
1069
+ prompt: z.string().default(""),
1070
+ size: z.object({ width: z.number().int().positive(), height: z.number().int().positive() }),
1071
+ directions: z.number().default(0),
1072
+ created_at: z.string(),
1073
+ view: z.string().nullable().default(null),
1074
+ preview_url: z.string().nullable().optional(),
1075
+ rotation_urls: z.record(z.string().nullable()).nullable().optional(),
1076
+ frame_urls: z.array(z.string()).nullable().optional(),
1077
+ tags: z.array(z.string()).default([]),
1078
+ status: z.string().nullable().default(null),
1079
+ progress_percent: z.number().nullable().optional(),
1080
+ eta_seconds: z.number().nullable().optional()
1081
+ }).passthrough();
1082
+ var MapObjectSchema = z.object({
1083
+ object_id: z.string().min(1),
1084
+ status: z.string(),
1085
+ description: z.string().nullable().default(null),
1086
+ width: z.number().nullable().default(null),
1087
+ height: z.number().nullable().default(null),
1088
+ download_url: z.string().nullable().default(null)
1089
+ }).passthrough();
1090
+ var ObjectListSchema = z.object({ objects: z.array(PixelLabObjectSchema), total: z.number().int().min(0) }).passthrough();
1091
+ var PixfluxResponseSchema = z.object({ image: z.object({ base64: z.string().min(1) }).passthrough(), usage: z.unknown().optional() }).passthrough();
1092
+ var SelectFramesSchema = z.object({ created_object_ids: z.array(z.string()) }).passthrough();
1093
+ function validateResponse(schema, raw, operation) {
1094
+ const parsed = schema.safeParse(raw);
1095
+ if (parsed.success) return parsed.data;
1096
+ const issues = parsed.error.issues.slice(0, 4).map((i) => `${i.path.join(".") || "response"}: ${i.message}`).join("; ");
1097
+ throw new Error(`Invalid PixelLab response for ${operation}: ${issues}`);
1098
+ }
1099
+ var PixelLabError = class extends Error {
1100
+ constructor(message2, status, body) {
1101
+ super(message2);
1102
+ this.status = status;
1103
+ this.body = body;
1104
+ this.name = "PixelLabError";
1105
+ }
1106
+ status;
1107
+ body;
1108
+ };
1109
+ var PixelLabClient = class {
1110
+ constructor(apiKey, timeoutMs = 12e4) {
1111
+ this.apiKey = apiKey;
1112
+ this.timeoutMs = timeoutMs;
1113
+ if (!apiKey) throw new Error("PIXELLAB_API_KEY is required");
1114
+ }
1115
+ apiKey;
1116
+ timeoutMs;
1117
+ /**
1118
+ * Retries only what is safe to retry: transport failures, 429, and 5xx.
1119
+ * A 4xx other than 429 is a bad request and retrying it just wastes time.
1120
+ *
1121
+ * POSTs that create objects are included, which is a deliberate trade: the
1122
+ * failure mode of not retrying (a dropped asset in a 65-item run) is more
1123
+ * common than the failure mode of retrying (a duplicate object), and a
1124
+ * duplicate is visible and free to delete whereas a silent gap is neither.
1125
+ */
1126
+ async request(path20, init, attempt = 0) {
1127
+ const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
1128
+ let res;
1129
+ try {
1130
+ res = await fetch(`${BASE}${path20}`, {
1131
+ ...init,
1132
+ signal: init?.signal ?? AbortSignal.timeout(this.timeoutMs),
1133
+ headers: {
1134
+ Authorization: auth,
1135
+ "Content-Type": "application/json",
1136
+ ...init?.headers ?? {}
1137
+ }
1138
+ });
1139
+ } catch (err) {
1140
+ if (attempt < MAX_RETRIES) {
1141
+ await sleep(backoffMs(attempt));
1142
+ return this.request(path20, init, attempt + 1);
1143
+ }
1144
+ throw err;
1145
+ }
1146
+ if (!res.ok && shouldRetry(res.status) && attempt < MAX_RETRIES) {
1147
+ const waitMs = retryAfterMs(res.headers.get("retry-after")) ?? backoffMs(attempt);
1148
+ await sleep(waitMs);
1149
+ return this.request(path20, init, attempt + 1);
1150
+ }
1151
+ const text = await res.text();
1152
+ if (!res.ok) {
1153
+ throw new PixelLabError(`${init?.method ?? "GET"} ${path20} \u2192 ${res.status}`, res.status, text);
1154
+ }
1155
+ if (!text) return {};
1156
+ try {
1157
+ return JSON.parse(text);
1158
+ } catch {
1159
+ throw new Error(`${init?.method ?? "GET"} ${path20} returned invalid JSON`);
1160
+ }
1161
+ }
1162
+ async balance() {
1163
+ const raw = validateResponse(BalanceResponseSchema, await this.request("/balance"), "balance");
1164
+ return {
1165
+ usd: raw.credits?.usd ?? 0,
1166
+ generations: raw.subscription?.generations ?? 0,
1167
+ total: raw.subscription?.total ?? 0,
1168
+ plan: raw.subscription?.plan ?? "unknown"
1169
+ };
1170
+ }
1171
+ /**
1172
+ * Square objects that persist indefinitely.
1173
+ *
1174
+ * `size` and `styleImages` are mutually exclusive at the API level: when style
1175
+ * images are supplied the largest one dictates the output size. So style
1176
+ * references must already be at the target resolution — a 128px reference
1177
+ * silently produces 128px output and a different candidate count.
1178
+ */
1179
+ async create1Direction(args) {
1180
+ const body = { description: args.description };
1181
+ if (args.styleImages?.length) {
1182
+ body.style_images = args.styleImages.map(({ base64, format }) => ({
1183
+ type: "base64",
1184
+ base64,
1185
+ format
1186
+ }));
1187
+ } else if (args.size != null) {
1188
+ body.size = args.size;
1189
+ }
1190
+ if (args.view) body.view = args.view;
1191
+ if (args.itemDescriptions?.length) body.item_descriptions = args.itemDescriptions;
1192
+ const raw = await this.request("/create-1-direction-object", {
1193
+ method: "POST",
1194
+ body: JSON.stringify(body)
1195
+ });
1196
+ const parsed = validateResponse(ObjectSubmitSchema, raw, "create-1-direction-object");
1197
+ return parsed;
1198
+ }
1199
+ /**
1200
+ * Arbitrary width x height. Returns a single result — no selection step.
1201
+ *
1202
+ * These AUTO-DELETE AFTER 8 HOURS, so `fetch` must run in the same session as
1203
+ * `submit`. The pipeline warns when a map-object entry is older than that.
1204
+ */
1205
+ async createMapObject(args) {
1206
+ const body = {
1207
+ description: args.description,
1208
+ image_size: { width: args.width, height: args.height }
1209
+ };
1210
+ if (args.view) body.view = args.view;
1211
+ if (args.outline) body.outline = args.outline;
1212
+ if (args.shading) body.shading = args.shading;
1213
+ if (args.detail) body.detail = args.detail;
1214
+ if (args.seed != null) body.seed = args.seed;
1215
+ return validateResponse(
1216
+ MapSubmitSchema,
1217
+ await this.request("/map-objects", { method: "POST", body: JSON.stringify(body) }),
1218
+ "create map object"
1219
+ );
1220
+ }
1221
+ /**
1222
+ * Draws a whole tile set in one call — many variations, or a connectable
1223
+ * set when `tileFeature` is given.
1224
+ *
1225
+ * `styleImages` here is NOT the shape `create-1-direction-object` uses.
1226
+ * TilesProStyleImage is flat — `{base64, width, height}`, all three
1227
+ * required — where 1dir wants `{type, base64, format}`. Confirmed against
1228
+ * the OpenAPI schema; sending 1dir's shape is rejected as an extra field.
1229
+ *
1230
+ * Passing style images also makes the API ignore `tileType` and `tileView`
1231
+ * and copy the reference's tile geometry instead.
1232
+ */
1233
+ async createTilesPro(args) {
1234
+ const body = { description: args.description };
1235
+ if (args.tileSize != null) body.tile_size = args.tileSize;
1236
+ if (args.tileType) body.tile_type = args.tileType;
1237
+ if (args.tileView) body.tile_view = args.tileView;
1238
+ if (args.tileFeature) body.tile_feature = args.tileFeature;
1239
+ if (args.outlineMode) body.outline_mode = args.outlineMode;
1240
+ if (args.seed != null) body.seed = args.seed;
1241
+ if (args.styleImages?.length) body.style_images = args.styleImages;
1242
+ return validateResponse(
1243
+ TilesSubmitSchema,
1244
+ await this.request("/create-tiles-pro", { method: "POST", body: JSON.stringify(body) }),
1245
+ "create tiles"
1246
+ );
1247
+ }
1248
+ /** Throws PixelLabError(423) while the set is still drawing — see TilesPro. */
1249
+ async getTilesPro(tileId) {
1250
+ return validateResponse(
1251
+ TilesProSchema,
1252
+ await this.request(`/tiles-pro/${tileId}`),
1253
+ "get tiles"
1254
+ );
1255
+ }
1256
+ /**
1257
+ * Synchronous single-image generation. Returns the PNG inline rather than a
1258
+ * job id, and is the only endpoint that honours a forced palette —
1259
+ * `color_image` on /map-objects returns a 500 whatever the payload shape.
1260
+ */
1261
+ async createImagePixflux(args) {
1262
+ const body = {
1263
+ description: args.description,
1264
+ image_size: { width: args.width, height: args.height },
1265
+ no_background: args.noBackground ?? true
1266
+ };
1267
+ if (args.paletteSwatchBase64) {
1268
+ body.color_image = { type: "base64", base64: args.paletteSwatchBase64, format: "png" };
1269
+ }
1270
+ if (args.seed != null) body.seed = args.seed;
1271
+ const res = validateResponse(
1272
+ PixfluxResponseSchema,
1273
+ await this.request("/create-image-pixflux", {
1274
+ method: "POST",
1275
+ body: JSON.stringify(body)
1276
+ }),
1277
+ "create pixflux image"
1278
+ );
1279
+ const b64 = res.image.base64;
1280
+ return { png: Buffer.from(b64, "base64"), usage: res.usage };
1281
+ }
1282
+ async getObject(objectId) {
1283
+ return validateResponse(
1284
+ PixelLabObjectSchema,
1285
+ await this.request(`/objects/${objectId}`),
1286
+ "get object"
1287
+ );
1288
+ }
1289
+ async getMapObject(objectId) {
1290
+ return validateResponse(
1291
+ MapObjectSchema,
1292
+ await this.request(`/map-objects/${objectId}`),
1293
+ "get map object"
1294
+ );
1295
+ }
1296
+ async listObjects(limit = 50, offset = 0) {
1297
+ return validateResponse(
1298
+ ObjectListSchema,
1299
+ await this.request(`/objects?limit=${limit}&offset=${offset}`),
1300
+ "list objects"
1301
+ );
1302
+ }
1303
+ /** Walks the whole account. Used by `adopt` to reconcile orphaned objects. */
1304
+ async *iterateObjects(pageSize = 100) {
1305
+ let offset = 0;
1306
+ for (; ; ) {
1307
+ const page = await this.listObjects(pageSize, offset);
1308
+ for (const obj of page.objects) yield obj;
1309
+ offset += page.objects.length;
1310
+ if (page.objects.length === 0 || offset >= page.total) return;
1311
+ }
1312
+ }
1313
+ /**
1314
+ * Promotes chosen candidates to standalone objects, each with its own id.
1315
+ * The review parent survives until nothing is left in it, so the returned
1316
+ * `created_object_ids` — not the parent id — is what should be recorded.
1317
+ */
1318
+ async selectFrames(objectId, indices, commonTag) {
1319
+ const raw = await this.request(`/objects/${objectId}/select-frames`, {
1320
+ method: "POST",
1321
+ body: JSON.stringify(commonTag ? { indices, common_tag: commonTag } : { indices })
1322
+ });
1323
+ return validateResponse(SelectFramesSchema, raw, "select frames");
1324
+ }
1325
+ /** Irreversible. Only reached via `purge`, behind an explicit confirmation. */
1326
+ deleteObject(objectId) {
1327
+ return this.request(`/objects/${objectId}`, { method: "DELETE" });
756
1328
  }
757
- const ihdr = Buffer.alloc(13);
758
- ihdr.writeUInt32BE(width, 0);
759
- ihdr.writeUInt32BE(height, 4);
760
- ihdr[8] = 8;
761
- ihdr[9] = 2;
762
- return Buffer.concat([
763
- Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]),
764
- chunk("IHDR", ihdr),
765
- chunk("IDAT", deflateSync(raw)),
766
- chunk("IEND", Buffer.alloc(0))
767
- ]);
768
- }
769
- function encodeRgbaPng(width, height, rgba) {
770
- if (rgba.length !== width * height * 4) {
771
- throw new Error(`expected ${width * height * 4} bytes of RGBA, got ${rgba.length}`);
1329
+ dismissReview(objectId) {
1330
+ return this.request(`/objects/${objectId}/dismiss-review`, { method: "POST" });
772
1331
  }
773
- const stride = width * 4;
774
- const raw = Buffer.alloc((stride + 1) * height);
775
- for (let y = 0; y < height; y++) {
776
- raw[y * (stride + 1)] = 0;
777
- rgba.copy(raw, y * (stride + 1) + 1, y * stride, (y + 1) * stride);
1332
+ /** Free and synchronous. Replaces the full tag set — include tags you want to keep. */
1333
+ setTags(objectId, tags) {
1334
+ return this.request(`/objects/${objectId}/tags`, { method: "PATCH", body: JSON.stringify({ tags }) });
778
1335
  }
779
- const ihdr = Buffer.alloc(13);
780
- ihdr.writeUInt32BE(width, 0);
781
- ihdr.writeUInt32BE(height, 4);
782
- ihdr[8] = 8;
783
- ihdr[9] = 6;
784
- return Buffer.concat([
785
- Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]),
786
- chunk("IHDR", ihdr),
787
- chunk("IDAT", deflateSync(raw)),
788
- chunk("IEND", Buffer.alloc(0))
789
- ]);
790
- }
791
- function paletteSwatch(hexes, size = 64) {
792
- const colours = hexes.map(parseHex);
793
- if (!colours.length) throw new Error("palette is empty");
794
- const rgb = Buffer.alloc(size * size * 3);
795
- const band = Math.max(1, Math.floor(size / colours.length));
796
- for (let y = 0; y < size; y++) {
797
- for (let x = 0; x < size; x++) {
798
- const c = colours[Math.min(colours.length - 1, Math.floor(x / band))];
799
- const o = (y * size + x) * 3;
800
- rgb[o] = c.r;
801
- rgb[o + 1] = c.g;
802
- rgb[o + 2] = c.b;
1336
+ /** Storage URLs are public; no auth header, and sending one can break the CDN request. */
1337
+ async download(url) {
1338
+ const res = await fetch(url, { signal: AbortSignal.timeout(this.timeoutMs) });
1339
+ if (!res.ok) throw new PixelLabError(`download ${url} \u2192 ${res.status}`, res.status, "");
1340
+ const declared = Number(res.headers.get("content-length"));
1341
+ if (Number.isFinite(declared) && declared > MAX_DOWNLOAD_BYTES) {
1342
+ throw new Error(`download ${url} exceeds the ${MAX_DOWNLOAD_BYTES}-byte safety limit`);
1343
+ }
1344
+ const buf = Buffer.from(await res.arrayBuffer());
1345
+ if (buf.length > MAX_DOWNLOAD_BYTES) {
1346
+ throw new Error(`download ${url} exceeds the ${MAX_DOWNLOAD_BYTES}-byte safety limit`);
803
1347
  }
1348
+ return buf;
804
1349
  }
805
- return encodeRgbPng(size, size, rgb);
806
- }
807
- function parseHex(hex2) {
808
- const m = /^#?([0-9a-f]{6})$/i.exec(hex2.trim());
809
- if (!m) throw new Error(`invalid hex colour "${hex2}" \u2014 expected #rrggbb`);
810
- const n = parseInt(m[1], 16);
811
- return { r: n >> 16 & 255, g: n >> 8 & 255, b: n & 255 };
1350
+ };
1351
+ function clientFromEnv() {
1352
+ const key = process.env.PIXELLAB_API_KEY;
1353
+ if (!key) {
1354
+ throw new Error(
1355
+ "PIXELLAB_API_KEY is not set.\nLooked in the environment, and in .env.local / .env beside the manifest and in the current directory."
1356
+ );
1357
+ }
1358
+ return new PixelLabClient(key);
812
1359
  }
813
1360
 
814
1361
  // src/types.ts
@@ -853,8 +1400,8 @@ var StyleSchema = z2.object({
853
1400
  // `map` is the default because it is 20-40x cheaper and correct for any
854
1401
  // asset that is not going to be rotated or animated.
855
1402
  generator: GeneratorSchema.default("map"),
856
- /** Square edge length for `1dir`. 32-256. */
857
- size: z2.number().int().min(32).max(256).optional(),
1403
+ /** Square edge length. The active provider owns its exact limit. */
1404
+ size: z2.number().int().min(16).max(8192).optional(),
858
1405
  view: z2.string().optional(),
859
1406
  /** Appended to every asset prompt in this style. Where the look is defined. */
860
1407
  promptSuffix: z2.string().default(""),
@@ -970,8 +1517,8 @@ var AssetSchema = z2.object({
970
1517
  /** Subdirectory under the style's outDir. Optional. */
971
1518
  category: z2.string().optional(),
972
1519
  /** Overrides the style default. `map` generator only. */
973
- width: z2.number().int().min(16).max(400).optional(),
974
- height: z2.number().int().min(16).max(400).optional(),
1520
+ width: z2.number().int().min(16).max(8192).optional(),
1521
+ height: z2.number().int().min(16).max(8192).optional(),
975
1522
  /** Overrides the style default. `1dir` generator only. */
976
1523
  size: z2.number().int().min(32).max(256).optional(),
977
1524
  /** Explicit output path relative to outDir. Media-aware providers may replace its extension. */
@@ -1152,7 +1699,7 @@ var PixelLabProvider = class _PixelLabProvider {
1152
1699
  * filename, polling is an existence check, and downloading is a file read.
1153
1700
  */
1154
1701
  static cacheDir() {
1155
- const dir = path2.join(os.tmpdir(), "pixelkiln-pixflux");
1702
+ const dir = path3.join(os.tmpdir(), "pixelkiln-pixflux");
1156
1703
  mkdirSync(dir, { recursive: true });
1157
1704
  return dir;
1158
1705
  }
@@ -1167,6 +1714,12 @@ var PixelLabProvider = class _PixelLabProvider {
1167
1714
  };
1168
1715
  }
1169
1716
  validate(spec, styleImages) {
1717
+ if (spec.generator === "1dir" && (spec.width < 32 || spec.width > 256)) {
1718
+ throw new Error("PixelLab 1dir dimensions must be between 32 and 256 pixels");
1719
+ }
1720
+ if ((spec.generator === "map" || spec.generator === "pixflux") && (spec.width < 16 || spec.height < 16 || spec.width > 400 || spec.height > 400)) {
1721
+ throw new Error(`PixelLab ${spec.generator} dimensions must be between 16 and 400 pixels`);
1722
+ }
1170
1723
  if (spec.generator === "map") {
1171
1724
  requirePixelLabOption("view", spec.view, ["low top-down", "high top-down", "side"]);
1172
1725
  requirePixelLabOption("outline", spec.outline, [
@@ -1214,8 +1767,8 @@ var PixelLabProvider = class _PixelLabProvider {
1214
1767
  paletteSwatchBase64: swatch,
1215
1768
  seed: spec.seed
1216
1769
  });
1217
- const jobId = randomUUID();
1218
- writeFileSync(path2.join(_PixelLabProvider.cacheDir(), `${jobId}.png`), png);
1770
+ const jobId = randomUUID2();
1771
+ writeFileSync(path3.join(_PixelLabProvider.cacheDir(), `${jobId}.png`), png);
1219
1772
  return { jobId };
1220
1773
  }
1221
1774
  if (spec.generator === "tiles") {
@@ -1256,7 +1809,7 @@ var PixelLabProvider = class _PixelLabProvider {
1256
1809
  }
1257
1810
  async poll(jobId, generator, context) {
1258
1811
  if (generator === "pixflux") {
1259
- const file = path2.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
1812
+ const file = path3.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
1260
1813
  if (existsSync2(file)) {
1261
1814
  const sourceUrl = `file://${file}`;
1262
1815
  return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }] };
@@ -1402,95 +1955,10 @@ function firstUrl(urls) {
1402
1955
  return Object.values(urls).find((u) => typeof u === "string") ?? null;
1403
1956
  }
1404
1957
 
1405
- // src/media.ts
1406
- var MediaType = {
1407
- PNG: "image/png",
1408
- GIF: "image/gif"
1409
- };
1410
- var PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
1411
- function mediaExtension(mediaType) {
1412
- return mediaType === MediaType.GIF ? ".gif" : ".png";
1413
- }
1414
- function mediaTypeFromExtension(file) {
1415
- const lower = file.toLowerCase();
1416
- if (lower.endsWith(".png")) return MediaType.PNG;
1417
- if (lower.endsWith(".gif")) return MediaType.GIF;
1418
- return null;
1419
- }
1420
- function detectMediaType(bytes) {
1421
- if (bytes.subarray(0, PNG_SIGNATURE.length).equals(PNG_SIGNATURE)) return MediaType.PNG;
1422
- const header = bytes.subarray(0, 6).toString("ascii");
1423
- if (header === "GIF87a" || header === "GIF89a") return MediaType.GIF;
1424
- return null;
1425
- }
1426
- function validateMedia(bytes, expected) {
1427
- const actual = detectMediaType(bytes);
1428
- if (!actual) throw new Error(`response was not a supported PNG or GIF (${bytes.length} bytes)`);
1429
- if (expected && actual !== expected) {
1430
- throw new Error(`response was ${actual}, expected ${expected}`);
1431
- }
1432
- if (actual === MediaType.PNG) {
1433
- decodePng(bytes);
1434
- } else {
1435
- validateGif(bytes);
1436
- }
1437
- return actual;
1438
- }
1439
- function validateGif(bytes) {
1440
- if (bytes.length < 14) throw new Error("invalid GIF: truncated logical screen descriptor");
1441
- const width = bytes.readUInt16LE(6);
1442
- const height = bytes.readUInt16LE(8);
1443
- if (!width || !height) throw new Error("invalid GIF: zero-sized logical screen");
1444
- const packed = bytes[10];
1445
- let offset = 13;
1446
- if (packed & 128) offset += 3 * 2 ** ((packed & 7) + 1);
1447
- if (offset > bytes.length) throw new Error("invalid GIF: truncated global color table");
1448
- let sawImage = false;
1449
- while (offset < bytes.length) {
1450
- const marker = bytes[offset];
1451
- if (marker === 59) {
1452
- if (!sawImage) throw new Error("invalid GIF: contains no image frame");
1453
- return;
1454
- }
1455
- if (marker === 44) {
1456
- if (offset + 10 > bytes.length) throw new Error("invalid GIF: truncated image descriptor");
1457
- const imagePacked = bytes[offset + 9];
1458
- offset += 10;
1459
- if (imagePacked & 128) offset += 3 * 2 ** ((imagePacked & 7) + 1);
1460
- if (offset >= bytes.length) throw new Error("invalid GIF: missing image data");
1461
- offset++;
1462
- offset = skipSubBlocks(bytes, offset);
1463
- sawImage = true;
1464
- continue;
1465
- }
1466
- if (marker === 33) {
1467
- if (offset + 2 > bytes.length) throw new Error("invalid GIF: truncated extension");
1468
- offset = skipSubBlocks(bytes, offset + 2);
1469
- continue;
1470
- }
1471
- throw new Error(`invalid GIF: unexpected block marker 0x${marker.toString(16)}`);
1472
- }
1473
- throw new Error("invalid GIF: missing trailer");
1474
- }
1475
- function skipSubBlocks(bytes, start) {
1476
- let offset = start;
1477
- for (; ; ) {
1478
- if (offset >= bytes.length) throw new Error("invalid GIF: truncated data blocks");
1479
- const size = bytes[offset];
1480
- offset++;
1481
- if (size === 0) return offset;
1482
- offset += size;
1483
- if (offset > bytes.length) throw new Error("invalid GIF: truncated data block");
1484
- }
1485
- }
1486
- function cacheFileName(hash, mediaType = MediaType.PNG) {
1487
- return `${hash}${mediaExtension(mediaType)}`;
1488
- }
1489
-
1490
1958
  // src/providers/retrodiffusion.ts
1491
- var DEFAULT_BASE_URL = "https://api.retrodiffusion.ai/v1";
1959
+ var DEFAULT_BASE_URL2 = "https://api.retrodiffusion.ai/v1";
1492
1960
  var RetroDiffusionClient = class {
1493
- constructor(token, baseUrl = DEFAULT_BASE_URL, request = fetch) {
1961
+ constructor(token, baseUrl = DEFAULT_BASE_URL2, request = fetch) {
1494
1962
  this.token = token;
1495
1963
  this.baseUrl = baseUrl;
1496
1964
  this.request = request;
@@ -1528,9 +1996,9 @@ var RetroDiffusionClient = class {
1528
1996
  }
1529
1997
  return balance;
1530
1998
  }
1531
- async call(path19, init = {}) {
1999
+ async call(path20, init = {}) {
1532
2000
  if (!this.token) throw new Error("RD_API_KEY is not set");
1533
- const response = await this.request(`${this.baseUrl}${path19}`, {
2001
+ const response = await this.request(`${this.baseUrl}${path20}`, {
1534
2002
  ...init,
1535
2003
  headers: {
1536
2004
  "Content-Type": "application/json",
@@ -1837,56 +2305,23 @@ registerProvider({
1837
2305
  return RetroDiffusionProvider.forOffline();
1838
2306
  }
1839
2307
  });
2308
+ registerProvider({
2309
+ id: "comfyui",
2310
+ create(mode2) {
2311
+ if (mode2 === "online") return ComfyUIProvider.fromEnv();
2312
+ if (mode2 === "downloads") return ComfyUIProvider.forDownloads();
2313
+ return ComfyUIProvider.forOffline();
2314
+ }
2315
+ });
1840
2316
 
1841
2317
  // src/manifest.ts
1842
- import { readFile as readFile2 } from "fs/promises";
2318
+ import { readFile as readFile3 } from "fs/promises";
1843
2319
  import { existsSync as existsSync3 } from "fs";
1844
- import path3 from "path";
1845
-
1846
- // src/hash.ts
1847
- import { createHash } from "crypto";
1848
- import { readFile } from "fs/promises";
1849
- function sha256(data) {
1850
- return createHash("sha256").update(data).digest("hex");
1851
- }
1852
- async function sha256File(path19) {
1853
- return sha256(await readFile(path19));
1854
- }
1855
- function specHash(spec, styleImageHashes) {
1856
- return sha256(
1857
- JSON.stringify({
1858
- // Preserve every existing PixelLab hash while making a provider switch
1859
- // invalidate the spec. Older manifests implicitly mean pixellab.
1860
- provider: spec.provider === "pixellab" ? void 0 : spec.provider,
1861
- providerOptions: Object.keys(spec.providerOptions).length > 0 ? spec.providerOptions : void 0,
1862
- generator: spec.generator,
1863
- prompt: spec.prompt,
1864
- width: spec.width,
1865
- height: spec.height,
1866
- view: spec.view,
1867
- outline: spec.outline ?? null,
1868
- shading: spec.shading ?? null,
1869
- detail: spec.detail ?? null,
1870
- seed: spec.seed ?? null,
1871
- palette: spec.palette,
1872
- // `noBackground` only reaches the wire for pixflux; the tile fields are
1873
- // undefined for every other generator. `tileSize` is intentionally
1874
- // absent — width/height are derived from it, so it is already covered.
1875
- noBackground: spec.generator === "pixflux" || spec.provider !== "pixellab" ? spec.noBackground : void 0,
1876
- tileType: spec.tileType,
1877
- tileView: spec.tileView,
1878
- tileFeature: spec.tileFeature,
1879
- outlineMode: spec.outlineMode,
1880
- styleImages: styleImageHashes
1881
- })
1882
- );
1883
- }
1884
-
1885
- // src/manifest.ts
2320
+ import path4 from "path";
1886
2321
  async function loadManifest(manifestPath) {
1887
- const abs = path3.resolve(manifestPath);
2322
+ const abs = path4.resolve(manifestPath);
1888
2323
  if (!existsSync3(abs)) throw new Error(`No manifest at ${abs}`);
1889
- const parsed = ManifestSchema.safeParse(JSON.parse(await readFile2(abs, "utf8")));
2324
+ const parsed = ManifestSchema.safeParse(JSON.parse(await readFile3(abs, "utf8")));
1890
2325
  if (!parsed.success) {
1891
2326
  const issues = parsed.error.issues.map((i) => ` ${i.path.join(".")}: ${i.message}`).join("\n");
1892
2327
  throw new Error(`Manifest at ${abs} is invalid:
@@ -1908,7 +2343,7 @@ ${issues}`);
1908
2343
  throw new Error(`Manifest at ${abs} is invalid:
1909
2344
  ${unknownReferences.map((i) => ` ${i}`).join("\n")}`);
1910
2345
  }
1911
- return { manifest: parsed.data, root: path3.dirname(abs), path: abs };
2346
+ return { manifest: parsed.data, root: path4.dirname(abs), path: abs };
1912
2347
  }
1913
2348
  async function resolveSpecs(loaded, filter) {
1914
2349
  const { manifest, root } = loaded;
@@ -1931,11 +2366,11 @@ async function resolveSpecs(loaded, filter) {
1931
2366
  }
1932
2367
  const styleImageCache = /* @__PURE__ */ new Map();
1933
2368
  async function loadStyleImage(rel) {
1934
- const abs = path3.resolve(root, rel);
2369
+ const abs = path4.resolve(root, rel);
1935
2370
  let hit = styleImageCache.get(abs);
1936
2371
  if (!hit) {
1937
2372
  if (!existsSync3(abs)) throw new Error(`Style image not found: ${abs}`);
1938
- const buf = await readFile2(abs);
2373
+ const buf = await readFile3(abs);
1939
2374
  const metadata = imageMetadata(buf);
1940
2375
  if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${abs}`);
1941
2376
  if (metadata.width < 1 || metadata.height < 1) {
@@ -1948,6 +2383,10 @@ async function resolveSpecs(loaded, filter) {
1948
2383
  }
1949
2384
  for (const styleId of styleIds) {
1950
2385
  const style = manifest.styles[styleId];
2386
+ const rawProviderOptions = style.providerOptions[activeProvider.id] ?? {};
2387
+ const optionResolution = activeProvider.resolveOptions ? await activeProvider.resolveOptions(rawProviderOptions, { root, styleId }) : { options: rawProviderOptions };
2388
+ const providerOptions = optionResolution.options;
2389
+ const providerOptionIdentity = optionResolution.identity ?? providerOptions;
1951
2390
  const styleImageHashes = [];
1952
2391
  const styleImageDimensions = [];
1953
2392
  for (const img of style.styleImages) {
@@ -1980,15 +2419,15 @@ async function resolveSpecs(loaded, filter) {
1980
2419
  }
1981
2420
  const subject = asset.promptByStyle[styleId] ?? asset.prompt;
1982
2421
  const prompt = [style.promptPrefix, subject, style.promptSuffix].map((p) => p.trim()).filter(Boolean).join(", ");
1983
- const relFile = asset.file ?? path3.join(asset.category ?? "", `${assetId}.png`);
1984
- const outFile = path3.resolve(root, style.outDir, relFile);
2422
+ const relFile = asset.file ?? path4.join(asset.category ?? "", `${assetId}.png`);
2423
+ const outFile = path4.resolve(root, style.outDir, relFile);
1985
2424
  const tileSize = generator === "tiles" ? size : style.tileSize ?? 32;
1986
2425
  const tileVariations = tileFeatureOutputCount(generator === "tiles" ? style.tileFeature : void 0) ?? tileVariationCount(countNumberedDescriptions(prompt));
1987
2426
  const base = {
1988
2427
  styleId,
1989
2428
  assetId,
1990
2429
  provider: activeProvider.id,
1991
- providerOptions: style.providerOptions[activeProvider.id] ?? {},
2430
+ providerOptions,
1992
2431
  generator,
1993
2432
  prompt,
1994
2433
  width,
@@ -2026,10 +2465,10 @@ async function resolveSpecs(loaded, filter) {
2026
2465
  outFile,
2027
2466
  tags,
2028
2467
  source: asset.source,
2029
- specHash: specHash(base, styleImageHashes)
2468
+ specHash: specHash(base, styleImageHashes, providerOptionIdentity)
2030
2469
  };
2031
2470
  const resolvedImages = style.styleImages.map((image) => {
2032
- const hit = styleImageCache.get(path3.resolve(root, image.path));
2471
+ const hit = styleImageCache.get(path4.resolve(root, image.path));
2033
2472
  return { base64: hit.base64, width: hit.width, height: hit.height, format: hit.format };
2034
2473
  });
2035
2474
  activeProvider.validate?.(resolved, resolvedImages);
@@ -2056,8 +2495,8 @@ async function resolveStyleImages(loaded, styleId) {
2056
2495
  if (!style) throw new Error(`Unknown style "${styleId}"`);
2057
2496
  const out = [];
2058
2497
  for (const img of style.styleImages) {
2059
- const file = path3.resolve(loaded.root, img.path);
2060
- const buf = await readFile2(file);
2498
+ const file = path4.resolve(loaded.root, img.path);
2499
+ const buf = await readFile3(file);
2061
2500
  const metadata = imageMetadata(buf);
2062
2501
  if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${file}`);
2063
2502
  if (metadata.width < 1 || metadata.height < 1) {
@@ -2098,19 +2537,19 @@ function imageMetadata(buf) {
2098
2537
 
2099
2538
  // src/pipeline/pack.ts
2100
2539
  import { readFileSync as readFileSync3 } from "fs";
2101
- import path6 from "path";
2540
+ import path7 from "path";
2102
2541
 
2103
2542
  // src/outputs.ts
2104
- import path5 from "path";
2543
+ import path6 from "path";
2105
2544
 
2106
2545
  // src/lock.ts
2107
- import { readFile as readFile3, writeFile, rename, mkdir, rmdir, rm, stat } from "fs/promises";
2546
+ import { readFile as readFile4, writeFile, rename, mkdir, rmdir, rm, stat } from "fs/promises";
2108
2547
  import { existsSync as existsSync4 } from "fs";
2109
- import path4 from "path";
2548
+ import path5 from "path";
2110
2549
  async function loadLock(lockPath) {
2111
2550
  if (!existsSync4(lockPath)) return { version: 2, entries: {} };
2112
2551
  try {
2113
- return parseLock(JSON.parse(await readFile3(lockPath, "utf8")));
2552
+ return parseLock(JSON.parse(await readFile4(lockPath, "utf8")));
2114
2553
  } catch (err) {
2115
2554
  throw new Error(
2116
2555
  `Lockfile at ${lockPath} is malformed:
@@ -2122,7 +2561,7 @@ var saveQueues = /* @__PURE__ */ new Map();
2122
2561
  var dirtyPatches = /* @__PURE__ */ new WeakMap();
2123
2562
  var dirtyDeletes = /* @__PURE__ */ new WeakMap();
2124
2563
  function saveLock(lockPath, lock) {
2125
- const queueKey = path4.resolve(lockPath);
2564
+ const queueKey = path5.resolve(lockPath);
2126
2565
  const previous = saveQueues.get(queueKey) ?? Promise.resolve();
2127
2566
  const next = previous.catch(() => {
2128
2567
  }).then(() => writeLockNow(lockPath, lock));
@@ -2134,7 +2573,7 @@ function saveLock(lockPath, lock) {
2134
2573
  return next;
2135
2574
  }
2136
2575
  async function writeLockNow(lockPath, lock) {
2137
- await mkdir(path4.dirname(path4.resolve(lockPath)), { recursive: true });
2576
+ await mkdir(path5.dirname(path5.resolve(lockPath)), { recursive: true });
2138
2577
  const release = await acquireFileLock(lockPath);
2139
2578
  try {
2140
2579
  await writeLockWhileHeld(lockPath, lock);
@@ -2146,7 +2585,7 @@ async function writeLockWhileHeld(file, lock) {
2146
2585
  let disk = { version: 2, entries: {} };
2147
2586
  if (existsSync4(file)) {
2148
2587
  try {
2149
- disk = parseLock(JSON.parse(await readFile3(file, "utf8")));
2588
+ disk = parseLock(JSON.parse(await readFile4(file, "utf8")));
2150
2589
  } catch (err) {
2151
2590
  throw new Error(
2152
2591
  `Refusing to overwrite malformed lockfile at ${file}: ${err instanceof Error ? err.message : String(err)}`
@@ -2275,19 +2714,19 @@ function spendByUnit(lock) {
2275
2714
 
2276
2715
  // src/outputs.ts
2277
2716
  function portableOutputPath(file, manifestDir) {
2278
- const root = path5.resolve(manifestDir);
2279
- const absolute = path5.isAbsolute(file) ? path5.normalize(file) : path5.resolve(root, file);
2280
- const relative = path5.relative(root, absolute);
2281
- const value = path5.isAbsolute(relative) ? absolute : relative;
2282
- return value.split(path5.sep).join("/");
2717
+ const root = path6.resolve(manifestDir);
2718
+ const absolute = path6.isAbsolute(file) ? path6.normalize(file) : path6.resolve(root, file);
2719
+ const relative = path6.relative(root, absolute);
2720
+ const value = path6.isAbsolute(relative) ? absolute : relative;
2721
+ return value.split(path6.sep).join("/");
2283
2722
  }
2284
2723
  function resolveOutputPath(recordedPath, manifestDir) {
2285
- if (path5.isAbsolute(recordedPath)) return path5.normalize(recordedPath);
2286
- if (path5.win32.isAbsolute(recordedPath)) return recordedPath;
2287
- return path5.resolve(manifestDir, recordedPath.split(/[\\/]/).join(path5.sep));
2724
+ if (path6.isAbsolute(recordedPath)) return path6.normalize(recordedPath);
2725
+ if (path6.win32.isAbsolute(recordedPath)) return recordedPath;
2726
+ return path6.resolve(manifestDir, recordedPath.split(/[\\/]/).join(path6.sep));
2288
2727
  }
2289
2728
  function expectedOutputPath(spec, role, index, total, mediaType) {
2290
- const originalExt = path5.extname(spec.outFile);
2729
+ const originalExt = path6.extname(spec.outFile);
2291
2730
  const ext = mediaType ? mediaExtension(mediaType) : originalExt || ".png";
2292
2731
  const stem = originalExt ? spec.outFile.slice(0, -originalExt.length) : spec.outFile;
2293
2732
  if (total === 1) return `${stem}${ext}`;
@@ -2370,7 +2809,7 @@ function resolveSpecOutputs(spec, lock, manifestDir) {
2370
2809
  id: spec.assetId,
2371
2810
  index: 0,
2372
2811
  path: spec.outFile,
2373
- absolutePath: path5.resolve(spec.outFile),
2812
+ absolutePath: path6.resolve(spec.outFile),
2374
2813
  sha256: ""
2375
2814
  }];
2376
2815
  }
@@ -2380,13 +2819,13 @@ function resolvePackInputs(raw, inputsFilePath) {
2380
2819
  if (!Array.isArray(raw) || !raw.length) {
2381
2820
  throw new Error("--inputs must be a non-empty JSON array of { id, path }");
2382
2821
  }
2383
- const baseDir = path6.dirname(path6.resolve(inputsFilePath));
2822
+ const baseDir = path7.dirname(path7.resolve(inputsFilePath));
2384
2823
  return raw.map((entry, i) => {
2385
2824
  const e = entry;
2386
2825
  if (typeof e.id !== "string" || typeof e.path !== "string") {
2387
2826
  throw new Error(`--inputs[${i}] needs string "id" and "path"`);
2388
2827
  }
2389
- return { id: e.id, path: path6.resolve(baseDir, e.path) };
2828
+ return { id: e.id, path: path7.resolve(baseDir, e.path) };
2390
2829
  });
2391
2830
  }
2392
2831
  function packSprites(inputs, options = {}) {
@@ -2583,7 +3022,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
2583
3022
  const cell = cells[id];
2584
3023
  const source = sources[id];
2585
3024
  if (source) {
2586
- placements.push({ id, path: path6.resolve(manifestDir, source), cell });
3025
+ placements.push({ id, path: path7.resolve(manifestDir, source), cell });
2587
3026
  continue;
2588
3027
  }
2589
3028
  const entry = lock.entries[`${styleId}/${id}`];
@@ -2607,7 +3046,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
2607
3046
  `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}`
2608
3047
  );
2609
3048
  }
2610
- const basePng = mount.base ? readFileSync3(path6.resolve(manifestDir, mount.base)) : void 0;
3049
+ const basePng = mount.base ? readFileSync3(path7.resolve(manifestDir, mount.base)) : void 0;
2611
3050
  const mounted = mountSprites(placements, {
2612
3051
  cellWidth: mount.cellWidth,
2613
3052
  cellHeight: mount.cellHeight,
@@ -2617,7 +3056,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
2617
3056
  ...mounted,
2618
3057
  sources: mount.base ? [{
2619
3058
  id: "$base",
2620
- path: path6.resolve(manifestDir, mount.base),
3059
+ path: path7.resolve(manifestDir, mount.base),
2621
3060
  sha256: sha256(basePng),
2622
3061
  included: true
2623
3062
  }, ...mounted.sources] : mounted.sources,
@@ -2628,7 +3067,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
2628
3067
 
2629
3068
  // src/pipeline/plan.ts
2630
3069
  import { existsSync as existsSync5 } from "fs";
2631
- import path7 from "path";
3070
+ import path8 from "path";
2632
3071
  async function anyOutputModified(entry, spec) {
2633
3072
  for (let index = 0; index < entry.outputs.length; index++) {
2634
3073
  const output = entry.outputs[index];
@@ -2647,7 +3086,7 @@ async function buildPlan(specs, lock, opts = {}) {
2647
3086
  state = "missing";
2648
3087
  reason = "--force";
2649
3088
  } else if (!entry && spec.source) {
2650
- if (existsSync5(path7.resolve(spec.root, spec.source))) {
3089
+ if (existsSync5(path8.resolve(spec.root, spec.source))) {
2651
3090
  state = "ok";
2652
3091
  reason = `placed from ${spec.source}; not generated`;
2653
3092
  } else {
@@ -2859,7 +3298,11 @@ async function poll(provider, lock, lockPath, opts = {}) {
2859
3298
  spec: currentSpec
2860
3299
  });
2861
3300
  if (state.status === "review") {
2862
- upsert(lock, key, { status: "review", reviewObjectId: entry.jobId });
3301
+ upsert(lock, key, {
3302
+ status: "review",
3303
+ reviewObjectId: entry.jobId,
3304
+ providerMetadata: state.metadata ? { ...entry.providerMetadata, [provider.id]: state.metadata } : entry.providerMetadata
3305
+ });
2863
3306
  result.review++;
2864
3307
  log2(` review ${key} (${state.candidateUrls.length} candidates)`);
2865
3308
  } else if (state.status === "ready") {
@@ -2892,15 +3335,15 @@ async function poll(provider, lock, lockPath, opts = {}) {
2892
3335
 
2893
3336
  // src/pipeline/fetch.ts
2894
3337
  import { existsSync as existsSync6 } from "fs";
2895
- import { mkdir as mkdir2, readFile as readFile4, rename as rename2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
2896
- import path8 from "path";
3338
+ import { mkdir as mkdir2, readFile as readFile5, rename as rename2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
3339
+ import path9 from "path";
2897
3340
  async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
2898
3341
  const log2 = opts.onProgress ?? (() => {
2899
3342
  });
2900
3343
  const result = { downloaded: 0, skipped: 0, failed: 0 };
2901
3344
  const specByKey = new Map(specs.map((s) => [lockKey(s.styleId, s.assetId), s]));
2902
3345
  normalizeLockOutputPaths(lock, specs);
2903
- const cacheDir = opts.cacheDir === false ? null : path8.resolve(opts.cacheDir ?? path8.join(path8.dirname(lockPath), ".pixelkiln", "cache"));
3346
+ const cacheDir = opts.cacheDir === false ? null : path9.resolve(opts.cacheDir ?? path9.join(path9.dirname(lockPath), ".pixelkiln", "cache"));
2904
3347
  const pending = Object.entries(lock.entries).filter(([key, e]) => {
2905
3348
  if (e.status === "selected" || e.status === "download-failed") return true;
2906
3349
  if (!opts.repair || e.status !== "downloaded") return false;
@@ -2952,7 +3395,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
2952
3395
  if (cacheDir) {
2953
3396
  await cacheMedia(
2954
3397
  cacheDir,
2955
- await readFile4(target),
3398
+ await readFile5(target),
2956
3399
  recorded.mediaType ?? MediaType.PNG,
2957
3400
  recorded.sha256
2958
3401
  );
@@ -2963,7 +3406,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
2963
3406
  const expectedMediaType = source.mediaType ?? recorded?.mediaType ?? MediaType.PNG;
2964
3407
  let buf = recorded && cacheDir ? await readCachedMedia(cacheDir, recorded.sha256, expectedMediaType) : null;
2965
3408
  if (buf) {
2966
- log2(` cached ${path8.relative(process.cwd(), target)}`);
3409
+ log2(` cached ${path9.relative(process.cwd(), target)}`);
2967
3410
  } else {
2968
3411
  if (!source.url) {
2969
3412
  throw new Error(`no source URL or cached bytes remain for ${source.role ?? "asset"}`);
@@ -2981,7 +3424,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
2981
3424
  );
2982
3425
  }
2983
3426
  if (cacheDir) await cacheMedia(cacheDir, buf, mediaType);
2984
- await mkdir2(path8.dirname(target), { recursive: true });
3427
+ await mkdir2(path9.dirname(target), { recursive: true });
2985
3428
  const tmp = `${target}.pixelkiln.tmp`;
2986
3429
  await writeFile2(tmp, buf);
2987
3430
  outputs.push({
@@ -2993,7 +3436,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
2993
3436
  upsert(lock, key, { outputs: mergeOutputs(entry.outputs, outputs) });
2994
3437
  await saveLock(lockPath, lock);
2995
3438
  await rename2(tmp, target);
2996
- log2(` wrote ${path8.relative(process.cwd(), target)}`);
3439
+ log2(` wrote ${path9.relative(process.cwd(), target)}`);
2997
3440
  }
2998
3441
  upsert(lock, key, {
2999
3442
  status: "downloaded",
@@ -3026,10 +3469,10 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
3026
3469
  return result;
3027
3470
  }
3028
3471
  async function readCachedMedia(cacheDir, hash, mediaType) {
3029
- const file = path8.join(cacheDir, cacheFileName(hash, mediaType));
3472
+ const file = path9.join(cacheDir, cacheFileName(hash, mediaType));
3030
3473
  if (!existsSync6(file)) return null;
3031
3474
  try {
3032
- const buf = await readFile4(file);
3475
+ const buf = await readFile5(file);
3033
3476
  if (sha256(buf) !== hash) return null;
3034
3477
  validateMedia(buf, mediaType);
3035
3478
  return buf;
@@ -3040,7 +3483,7 @@ async function readCachedMedia(cacheDir, hash, mediaType) {
3040
3483
  async function cacheMedia(cacheDir, buf, mediaType, knownHash) {
3041
3484
  const hash = knownHash ?? sha256(buf);
3042
3485
  validateMedia(buf, mediaType);
3043
- const file = path8.join(cacheDir, cacheFileName(hash, mediaType));
3486
+ const file = path9.join(cacheDir, cacheFileName(hash, mediaType));
3044
3487
  if (await readCachedMedia(cacheDir, hash, mediaType)) return;
3045
3488
  await mkdir2(cacheDir, { recursive: true });
3046
3489
  const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
@@ -3083,7 +3526,7 @@ async function pushTags(provider, specs, lock, opts = {}) {
3083
3526
  // src/pipeline/doctor.ts
3084
3527
  import { constants, existsSync as existsSync7 } from "fs";
3085
3528
  import { access, stat as stat2 } from "fs/promises";
3086
- import path9 from "path";
3529
+ import path10 from "path";
3087
3530
  async function doctor(loaded, specs, lock, lockPath, opts = {}) {
3088
3531
  const checks = [];
3089
3532
  const add = (id, level, message2) => checks.push({ id, level, message: message2 });
@@ -3093,7 +3536,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
3093
3536
  `${Object.keys(loaded.manifest.styles).length} style(s), ${Object.keys(loaded.manifest.assets).length} asset(s), ${specs.length} resolved spec(s)`
3094
3537
  );
3095
3538
  add("lock", "ok", `${Object.keys(lock.entries).length} valid v2 lock entr(ies)`);
3096
- const dirs = [...new Set(specs.map((s) => path9.dirname(s.outFile)))];
3539
+ const dirs = [...new Set(specs.map((s) => path10.dirname(s.outFile)))];
3097
3540
  const unwritable = [];
3098
3541
  for (const dir of dirs) {
3099
3542
  const ancestor = await nearestExistingDirectory(dir);
@@ -3115,7 +3558,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
3115
3558
  for (let index = 0; index < entry.outputs.length; index++) {
3116
3559
  const output = entry.outputs[index];
3117
3560
  const spec = specByKey.get(key);
3118
- const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, path9.dirname(lockPath));
3561
+ const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, path10.dirname(lockPath));
3119
3562
  const owner = outputOwners.get(absolute);
3120
3563
  if (owner && owner !== key) duplicateOutputs.push(`${output.path} (${owner}, ${key})`);
3121
3564
  outputOwners.set(absolute, key);
@@ -3130,7 +3573,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
3130
3573
  } else {
3131
3574
  add("lock-outputs", "ok", `${outputOwners.size} recorded output path(s) are uniquely owned`);
3132
3575
  }
3133
- const cacheDir = path9.join(path9.dirname(lockPath), ".pixelkiln", "cache");
3576
+ const cacheDir = path10.join(path10.dirname(lockPath), ".pixelkiln", "cache");
3134
3577
  const stranded = [];
3135
3578
  const invalidState = [];
3136
3579
  const staleMapUrls = [];
@@ -3146,7 +3589,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
3146
3589
  const hasSource = Boolean(entry.sourceUrl || entry.sourceUrls?.length);
3147
3590
  let hasCache = false;
3148
3591
  for (const output of entry.outputs) {
3149
- const cached = path9.join(
3592
+ const cached = path10.join(
3150
3593
  cacheDir,
3151
3594
  cacheFileName(output.sha256, output.mediaType ?? MediaType.PNG)
3152
3595
  );
@@ -3181,8 +3624,15 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
3181
3624
  "error",
3182
3625
  opts.apiKeyPresent === false ? `${opts.credentialEnv ?? "PIXELLAB_API_KEY"} is not configured` : "provider is not configured"
3183
3626
  );
3627
+ } else if (!opts.provider.balance && !opts.provider.checkConnection) {
3628
+ add("provider", "ok", `${opts.provider.id} configured; connectivity check unavailable`);
3184
3629
  } else if (!opts.provider.balance) {
3185
- add("provider", "ok", `${opts.provider.id} configured; balance reporting unavailable`);
3630
+ try {
3631
+ await opts.provider.checkConnection();
3632
+ add("provider", "ok", `${opts.provider.id} reachable; balance reporting unavailable`);
3633
+ } catch (err) {
3634
+ add("provider", "error", `provider connectivity failed: ${err instanceof Error ? err.message : String(err)}`);
3635
+ }
3186
3636
  } else {
3187
3637
  const balanceFn = opts.provider.balance.bind(opts.provider);
3188
3638
  try {
@@ -3201,7 +3651,7 @@ async function nearestExistingDirectory(start) {
3201
3651
  if ((await stat2(current)).isDirectory()) return current;
3202
3652
  } catch {
3203
3653
  }
3204
- const parent = path9.dirname(current);
3654
+ const parent = path10.dirname(current);
3205
3655
  if (parent === current) return current;
3206
3656
  current = parent;
3207
3657
  }
@@ -3209,11 +3659,11 @@ async function nearestExistingDirectory(start) {
3209
3659
 
3210
3660
  // src/pipeline/adopt.ts
3211
3661
  import { existsSync as existsSync9 } from "fs";
3212
- import { readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
3213
- import path10 from "path";
3662
+ import { readFile as readFile7, writeFile as writeFile4 } from "fs/promises";
3663
+ import path11 from "path";
3214
3664
 
3215
3665
  // src/cache.ts
3216
- import { mkdir as mkdir3, readFile as readFile5, rename as rename3, rm as rm3, writeFile as writeFile3 } from "fs/promises";
3666
+ import { mkdir as mkdir3, readFile as readFile6, rename as rename3, rm as rm3, writeFile as writeFile3 } from "fs/promises";
3217
3667
  import { existsSync as existsSync8 } from "fs";
3218
3668
  import pathModule from "path";
3219
3669
  import { z as z3 } from "zod";
@@ -3229,10 +3679,10 @@ function isSha256Hash(value) {
3229
3679
  function parseCache(value) {
3230
3680
  return HashCacheSchema.parse(value);
3231
3681
  }
3232
- async function loadCache(path19) {
3233
- if (!existsSync8(path19)) return { version: 1, hashes: {} };
3682
+ async function loadCache(path20) {
3683
+ if (!existsSync8(path20)) return { version: 1, hashes: {} };
3234
3684
  try {
3235
- const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile5(path19, "utf8")));
3685
+ const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile6(path20, "utf8")));
3236
3686
  if (!parsed.success) return { version: 1, hashes: {} };
3237
3687
  return {
3238
3688
  version: 1,
@@ -3244,18 +3694,18 @@ async function loadCache(path19) {
3244
3694
  return { version: 1, hashes: {} };
3245
3695
  }
3246
3696
  }
3247
- async function saveCache(path19, cache) {
3697
+ async function saveCache(path20, cache) {
3248
3698
  const sorted = {};
3249
3699
  for (const key of Object.keys(cache.hashes).sort()) {
3250
3700
  const hash = cache.hashes[key];
3251
3701
  if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
3252
3702
  sorted[key] = hash;
3253
3703
  }
3254
- await mkdir3(pathModule.dirname(pathModule.resolve(path19)), { recursive: true });
3255
- const tmp = `${path19}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
3704
+ await mkdir3(pathModule.dirname(pathModule.resolve(path20)), { recursive: true });
3705
+ const tmp = `${path20}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
3256
3706
  try {
3257
3707
  await writeFile3(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
3258
- await rename3(tmp, path19);
3708
+ await rename3(tmp, path20);
3259
3709
  } finally {
3260
3710
  await rm3(tmp, { force: true });
3261
3711
  }
@@ -3290,7 +3740,7 @@ async function adopt(provider, specs, lock, lockPath, opts = {}) {
3290
3740
  unmatchedLocal.push(`${spec.styleId}/${spec.assetId} (no file at ${spec.outFile})`);
3291
3741
  continue;
3292
3742
  }
3293
- const bytes = await readFile6(spec.outFile);
3743
+ const bytes = await readFile7(spec.outFile);
3294
3744
  try {
3295
3745
  decodePng(bytes);
3296
3746
  } catch (err) {
@@ -3407,7 +3857,7 @@ async function tagAdopted(provider, specs, lock, opts = {}) {
3407
3857
  async function writePromptsBack(manifestPath, lock, opts = {}) {
3408
3858
  const log2 = opts.onProgress ?? (() => {
3409
3859
  });
3410
- const raw = JSON.parse(await readFile6(manifestPath, "utf8"));
3860
+ const raw = JSON.parse(await readFile7(manifestPath, "utf8"));
3411
3861
  const best = /* @__PURE__ */ new Map();
3412
3862
  for (const entry of Object.values(lock.entries)) {
3413
3863
  if (!entry.objectId || !entry.prompt) continue;
@@ -3797,9 +4247,9 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
3797
4247
  }
3798
4248
 
3799
4249
  // src/pipeline/init.ts
3800
- import { readdir, readFile as readFile7, writeFile as writeFile5 } from "fs/promises";
4250
+ import { readdir, readFile as readFile8, writeFile as writeFile5 } from "fs/promises";
3801
4251
  import { existsSync as existsSync10 } from "fs";
3802
- import path11 from "path";
4252
+ import path12 from "path";
3803
4253
  function pngSize(buf) {
3804
4254
  if (buf.length < 24) return null;
3805
4255
  const isPng = buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834;
@@ -3813,7 +4263,7 @@ function slugify(value) {
3813
4263
  async function walk(dir, exclude) {
3814
4264
  const out = [];
3815
4265
  for (const entry of await readdir(dir, { withFileTypes: true })) {
3816
- const full = path11.join(dir, entry.name);
4266
+ const full = path12.join(dir, entry.name);
3817
4267
  if (exclude.some((x) => full.includes(x))) continue;
3818
4268
  if (entry.isDirectory()) out.push(...await walk(full, exclude));
3819
4269
  else if (entry.name.toLowerCase().endsWith(".png")) out.push(full);
@@ -3827,14 +4277,14 @@ async function scanAssets(root, opts = {}) {
3827
4277
  const skipped = [];
3828
4278
  const seen = /* @__PURE__ */ new Map();
3829
4279
  for (const file of files.sort()) {
3830
- const size = pngSize(await readFile7(file));
4280
+ const size = pngSize(await readFile8(file));
3831
4281
  if (!size) {
3832
4282
  skipped.push(`${file} (not a readable PNG)`);
3833
4283
  continue;
3834
4284
  }
3835
- const rel = path11.relative(root, file);
3836
- const category = path11.dirname(rel) === "." ? "" : path11.dirname(rel);
3837
- let id = slugify(path11.basename(rel));
4285
+ const rel = path12.relative(root, file);
4286
+ const category = path12.dirname(rel) === "." ? "" : path12.dirname(rel);
4287
+ let id = slugify(path12.basename(rel));
3838
4288
  if (seen.has(id)) {
3839
4289
  const n = seen.get(id) + 1;
3840
4290
  seen.set(id, n);
@@ -3896,16 +4346,16 @@ async function writeManifestFile(target, manifest) {
3896
4346
  }
3897
4347
 
3898
4348
  // src/pipeline/salvage.ts
3899
- import { readFile as readFile8 } from "fs/promises";
4349
+ import { readFile as readFile9 } from "fs/promises";
3900
4350
  import { existsSync as existsSync11 } from "fs";
3901
- import path12 from "path";
4351
+ import path13 from "path";
3902
4352
  async function loadClaims(lockPaths) {
3903
4353
  const claimed = /* @__PURE__ */ new Set();
3904
4354
  for (const p of lockPaths) {
3905
4355
  if (!existsSync11(p)) throw new Error(`Claim lockfile not found: ${p}`);
3906
4356
  let parsed;
3907
4357
  try {
3908
- parsed = parseLock(JSON.parse(await readFile8(p, "utf8")));
4358
+ parsed = parseLock(JSON.parse(await readFile9(p, "utf8")));
3909
4359
  } catch {
3910
4360
  throw new Error(`Claim lockfile is malformed: ${p}`);
3911
4361
  }
@@ -3947,20 +4397,20 @@ function matchStyleByPattern(prompt, manifest) {
3947
4397
  return null;
3948
4398
  }
3949
4399
  async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, claimPaths) {
3950
- const own = path12.resolve(ownManifestPath);
4400
+ const own = path13.resolve(ownManifestPath);
3951
4401
  const siblingManifestPaths = [
3952
4402
  .../* @__PURE__ */ new Set([
3953
4403
  ...workspaceManifestPaths,
3954
- ...claimPaths.map((c) => path12.join(path12.dirname(path12.resolve(c)), "pixelkiln.manifest.json"))
4404
+ ...claimPaths.map((c) => path13.join(path13.dirname(path13.resolve(c)), "pixelkiln.manifest.json"))
3955
4405
  ])
3956
4406
  ];
3957
4407
  const siblings = [];
3958
4408
  for (const siblingManifestPath of siblingManifestPaths) {
3959
- if (path12.resolve(siblingManifestPath) === own) continue;
4409
+ if (path13.resolve(siblingManifestPath) === own) continue;
3960
4410
  if (!existsSync11(siblingManifestPath)) continue;
3961
4411
  try {
3962
4412
  const { manifest } = await loadManifest(siblingManifestPath);
3963
- siblings.push({ label: path12.basename(path12.dirname(siblingManifestPath)), manifest });
4413
+ siblings.push({ label: path13.basename(path13.dirname(siblingManifestPath)), manifest });
3964
4414
  } catch {
3965
4415
  }
3966
4416
  }
@@ -4076,9 +4526,9 @@ async function applyTags(provider, decisions, existing, opts = {}) {
4076
4526
  }
4077
4527
 
4078
4528
  // src/workspace.ts
4079
- import { mkdir as mkdir4, readFile as readFile9, rename as rename4, rm as rm4, writeFile as writeFile6 } from "fs/promises";
4529
+ import { mkdir as mkdir4, readFile as readFile10, rename as rename4, rm as rm4, writeFile as writeFile6 } from "fs/promises";
4080
4530
  import { existsSync as existsSync12 } from "fs";
4081
- import path13 from "path";
4531
+ import path14 from "path";
4082
4532
  import { z as z4 } from "zod";
4083
4533
  var WorkspaceProjectSchema = z4.object({
4084
4534
  id: z4.string().min(1),
@@ -4106,7 +4556,7 @@ async function loadWorkspace(workspacePath) {
4106
4556
  if (!existsSync12(workspacePath)) return { version: 1, projects: [] };
4107
4557
  let raw;
4108
4558
  try {
4109
- raw = JSON.parse(await readFile9(workspacePath, "utf8"));
4559
+ raw = JSON.parse(await readFile10(workspacePath, "utf8"));
4110
4560
  } catch (err) {
4111
4561
  throw new Error(
4112
4562
  `Workspace catalog at ${workspacePath} is malformed:
@@ -4120,7 +4570,7 @@ async function saveWorkspace(workspacePath, ws) {
4120
4570
  version: 1,
4121
4571
  projects: [...ws.projects].sort((a, b) => a.id.localeCompare(b.id))
4122
4572
  };
4123
- await mkdir4(path13.dirname(path13.resolve(workspacePath)), { recursive: true });
4573
+ await mkdir4(path14.dirname(path14.resolve(workspacePath)), { recursive: true });
4124
4574
  const tmp = `${workspacePath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
4125
4575
  try {
4126
4576
  await writeFile6(tmp, JSON.stringify(sorted, null, 2) + "\n");
@@ -4130,12 +4580,12 @@ async function saveWorkspace(workspacePath, ws) {
4130
4580
  }
4131
4581
  }
4132
4582
  function toPortablePath(dir, absolute) {
4133
- return path13.relative(dir, absolute).split(path13.sep).join("/");
4583
+ return path14.relative(dir, absolute).split(path14.sep).join("/");
4134
4584
  }
4135
4585
  function resolveProject(dir, project) {
4136
4586
  return {
4137
- manifestPath: path13.resolve(dir, project.manifest.split("/").join(path13.sep)),
4138
- lockPath: path13.resolve(dir, project.lock.split("/").join(path13.sep))
4587
+ manifestPath: path14.resolve(dir, project.manifest.split("/").join(path14.sep)),
4588
+ lockPath: path14.resolve(dir, project.lock.split("/").join(path14.sep))
4139
4589
  };
4140
4590
  }
4141
4591
  function validateWorkspace(ws, dir) {
@@ -4148,7 +4598,7 @@ function validateWorkspace(ws, dir) {
4148
4598
  const { manifestPath, lockPath } = resolveProject(dir, project);
4149
4599
  lockOwners.set(lockPath, [...lockOwners.get(lockPath) ?? [], project.id]);
4150
4600
  manifestOwners.set(manifestPath, [...manifestOwners.get(manifestPath) ?? [], project.id]);
4151
- if (path13.isAbsolute(project.manifest) || path13.isAbsolute(project.lock)) {
4601
+ if (path14.isAbsolute(project.manifest) || path14.isAbsolute(project.lock)) {
4152
4602
  diagnostics.push({
4153
4603
  id: "absolute-path",
4154
4604
  level: "warning",
@@ -4303,9 +4753,9 @@ async function workspaceStatus(ws, dir) {
4303
4753
  }
4304
4754
 
4305
4755
  // src/pipeline/audit.ts
4306
- import { readFile as readFile10 } from "fs/promises";
4756
+ import { readFile as readFile11 } from "fs/promises";
4307
4757
  import { existsSync as existsSync13 } from "fs";
4308
- import path14 from "path";
4758
+ import path15 from "path";
4309
4759
  function colorDistance(a, b) {
4310
4760
  const rmean = (a.r + b.r) / 2;
4311
4761
  const dr = a.r - b.r;
@@ -4355,7 +4805,7 @@ async function auditStyle(loaded, specs, styleId, lock) {
4355
4805
  continue;
4356
4806
  }
4357
4807
  try {
4358
- const png = decodePng(await readFile10(output.absolutePath));
4808
+ const png = decodePng(await readFile11(output.absolutePath));
4359
4809
  const palette = extractPalette(png, 12);
4360
4810
  assets.push({
4361
4811
  assetId: spec.assetId,
@@ -4379,10 +4829,10 @@ async function auditStyle(loaded, specs, styleId, lock) {
4379
4829
  let referenceFromStyleImages = false;
4380
4830
  const refPalettes = [];
4381
4831
  for (const img of style.styleImages) {
4382
- const abs = path14.resolve(loaded.root, img.path);
4832
+ const abs = path15.resolve(loaded.root, img.path);
4383
4833
  if (!existsSync13(abs)) continue;
4384
4834
  try {
4385
- refPalettes.push(extractPalette(decodePng(await readFile10(abs)), 12));
4835
+ refPalettes.push(extractPalette(decodePng(await readFile11(abs)), 12));
4386
4836
  } catch {
4387
4837
  }
4388
4838
  }
@@ -4459,14 +4909,14 @@ function hex(c) {
4459
4909
 
4460
4910
  // src/pipeline/cache-health.ts
4461
4911
  import { existsSync as existsSync14 } from "fs";
4462
- import { readFile as readFile11, readdir as readdir2, rm as rm5 } from "fs/promises";
4463
- import path15 from "path";
4912
+ import { readFile as readFile12, readdir as readdir2, rm as rm5 } from "fs/promises";
4913
+ import path16 from "path";
4464
4914
  async function inspectCaches(lock, lockPath, options = {}) {
4465
4915
  if (options.prune && !existsSync14(lockPath)) {
4466
- throw new Error(`Refusing to prune without an existing lockfile at ${path15.resolve(lockPath)}`);
4916
+ throw new Error(`Refusing to prune without an existing lockfile at ${path16.resolve(lockPath)}`);
4467
4917
  }
4468
- const contentDir = path15.resolve(path15.dirname(lockPath), ".pixelkiln", "cache");
4469
- const remotePath = path15.resolve(cachePathFor(lockPath));
4918
+ const contentDir = path16.resolve(path16.dirname(lockPath), ".pixelkiln", "cache");
4919
+ const remotePath = path16.resolve(cachePathFor(lockPath));
4470
4920
  const referenced = new Set(
4471
4921
  Object.values(lock.entries).flatMap((entry) => entry.outputs.map((output) => output.sha256))
4472
4922
  );
@@ -4480,7 +4930,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
4480
4930
  ]);
4481
4931
  for (const name of names) {
4482
4932
  try {
4483
- await rm5(path15.join(contentDir, name), { force: true });
4933
+ await rm5(path16.join(contentDir, name), { force: true });
4484
4934
  removed.contentFiles++;
4485
4935
  } catch {
4486
4936
  }
@@ -4489,7 +4939,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
4489
4939
  await saveCache(remotePath, { version: 1, hashes: {} });
4490
4940
  removed.resetRemoteHashCache = true;
4491
4941
  } else if (remoteHashes.invalidIds.length) {
4492
- const cache = parseCache(JSON.parse(await readFile11(remotePath, "utf8")));
4942
+ const cache = parseCache(JSON.parse(await readFile12(remotePath, "utf8")));
4493
4943
  for (const id of remoteHashes.invalidIds) delete cache.hashes[id];
4494
4944
  removed.remoteHashEntries = remoteHashes.invalidIds.length;
4495
4945
  await saveCache(remotePath, cache);
@@ -4528,12 +4978,12 @@ async function inspectContentCache(contentDir, referenced) {
4528
4978
  continue;
4529
4979
  }
4530
4980
  report.files++;
4531
- const file = path15.join(contentDir, entry.name);
4981
+ const file = path16.join(contentDir, entry.name);
4532
4982
  const mediaType = mediaTypeFromExtension(entry.name);
4533
4983
  const expected = mediaType ? entry.name.slice(0, -4) : "";
4534
4984
  let bytes;
4535
4985
  try {
4536
- bytes = await readFile11(file);
4986
+ bytes = await readFile12(file);
4537
4987
  report.bytes += bytes.length;
4538
4988
  } catch (err) {
4539
4989
  report.invalid.push({
@@ -4580,7 +5030,7 @@ async function inspectRemoteHashCache(remotePath) {
4580
5030
  if (!report.exists) return report;
4581
5031
  let cache;
4582
5032
  try {
4583
- cache = parseCache(JSON.parse(await readFile11(remotePath, "utf8")));
5033
+ cache = parseCache(JSON.parse(await readFile12(remotePath, "utf8")));
4584
5034
  } catch (err) {
4585
5035
  report.error = err instanceof Error ? err.message : String(err);
4586
5036
  return report;
@@ -4851,8 +5301,8 @@ function isRecord(value) {
4851
5301
  }
4852
5302
 
4853
5303
  // src/pick/salvage-server.ts
4854
- import { mkdir as mkdir5, writeFile as writeFile7, readFile as readFile12 } from "fs/promises";
4855
- import path16 from "path";
5304
+ import { mkdir as mkdir5, writeFile as writeFile7, readFile as readFile13 } from "fs/promises";
5305
+ import path17 from "path";
4856
5306
 
4857
5307
  // src/pick/salvage-sheet.ts
4858
5308
  var escapeHtml2 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
@@ -5017,7 +5467,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
5017
5467
  });
5018
5468
  const html = renderSalvageSheet(orphans, {
5019
5469
  styleId: ctx.styleId,
5020
- importDir: path16.relative(process.cwd(), ctx.importDir) || "."
5470
+ importDir: path17.relative(process.cwd(), ctx.importDir) || "."
5021
5471
  });
5022
5472
  const byId = new Map(orphans.map((o) => [o.id, o]));
5023
5473
  const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
@@ -5046,9 +5496,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
5046
5496
  if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
5047
5497
  decodePng(buf);
5048
5498
  const assetId = idFromPrompt(orphan.prompt, taken);
5049
- const rel = path16.join("_salvaged", `${assetId}.png`);
5050
- const outFile = path16.resolve(ctx.importDir, rel);
5051
- await mkdir5(path16.dirname(outFile), { recursive: true });
5499
+ const rel = path17.join("_salvaged", `${assetId}.png`);
5500
+ const outFile = path17.resolve(ctx.importDir, rel);
5501
+ await mkdir5(path17.dirname(outFile), { recursive: true });
5052
5502
  await writeFile7(outFile, buf);
5053
5503
  ctx.manifest.assets[assetId] = {
5054
5504
  prompt: orphan.prompt,
@@ -5075,7 +5525,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
5075
5525
  error: null,
5076
5526
  sourceUrl: orphan.previewUrl,
5077
5527
  outputs: [{
5078
- path: portableOutputPath(outFile, path16.dirname(ctx.manifestPath)),
5528
+ path: portableOutputPath(outFile, path17.dirname(ctx.manifestPath)),
5079
5529
  sha256: sha256(buf)
5080
5530
  }],
5081
5531
  submittedAt: orphan.createdAt,
@@ -5099,7 +5549,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
5099
5549
  }
5100
5550
  }
5101
5551
  await applyTags(provider, decisions, existingTags, { onProgress: log2 });
5102
- const raw = JSON.parse(await readFile12(ctx.manifestPath, "utf8"));
5552
+ const raw = JSON.parse(await readFile13(ctx.manifestPath, "utf8"));
5103
5553
  for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
5104
5554
  await writeFile7(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
5105
5555
  await saveLock(ctx.lockPath, ctx.lock);
@@ -5109,9 +5559,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
5109
5559
  }
5110
5560
 
5111
5561
  // src/artifacts.ts
5112
- import path17 from "path";
5113
- import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
5114
- import { mkdir as mkdir6, readFile as readFile13, rename as rename5, rm as rm6, writeFile as writeFile8 } from "fs/promises";
5562
+ import path18 from "path";
5563
+ import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
5564
+ import { mkdir as mkdir6, readFile as readFile14, rename as rename5, rm as rm6, writeFile as writeFile8 } from "fs/promises";
5115
5565
  var activeTransactions = /* @__PURE__ */ new Set();
5116
5566
  function message(error) {
5117
5567
  return error instanceof Error ? error.message : String(error);
@@ -5120,27 +5570,27 @@ function digest(data) {
5120
5570
  return createHash2("sha256").update(data).digest("hex");
5121
5571
  }
5122
5572
  function portableRelative(from, to) {
5123
- return path17.relative(from, path17.resolve(to)).split(path17.sep).join("/") || ".";
5573
+ return path18.relative(from, path18.resolve(to)).split(path18.sep).join("/") || ".";
5124
5574
  }
5125
- function canonical(value) {
5575
+ function canonical2(value) {
5126
5576
  if (value === null || typeof value === "string" || typeof value === "boolean") return value;
5127
5577
  if (typeof value === "number") {
5128
5578
  if (!Number.isFinite(value)) throw new Error("Artifact provenance numbers must be finite.");
5129
5579
  return value;
5130
5580
  }
5131
- if (Array.isArray(value)) return value.map(canonical);
5581
+ if (Array.isArray(value)) return value.map(canonical2);
5132
5582
  if (typeof value === "object") {
5133
5583
  const sorted = {};
5134
5584
  for (const key of Object.keys(value).sort()) {
5135
5585
  const item = value[key];
5136
- if (item !== void 0) sorted[key] = canonical(item);
5586
+ if (item !== void 0) sorted[key] = canonical2(item);
5137
5587
  }
5138
5588
  return sorted;
5139
5589
  }
5140
5590
  throw new Error(`Artifact provenance cannot contain ${typeof value} values.`);
5141
5591
  }
5142
5592
  function fingerprint(provenance) {
5143
- return digest(JSON.stringify(canonical({
5593
+ return digest(JSON.stringify(canonical2({
5144
5594
  kind: provenance.kind,
5145
5595
  sources: provenance.sources,
5146
5596
  options: provenance.options,
@@ -5171,7 +5621,7 @@ function parseArtifactManifest(absolute, data) {
5171
5621
  }
5172
5622
  async function readOptional(file) {
5173
5623
  try {
5174
- return await readFile13(file);
5624
+ return await readFile14(file);
5175
5625
  } catch (error) {
5176
5626
  if (isCode(error, "ENOENT")) return null;
5177
5627
  throw error;
@@ -5190,8 +5640,8 @@ function processIsAlive(pid) {
5190
5640
  }
5191
5641
  }
5192
5642
  function validTemporaryPath(candidate, destination, type) {
5193
- return path17.dirname(candidate) === path17.dirname(destination) && path17.basename(candidate).startsWith(
5194
- `.${path17.basename(destination)}.pixelkiln-${type}-`
5643
+ return path18.dirname(candidate) === path18.dirname(destination) && path18.basename(candidate).startsWith(
5644
+ `.${path18.basename(destination)}.pixelkiln-${type}-`
5195
5645
  );
5196
5646
  }
5197
5647
  function parseTransaction(journal, data) {
@@ -5211,7 +5661,7 @@ async function removeTransactionFiles(journal) {
5211
5661
  await rm6(transactionMarker(journal), { force: true });
5212
5662
  }
5213
5663
  async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
5214
- const journal = path17.resolve(recoveryFile);
5664
+ const journal = path18.resolve(recoveryFile);
5215
5665
  const bytes = await readOptional(journal);
5216
5666
  if (!bytes) {
5217
5667
  await rm6(transactionMarker(journal), { force: true });
@@ -5227,7 +5677,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
5227
5677
  );
5228
5678
  }
5229
5679
  for (const entry of transaction.entries) {
5230
- if (typeof entry.destination !== "string" || typeof entry.stage !== "string" || typeof entry.sha256 !== "string" || entry.backup !== void 0 && typeof entry.backup !== "string" || !allowedDestinations.has(path17.resolve(entry.destination)) || !validTemporaryPath(entry.stage, entry.destination, "stage") || entry.backup !== void 0 && !validTemporaryPath(entry.backup, entry.destination, "backup")) {
5680
+ if (typeof entry.destination !== "string" || typeof entry.stage !== "string" || typeof entry.sha256 !== "string" || entry.backup !== void 0 && typeof entry.backup !== "string" || !allowedDestinations.has(path18.resolve(entry.destination)) || !validTemporaryPath(entry.stage, entry.destination, "stage") || entry.backup !== void 0 && !validTemporaryPath(entry.backup, entry.destination, "backup")) {
5231
5681
  throw new Error(
5232
5682
  `Refusing unsafe artifact recovery from ${journal}; its destinations or temporary paths do not match the current bundle.`
5233
5683
  );
@@ -5273,7 +5723,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
5273
5723
  await removeTransactionFiles(journal);
5274
5724
  }
5275
5725
  function createArtifactBundleManifest(manifestPath, outputs, provenance) {
5276
- const root = path17.dirname(path17.resolve(manifestPath));
5726
+ const root = path18.dirname(path18.resolve(manifestPath));
5277
5727
  const sources = provenance.sources.map((source) => ({
5278
5728
  ...source,
5279
5729
  path: portableRelative(root, source.path)
@@ -5281,7 +5731,7 @@ function createArtifactBundleManifest(manifestPath, outputs, provenance) {
5281
5731
  const normalized = {
5282
5732
  kind: provenance.kind,
5283
5733
  sources,
5284
- options: canonical(provenance.options)
5734
+ options: canonical2(provenance.options)
5285
5735
  };
5286
5736
  const manifestOutputs = outputs.map((output) => ({
5287
5737
  path: portableRelative(root, output.path),
@@ -5303,11 +5753,11 @@ function withArtifactManifest(manifestPath, outputs, provenance) {
5303
5753
  ];
5304
5754
  }
5305
5755
  async function writeManagedArtifactBundle(manifestPath, outputs, provenance, options = {}) {
5306
- const absoluteManifest = path17.resolve(manifestPath);
5756
+ const absoluteManifest = path18.resolve(manifestPath);
5307
5757
  const recoveryFile = `${absoluteManifest}.transaction`;
5308
5758
  const allowedDestinations = /* @__PURE__ */ new Set([
5309
5759
  absoluteManifest,
5310
- ...outputs.map((output) => path17.resolve(output.path))
5760
+ ...outputs.map((output) => path18.resolve(output.path))
5311
5761
  ]);
5312
5762
  await recoverArtifactTransaction(recoveryFile, allowedDestinations);
5313
5763
  const existingManifestBytes = await readOptional(absoluteManifest);
@@ -5325,13 +5775,13 @@ async function writeManagedArtifactBundle(manifestPath, outputs, provenance, opt
5325
5775
  }
5326
5776
  }
5327
5777
  if (!options.force) {
5328
- const root = path17.dirname(absoluteManifest);
5778
+ const root = path18.dirname(absoluteManifest);
5329
5779
  const recorded = new Map(
5330
- previous?.outputs.map((output) => [path17.resolve(root, output.path), output.sha256]) ?? []
5780
+ previous?.outputs.map((output) => [path18.resolve(root, output.path), output.sha256]) ?? []
5331
5781
  );
5332
5782
  const conflicts = [];
5333
5783
  for (const output of outputs) {
5334
- const destination = path17.resolve(output.path);
5784
+ const destination = path18.resolve(output.path);
5335
5785
  const current = await readOptional(destination);
5336
5786
  if (!current || digest(current) === digest(output.data)) continue;
5337
5787
  const expected = recorded.get(destination);
@@ -5386,7 +5836,7 @@ async function writeArtifactBundle(files, options = {}) {
5386
5836
  if (!files.length) throw new Error("An artifact bundle must contain at least one file.");
5387
5837
  const normalized = files.map((file) => {
5388
5838
  if (!file.path.trim()) throw new Error("Artifact paths cannot be empty.");
5389
- return { destination: path17.resolve(file.path), data: Buffer.from(file.data) };
5839
+ return { destination: path18.resolve(file.path), data: Buffer.from(file.data) };
5390
5840
  });
5391
5841
  const destinations = /* @__PURE__ */ new Set();
5392
5842
  for (const artifact of normalized) {
@@ -5402,7 +5852,7 @@ async function writeArtifactBundle(files, options = {}) {
5402
5852
  const unchanged = [];
5403
5853
  for (const artifact of normalized) {
5404
5854
  try {
5405
- const current = await readFile13(artifact.destination);
5855
+ const current = await readFile14(artifact.destination);
5406
5856
  if (current.equals(artifact.data)) {
5407
5857
  unchanged.push(artifact.destination);
5408
5858
  } else {
@@ -5414,23 +5864,23 @@ async function writeArtifactBundle(files, options = {}) {
5414
5864
  }
5415
5865
  }
5416
5866
  if (!changed.length) return { changed: [], unchanged };
5417
- const token = randomUUID2();
5867
+ const token = randomUUID3();
5418
5868
  for (const [index, artifact] of changed.entries()) {
5419
- const basename = path17.basename(artifact.destination);
5420
- artifact.stage = path17.join(
5421
- path17.dirname(artifact.destination),
5869
+ const basename = path18.basename(artifact.destination);
5870
+ artifact.stage = path18.join(
5871
+ path18.dirname(artifact.destination),
5422
5872
  `.${basename}.pixelkiln-stage-${token}-${index}`
5423
5873
  );
5424
5874
  if (artifact.existed) {
5425
- artifact.backup = path17.join(
5426
- path17.dirname(artifact.destination),
5875
+ artifact.backup = path18.join(
5876
+ path18.dirname(artifact.destination),
5427
5877
  `.${basename}.pixelkiln-backup-${token}-${index}`
5428
5878
  );
5429
5879
  }
5430
5880
  }
5431
- const journal = options.recoveryFile ? path17.resolve(options.recoveryFile) : null;
5881
+ const journal = options.recoveryFile ? path18.resolve(options.recoveryFile) : null;
5432
5882
  if (journal) {
5433
- await mkdir6(path17.dirname(journal), { recursive: true });
5883
+ await mkdir6(path18.dirname(journal), { recursive: true });
5434
5884
  const transaction = {
5435
5885
  format: "pixelkiln-artifact-transaction",
5436
5886
  version: 1,
@@ -5455,7 +5905,7 @@ async function writeArtifactBundle(files, options = {}) {
5455
5905
  }
5456
5906
  try {
5457
5907
  for (const [index, artifact] of changed.entries()) {
5458
- await mkdir6(path17.dirname(artifact.destination), { recursive: true });
5908
+ await mkdir6(path18.dirname(artifact.destination), { recursive: true });
5459
5909
  await options.beforeStage?.(artifact.destination, index);
5460
5910
  await writeFile8(artifact.stage, artifact.data, { flag: "wx" });
5461
5911
  }
@@ -5525,7 +5975,7 @@ async function writeArtifactBundle(files, options = {}) {
5525
5975
  // src/cli.ts
5526
5976
  var log = (msg = "") => console.log(msg);
5527
5977
  async function provenanceFile(id, file) {
5528
- const absolute = path18.resolve(file);
5978
+ const absolute = path19.resolve(file);
5529
5979
  return {
5530
5980
  id,
5531
5981
  path: absolute,
@@ -5708,7 +6158,7 @@ function parseArgs(argv) {
5708
6158
  return {
5709
6159
  command,
5710
6160
  manifest,
5711
- lock: get("--lock") ?? path18.join(path18.dirname(path18.resolve(manifest)), "pixelkiln.lock.json"),
6161
+ lock: get("--lock") ?? path19.join(path19.dirname(path19.resolve(manifest)), "pixelkiln.lock.json"),
5712
6162
  explicitLock: get("--lock"),
5713
6163
  styles: list("--style"),
5714
6164
  assets: list("--only"),
@@ -5866,7 +6316,7 @@ async function requireCompleteWorkspaceClaims(workspacePath) {
5866
6316
  if (!existsSync15(workspacePath)) {
5867
6317
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
5868
6318
  }
5869
- const dir = path18.dirname(path18.resolve(workspacePath));
6319
+ const dir = path19.dirname(path19.resolve(workspacePath));
5870
6320
  const ws = await loadWorkspace(workspacePath);
5871
6321
  const diagnostics = validateWorkspace(ws, dir);
5872
6322
  const errors = diagnostics.filter((d) => d.level === "error");
@@ -5887,13 +6337,13 @@ async function main() {
5887
6337
  }
5888
6338
  if (args.command === "--version" || args.command === "-v") {
5889
6339
  const pkg = JSON.parse(
5890
- await readFile14(new URL("../package.json", import.meta.url), "utf8")
6340
+ await readFile15(new URL("../package.json", import.meta.url), "utf8")
5891
6341
  );
5892
6342
  log(`${pkg.name} ${pkg.version}`);
5893
6343
  return;
5894
6344
  }
5895
6345
  if (args.command === "balance") {
5896
- loadEnvFiles(path18.dirname(path18.resolve(args.manifest)));
6346
+ loadEnvFiles(path19.dirname(path19.resolve(args.manifest)));
5897
6347
  loadEnvFiles(process.cwd());
5898
6348
  const loaded2 = await loadManifest(args.manifest);
5899
6349
  const p = createProvider(loaded2.manifest.provider, "online");
@@ -5905,31 +6355,31 @@ async function main() {
5905
6355
  }
5906
6356
  if (args.command === "init") {
5907
6357
  if (!args.from) throw new Error("init needs --from <dir> pointing at your existing PNGs.");
5908
- const root = path18.resolve(args.from);
6358
+ const root = path19.resolve(args.from);
5909
6359
  if (!existsSync15(root)) throw new Error(`No directory at ${root}`);
5910
6360
  const generator = args.generator ?? "map";
5911
6361
  if (generator !== "1dir" && generator !== "map") {
5912
6362
  throw new Error(`--generator must be "1dir" or "map", got "${args.generator}".`);
5913
6363
  }
5914
- const target = path18.resolve(args.out ?? "pixelkiln.manifest.json");
6364
+ const target = path19.resolve(args.out ?? "pixelkiln.manifest.json");
5915
6365
  const { assets, skipped } = await scanAssets(root, { exclude: args.exclude });
5916
6366
  if (!assets.length) throw new Error(`No PNGs found under ${root}`);
5917
6367
  const manifest = buildManifest(
5918
- args.name ?? path18.basename(path18.dirname(target)),
6368
+ args.name ?? path19.basename(path19.dirname(target)),
5919
6369
  args.styles[0] ?? "base",
5920
6370
  generator,
5921
- path18.relative(path18.dirname(target), root) || ".",
6371
+ path19.relative(path19.dirname(target), root) || ".",
5922
6372
  assets
5923
6373
  );
5924
6374
  await writeManifestFile(target, manifest);
5925
- log(` scanned ${assets.length} PNG(s) under ${path18.relative(process.cwd(), root)}`);
6375
+ log(` scanned ${assets.length} PNG(s) under ${path19.relative(process.cwd(), root)}`);
5926
6376
  if (skipped.length) log(` skipped ${skipped.length} unreadable file(s)`);
5927
- log(` wrote ${path18.relative(process.cwd(), target)}`);
6377
+ log(` wrote ${path19.relative(process.cwd(), target)}`);
5928
6378
  log(`
5929
6379
  Prompts are intentionally empty. To recover the real ones from your`);
5930
6380
  log(` PixelLab account instead of inventing them:`);
5931
6381
  log(`
5932
- pixelkiln adopt --manifest ${path18.relative(process.cwd(), target)} --write-prompts
6382
+ pixelkiln adopt --manifest ${path19.relative(process.cwd(), target)} --write-prompts
5933
6383
  `);
5934
6384
  return;
5935
6385
  }
@@ -5977,10 +6427,10 @@ async function main() {
5977
6427
  if (args.primaryOnly || args.outputRoles.length) {
5978
6428
  throw new Error("--primary-only and --output-role require manifest-driven pack");
5979
6429
  }
5980
- const raw = JSON.parse(await readFile14(path18.resolve(args.inputs), "utf8"));
6430
+ const raw = JSON.parse(await readFile15(path19.resolve(args.inputs), "utf8"));
5981
6431
  const inputs = resolvePackInputs(raw, args.inputs);
5982
6432
  const { png, atlas, skipped, sources } = packSprites(inputs, { columns: args.columns });
5983
- const base = path18.resolve(args.out.replace(/\.png$/, ""));
6433
+ const base = path19.resolve(args.out.replace(/\.png$/, ""));
5984
6434
  const outputs = [
5985
6435
  { path: `${base}.png`, data: png },
5986
6436
  { path: `${base}.json`, data: JSON.stringify(atlas, null, 2) + "\n" }
@@ -5993,17 +6443,17 @@ async function main() {
5993
6443
  log(
5994
6444
  ` ${atlas.frames.length} sprite(s), ${atlas.sheet.width}x${atlas.sheet.height} in ${atlas.columns} column(s) \u2014 ${(png.length / 1024).toFixed(1)} KB`
5995
6445
  );
5996
- log(` ${path18.relative(process.cwd(), base)}.png + .json + .pixelkiln.json`);
6446
+ log(` ${path19.relative(process.cwd(), base)}.png + .json + .pixelkiln.json`);
5997
6447
  for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
5998
6448
  return;
5999
6449
  }
6000
6450
  if (args.command === "workspace") {
6001
- const workspacePath = path18.resolve(args.workspace ?? "pixelkiln.workspace.json");
6002
- const dir = path18.dirname(workspacePath);
6451
+ const workspacePath = path19.resolve(args.workspace ?? "pixelkiln.workspace.json");
6452
+ const dir = path19.dirname(workspacePath);
6003
6453
  if (args.subcommand === "add") {
6004
- const manifestPath = path18.resolve(args.target);
6454
+ const manifestPath = path19.resolve(args.target);
6005
6455
  const loadedTarget = await loadManifest(manifestPath);
6006
- const lockPath = args.explicitLock ? path18.resolve(args.explicitLock) : path18.join(path18.dirname(manifestPath), "pixelkiln.lock.json");
6456
+ const lockPath = args.explicitLock ? path19.resolve(args.explicitLock) : path19.join(path19.dirname(manifestPath), "pixelkiln.lock.json");
6007
6457
  const ws = await loadWorkspace(workspacePath);
6008
6458
  const id = args.name ?? loadedTarget.manifest.name;
6009
6459
  if (ws.projects.some((p) => p.id === id)) {
@@ -6023,7 +6473,7 @@ async function main() {
6023
6473
  ...args.account ? { account: args.account } : {}
6024
6474
  };
6025
6475
  await saveWorkspace(workspacePath, { version: 1, projects: [...ws.projects, project] });
6026
- log(` registered "${id}" in ${path18.relative(process.cwd(), workspacePath)}`);
6476
+ log(` registered "${id}" in ${path19.relative(process.cwd(), workspacePath)}`);
6027
6477
  log(` manifest: ${project.manifest}`);
6028
6478
  log(` lock: ${project.lock}`);
6029
6479
  if (!existsSync15(lockPath)) {
@@ -6035,7 +6485,7 @@ async function main() {
6035
6485
  }
6036
6486
  if (args.subcommand === "remove") {
6037
6487
  const ws = await loadWorkspace(workspacePath);
6038
- const resolvedTarget = path18.resolve(args.target);
6488
+ const resolvedTarget = path19.resolve(args.target);
6039
6489
  const match = ws.projects.find(
6040
6490
  (p) => p.id === args.target || resolveProject(dir, p).manifestPath === resolvedTarget
6041
6491
  );
@@ -6046,7 +6496,7 @@ async function main() {
6046
6496
  version: 1,
6047
6497
  projects: ws.projects.filter((p) => p !== match)
6048
6498
  });
6049
- log(` removed "${match.id}" from ${path18.relative(process.cwd(), workspacePath)}`);
6499
+ log(` removed "${match.id}" from ${path19.relative(process.cwd(), workspacePath)}`);
6050
6500
  return;
6051
6501
  }
6052
6502
  if (args.subcommand === "list") {
@@ -6058,9 +6508,9 @@ async function main() {
6058
6508
  if (args.json) {
6059
6509
  log(JSON.stringify({ version: 1, workspace: workspacePath, projects: ws.projects, diagnostics }, null, 2));
6060
6510
  } else if (!ws.projects.length) {
6061
- log(` no projects registered in ${path18.relative(process.cwd(), workspacePath)}`);
6511
+ log(` no projects registered in ${path19.relative(process.cwd(), workspacePath)}`);
6062
6512
  } else {
6063
- log(` ${ws.projects.length} project(s) in ${path18.relative(process.cwd(), workspacePath)}:`);
6513
+ log(` ${ws.projects.length} project(s) in ${path19.relative(process.cwd(), workspacePath)}:`);
6064
6514
  for (const p of ws.projects) {
6065
6515
  log(` ${p.id.padEnd(24)} ${p.manifest.padEnd(40)} (${p.provider}${p.account ? `, ${p.account}` : ""})`);
6066
6516
  }
@@ -6078,7 +6528,7 @@ async function main() {
6078
6528
  if (args.json) {
6079
6529
  log(JSON.stringify({ ...report, workspace: workspacePath }, null, 2));
6080
6530
  } else {
6081
- log(` workspace: ${path18.relative(process.cwd(), workspacePath)}`);
6531
+ log(` workspace: ${path19.relative(process.cwd(), workspacePath)}`);
6082
6532
  for (const p of report.projects) {
6083
6533
  if (p.error) {
6084
6534
  log(`
@@ -6122,14 +6572,14 @@ async function main() {
6122
6572
  return;
6123
6573
  }
6124
6574
  }
6125
- if (!existsSync15(path18.resolve(args.manifest))) {
6575
+ if (!existsSync15(path19.resolve(args.manifest))) {
6126
6576
  throw new Error(
6127
- `No manifest at ${path18.resolve(args.manifest)}. Pass --manifest, or run \`pixelkiln init --from <dir>\`.`
6577
+ `No manifest at ${path19.resolve(args.manifest)}. Pass --manifest, or run \`pixelkiln init --from <dir>\`.`
6128
6578
  );
6129
6579
  }
6130
- const manifestDir = path18.dirname(path18.resolve(args.manifest));
6580
+ const manifestDir = path19.dirname(path19.resolve(args.manifest));
6131
6581
  const envFiles = [...loadEnvFiles(manifestDir)];
6132
- if (path18.resolve(process.cwd()) !== manifestDir) envFiles.push(...loadEnvFiles(process.cwd()));
6582
+ if (path19.resolve(process.cwd()) !== manifestDir) envFiles.push(...loadEnvFiles(process.cwd()));
6133
6583
  const loaded = await loadManifest(args.manifest);
6134
6584
  const estimator = createProvider(loaded.manifest.provider, "offline");
6135
6585
  const specs = await resolveSpecs(loaded, {
@@ -6264,7 +6714,7 @@ async function main() {
6264
6714
  if (args.primaryOnly && args.outputRoles.length) {
6265
6715
  throw new Error("pack accepts either --primary-only or --output-role, not both");
6266
6716
  }
6267
- const manifestDir2 = path18.dirname(path18.resolve(args.manifest));
6717
+ const manifestDir2 = path19.dirname(path19.resolve(args.manifest));
6268
6718
  const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
6269
6719
  for (const styleId of styleIds) {
6270
6720
  const { png, atlas, skipped, sources } = packStyle(lock, styleId, manifestDir2, {
@@ -6273,7 +6723,7 @@ async function main() {
6273
6723
  primaryOnly: args.primaryOnly
6274
6724
  });
6275
6725
  const style = loaded.manifest.styles[styleId];
6276
- const base = args.out ? path18.resolve(args.out.replace(/\.png$/, "")) : path18.resolve(manifestDir2, style.outDir, `${styleId}-sheet`);
6726
+ const base = args.out ? path19.resolve(args.out.replace(/\.png$/, "")) : path19.resolve(manifestDir2, style.outDir, `${styleId}-sheet`);
6277
6727
  const outputs = [
6278
6728
  { path: `${base}.png`, data: png },
6279
6729
  { path: `${base}.json`, data: JSON.stringify(atlas, null, 2) + "\n" }
@@ -6296,13 +6746,13 @@ async function main() {
6296
6746
  log(
6297
6747
  ` ${styleId} \u2014 ${atlas.frames.length} sprite(s), ${atlas.sheet.width}x${atlas.sheet.height} in ${atlas.columns} column(s)`
6298
6748
  );
6299
- log(` ${path18.relative(process.cwd(), base)}.png + .json + .pixelkiln.json`);
6749
+ log(` ${path19.relative(process.cwd(), base)}.png + .json + .pixelkiln.json`);
6300
6750
  for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
6301
6751
  }
6302
6752
  return;
6303
6753
  }
6304
6754
  if (args.command === "mount") {
6305
- const manifestDir2 = path18.dirname(path18.resolve(args.manifest));
6755
+ const manifestDir2 = path19.dirname(path19.resolve(args.manifest));
6306
6756
  const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
6307
6757
  for (const styleId of styleIds) {
6308
6758
  const style = loaded.manifest.styles[styleId];
@@ -6332,7 +6782,7 @@ async function main() {
6332
6782
  sources,
6333
6783
  outputRoles
6334
6784
  );
6335
- const out = path18.resolve(manifestDir2, style.mount.out);
6785
+ const out = path19.resolve(manifestDir2, style.mount.out);
6336
6786
  const metadata = out.replace(/\.png$/, "") + ".json";
6337
6787
  const companion = out.replace(/\.png$/, "") + ".pixelkiln.json";
6338
6788
  const outputs = [
@@ -6345,7 +6795,7 @@ async function main() {
6345
6795
  await provenanceFile("$manifest", args.manifest),
6346
6796
  await provenanceFile("$lock", args.lock),
6347
6797
  ...artifactSources.filter(
6348
- (source) => source.id !== "$base" || path18.resolve(source.path) !== out
6798
+ (source) => source.id !== "$base" || path19.resolve(source.path) !== out
6349
6799
  )
6350
6800
  ],
6351
6801
  options: {
@@ -6358,7 +6808,7 @@ async function main() {
6358
6808
  log(
6359
6809
  ` ${styleId} \u2014 ${atlas.frames.length} cell(s) into ${atlas.sheet.width}x${atlas.sheet.height}` + (overBase ? ` over ${style.mount.base}` : " (new sheet)")
6360
6810
  );
6361
- log(` ${path18.relative(process.cwd(), out)} + atlas/provenance JSON`);
6811
+ log(` ${path19.relative(process.cwd(), out)} + atlas/provenance JSON`);
6362
6812
  for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
6363
6813
  }
6364
6814
  return;
@@ -6414,7 +6864,7 @@ async function main() {
6414
6864
  }
6415
6865
  if (args.command === "export") {
6416
6866
  const format = args.format ?? "generic";
6417
- const manifestDir2 = path18.dirname(path18.resolve(args.manifest));
6867
+ const manifestDir2 = path19.dirname(path19.resolve(args.manifest));
6418
6868
  const selected = specs.filter((spec) => {
6419
6869
  if (spec.generator !== "tiles") return false;
6420
6870
  if (args.styles.length && !args.styles.includes(spec.styleId)) return false;
@@ -6430,12 +6880,12 @@ async function main() {
6430
6880
  for (const spec of selected) {
6431
6881
  const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
6432
6882
  const style = loaded.manifest.styles[spec.styleId];
6433
- const defaultBase = path18.resolve(manifestDir2, style.outDir, `${spec.assetId}-tileset`);
6434
- const base = args.out ? path18.resolve(args.out.replace(/\.(?:png|json|tsj|tres)$/i, "")) : defaultBase;
6883
+ const defaultBase = path19.resolve(manifestDir2, style.outDir, `${spec.assetId}-tileset`);
6884
+ const base = args.out ? path19.resolve(args.out.replace(/\.(?:png|json|tsj|tres)$/i, "")) : defaultBase;
6435
6885
  const result = exportTileset(entry, spec, {
6436
6886
  format,
6437
6887
  manifestDir: manifestDir2,
6438
- imageName: path18.basename(`${base}.png`),
6888
+ imageName: path19.basename(`${base}.png`),
6439
6889
  columns: args.columns
6440
6890
  });
6441
6891
  const outputs = [
@@ -6453,7 +6903,7 @@ async function main() {
6453
6903
  asset: spec.assetId,
6454
6904
  columns: args.columns ?? null,
6455
6905
  format,
6456
- image: path18.basename(`${base}.png`),
6906
+ image: path19.basename(`${base}.png`),
6457
6907
  providerRules: result.generic.providerRules,
6458
6908
  style: spec.styleId,
6459
6909
  tileType: spec.tileType ?? null
@@ -6463,7 +6913,7 @@ async function main() {
6463
6913
  ` ${spec.styleId}/${spec.assetId} \u2014 ${result.generic.tiles.length} tile(s), ${result.generic.sheet.width}x${result.generic.sheet.height} (${format})`
6464
6914
  );
6465
6915
  log(
6466
- ` ${path18.relative(process.cwd(), base)}.png + ${path18.basename(base)}${result.extension} + .pixelkiln.json`
6916
+ ` ${path19.relative(process.cwd(), base)}.png + ${path19.basename(base)}${result.extension} + .pixelkiln.json`
6467
6917
  );
6468
6918
  }
6469
6919
  return;
@@ -6503,10 +6953,10 @@ async function main() {
6503
6953
  tagged ${n} object(s) upstream`);
6504
6954
  }
6505
6955
  if (args.writePrompts) {
6506
- const { filled, stillEmpty } = await writePromptsBack(path18.resolve(args.manifest), lock, {
6956
+ const { filled, stillEmpty } = await writePromptsBack(path19.resolve(args.manifest), lock, {
6507
6957
  onProgress: log
6508
6958
  });
6509
- log(` recovered ${filled} prompt(s) into ${path18.relative(process.cwd(), args.manifest)}`);
6959
+ log(` recovered ${filled} prompt(s) into ${path19.relative(process.cwd(), args.manifest)}`);
6510
6960
  const reloaded = await loadManifest(args.manifest);
6511
6961
  const rebased = await resolveSpecs(reloaded, {
6512
6962
  styles: args.styles,
@@ -6535,12 +6985,12 @@ async function main() {
6535
6985
  if (args.command === "salvage") {
6536
6986
  const jsonMode = args.dryRun && args.json;
6537
6987
  const diag = jsonMode ? (msg = "") => console.error(msg) : log;
6538
- const ownLock = path18.resolve(args.lock);
6988
+ const ownLock = path19.resolve(args.lock);
6539
6989
  let workspaceProjects = [];
6540
6990
  let workspaceDir = "";
6541
6991
  if (args.workspace) {
6542
- const workspacePath = path18.resolve(args.workspace);
6543
- workspaceDir = path18.dirname(workspacePath);
6992
+ const workspacePath = path19.resolve(args.workspace);
6993
+ workspaceDir = path19.dirname(workspacePath);
6544
6994
  const complete = await requireCompleteWorkspaceClaims(workspacePath);
6545
6995
  workspaceProjects = complete.ws.projects;
6546
6996
  for (const d of complete.diagnostics) diag(` WARN ${d.id}: ${d.message}`);
@@ -6550,11 +7000,11 @@ async function main() {
6550
7000
  .../* @__PURE__ */ new Set([
6551
7001
  ...workspaceLockPaths,
6552
7002
  ...existsSync15(ownLock) ? [ownLock] : [],
6553
- ...args.claims.map((c) => path18.resolve(c))
7003
+ ...args.claims.map((c) => path19.resolve(c))
6554
7004
  ])
6555
7005
  ];
6556
7006
  diag(` claim set (${lockPaths.length} lockfile(s)):`);
6557
- for (const p of lockPaths) diag(` ${path18.relative(process.cwd(), p)}`);
7007
+ for (const p of lockPaths) diag(` ${path19.relative(process.cwd(), p)}`);
6558
7008
  if (!args.claims.length && !args.workspace) {
6559
7009
  diag(
6560
7010
  `
@@ -6562,7 +7012,7 @@ async function main() {
6562
7012
  pass every other project's lockfile via --claims a.json,b.json, or
6563
7013
  register every project in a workspace catalog and pass --workspace.`
6564
7014
  );
6565
- } else if (args.workspace && !workspaceProjects.some((p) => resolveProject(workspaceDir, p).manifestPath === path18.resolve(args.manifest))) {
7015
+ } else if (args.workspace && !workspaceProjects.some((p) => resolveProject(workspaceDir, p).manifestPath === path19.resolve(args.manifest))) {
6566
7016
  diag(
6567
7017
  `
6568
7018
  This project's manifest is not registered in the workspace catalog. Its own
@@ -6646,7 +7096,7 @@ async function main() {
6646
7096
  manifestPath: loaded.path,
6647
7097
  manifest: loaded.manifest,
6648
7098
  styleId,
6649
- importDir: path18.resolve(loaded.root, style.outDir),
7099
+ importDir: path19.resolve(loaded.root, style.outDir),
6650
7100
  lock,
6651
7101
  lockPath: args.lock
6652
7102
  },