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/PROVIDERS.md +103 -33
- package/README.md +24 -13
- package/dist/cli.js +1347 -897
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +694 -242
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +95 -5
- package/dist/index.d.ts +95 -5
- package/dist/index.js +655 -204
- package/dist/index.js.map +1 -1
- package/docs/AGENTS.md +7 -5
- package/docs/ARCHITECTURE.md +15 -6
- package/docs/CLI.md +6 -4
- package/docs/COMFYUI.md +191 -0
- package/docs/GETTING_STARTED.md +14 -7
- package/docs/LIBRARY.md +9 -7
- package/docs/MANIFEST.md +54 -8
- package/docs/PROVIDER_BENCHMARK.md +113 -16
- package/docs/README.md +3 -2
- package/docs/RETRO_DIFFUSION.md +1 -1
- package/examples/comfyui/README.md +24 -0
- package/examples/comfyui/pixelkiln.manifest.json +35 -0
- package/examples/comfyui/workflow-api.json +59 -0
- package/package.json +2 -1
- package/schema/manifest.schema.json +4 -4
- package/skills/pixelkiln/SKILL.md +5 -3
- package/skills/pixelkiln/references/comfyui.md +46 -0
- package/skills/pixelkiln/references/mixed-providers.md +15 -7
- package/skills/pixelkiln/references/pixellab.md +5 -3
- package/skills/pixelkiln/references/retro-diffusion.md +7 -0
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import
|
|
4
|
+
import path19 from "path";
|
|
5
5
|
import { existsSync as existsSync15 } from "fs";
|
|
6
|
-
import { readFile as
|
|
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/
|
|
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
|
|
108
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
110
109
|
import path2 from "path";
|
|
111
110
|
|
|
112
|
-
// src/
|
|
113
|
-
import {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
126
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
|
248
|
-
if (!
|
|
249
|
-
throw new
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
}
|
|
255
|
-
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
|
|
379
|
-
|
|
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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
-
|
|
393
|
-
|
|
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
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
-
|
|
426
|
-
|
|
317
|
+
if (colorType === 2 && transparency.length !== 6) {
|
|
318
|
+
throw new Error("RGB PNG has an invalid transparency chunk");
|
|
427
319
|
}
|
|
428
|
-
|
|
429
|
-
|
|
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
|
-
|
|
433
|
-
|
|
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
|
-
|
|
758
|
-
|
|
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
|
-
|
|
774
|
-
|
|
775
|
-
|
|
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
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
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
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
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
|
|
857
|
-
size: z2.number().int().min(
|
|
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(
|
|
974
|
-
height: z2.number().int().min(16).max(
|
|
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 =
|
|
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 =
|
|
1218
|
-
writeFileSync(
|
|
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 =
|
|
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
|
|
1959
|
+
var DEFAULT_BASE_URL2 = "https://api.retrodiffusion.ai/v1";
|
|
1492
1960
|
var RetroDiffusionClient = class {
|
|
1493
|
-
constructor(token, baseUrl =
|
|
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(
|
|
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}${
|
|
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
|
|
2318
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
1843
2319
|
import { existsSync as existsSync3 } from "fs";
|
|
1844
|
-
import
|
|
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 =
|
|
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
|
|
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:
|
|
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 =
|
|
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
|
|
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 ??
|
|
1984
|
-
const outFile =
|
|
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
|
|
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(
|
|
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 =
|
|
2060
|
-
const buf = await
|
|
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
|
|
2540
|
+
import path7 from "path";
|
|
2102
2541
|
|
|
2103
2542
|
// src/outputs.ts
|
|
2104
|
-
import
|
|
2543
|
+
import path6 from "path";
|
|
2105
2544
|
|
|
2106
2545
|
// src/lock.ts
|
|
2107
|
-
import { readFile as
|
|
2546
|
+
import { readFile as readFile4, writeFile, rename, mkdir, rmdir, rm, stat } from "fs/promises";
|
|
2108
2547
|
import { existsSync as existsSync4 } from "fs";
|
|
2109
|
-
import
|
|
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
|
|
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 =
|
|
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(
|
|
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
|
|
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 =
|
|
2279
|
-
const absolute =
|
|
2280
|
-
const relative =
|
|
2281
|
-
const value =
|
|
2282
|
-
return value.split(
|
|
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 (
|
|
2286
|
-
if (
|
|
2287
|
-
return
|
|
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 =
|
|
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:
|
|
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 =
|
|
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:
|
|
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:
|
|
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(
|
|
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:
|
|
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
|
|
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(
|
|
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, {
|
|
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
|
|
2896
|
-
import
|
|
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 :
|
|
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
|
|
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 ${
|
|
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(
|
|
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 ${
|
|
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 =
|
|
3472
|
+
const file = path9.join(cacheDir, cacheFileName(hash, mediaType));
|
|
3030
3473
|
if (!existsSync6(file)) return null;
|
|
3031
3474
|
try {
|
|
3032
|
-
const buf = await
|
|
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 =
|
|
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
|
|
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) =>
|
|
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,
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
|
3213
|
-
import
|
|
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
|
|
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(
|
|
3233
|
-
if (!existsSync8(
|
|
3682
|
+
async function loadCache(path20) {
|
|
3683
|
+
if (!existsSync8(path20)) return { version: 1, hashes: {} };
|
|
3234
3684
|
try {
|
|
3235
|
-
const parsed = HashCacheSchema.safeParse(JSON.parse(await
|
|
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(
|
|
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(
|
|
3255
|
-
const 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,
|
|
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
|
|
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
|
|
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
|
|
4250
|
+
import { readdir, readFile as readFile8, writeFile as writeFile5 } from "fs/promises";
|
|
3801
4251
|
import { existsSync as existsSync10 } from "fs";
|
|
3802
|
-
import
|
|
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 =
|
|
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
|
|
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 =
|
|
3836
|
-
const category =
|
|
3837
|
-
let id = slugify(
|
|
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
|
|
4349
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
3900
4350
|
import { existsSync as existsSync11 } from "fs";
|
|
3901
|
-
import
|
|
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
|
|
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 =
|
|
4400
|
+
const own = path13.resolve(ownManifestPath);
|
|
3951
4401
|
const siblingManifestPaths = [
|
|
3952
4402
|
.../* @__PURE__ */ new Set([
|
|
3953
4403
|
...workspaceManifestPaths,
|
|
3954
|
-
...claimPaths.map((c) =>
|
|
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 (
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
4583
|
+
return path14.relative(dir, absolute).split(path14.sep).join("/");
|
|
4134
4584
|
}
|
|
4135
4585
|
function resolveProject(dir, project) {
|
|
4136
4586
|
return {
|
|
4137
|
-
manifestPath:
|
|
4138
|
-
lockPath:
|
|
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 (
|
|
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
|
|
4756
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
4307
4757
|
import { existsSync as existsSync13 } from "fs";
|
|
4308
|
-
import
|
|
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
|
|
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 =
|
|
4832
|
+
const abs = path15.resolve(loaded.root, img.path);
|
|
4383
4833
|
if (!existsSync13(abs)) continue;
|
|
4384
4834
|
try {
|
|
4385
|
-
refPalettes.push(extractPalette(decodePng(await
|
|
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
|
|
4463
|
-
import
|
|
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 ${
|
|
4916
|
+
throw new Error(`Refusing to prune without an existing lockfile at ${path16.resolve(lockPath)}`);
|
|
4467
4917
|
}
|
|
4468
|
-
const contentDir =
|
|
4469
|
-
const remotePath =
|
|
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(
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
4855
|
-
import
|
|
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) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[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:
|
|
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 =
|
|
5050
|
-
const outFile =
|
|
5051
|
-
await mkdir5(
|
|
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,
|
|
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
|
|
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
|
|
5113
|
-
import { createHash as createHash2, randomUUID as
|
|
5114
|
-
import { mkdir as mkdir6, readFile as
|
|
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
|
|
5573
|
+
return path18.relative(from, path18.resolve(to)).split(path18.sep).join("/") || ".";
|
|
5124
5574
|
}
|
|
5125
|
-
function
|
|
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(
|
|
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] =
|
|
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(
|
|
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
|
|
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
|
|
5194
|
-
`.${
|
|
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 =
|
|
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(
|
|
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 =
|
|
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:
|
|
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 =
|
|
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) =>
|
|
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 =
|
|
5778
|
+
const root = path18.dirname(absoluteManifest);
|
|
5329
5779
|
const recorded = new Map(
|
|
5330
|
-
previous?.outputs.map((output) => [
|
|
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 =
|
|
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:
|
|
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
|
|
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 =
|
|
5867
|
+
const token = randomUUID3();
|
|
5418
5868
|
for (const [index, artifact] of changed.entries()) {
|
|
5419
|
-
const basename =
|
|
5420
|
-
artifact.stage =
|
|
5421
|
-
|
|
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 =
|
|
5426
|
-
|
|
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 ?
|
|
5881
|
+
const journal = options.recoveryFile ? path18.resolve(options.recoveryFile) : null;
|
|
5432
5882
|
if (journal) {
|
|
5433
|
-
await mkdir6(
|
|
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(
|
|
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 =
|
|
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") ??
|
|
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 =
|
|
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
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 ??
|
|
6368
|
+
args.name ?? path19.basename(path19.dirname(target)),
|
|
5919
6369
|
args.styles[0] ?? "base",
|
|
5920
6370
|
generator,
|
|
5921
|
-
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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
|
|
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 =
|
|
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(` ${
|
|
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 =
|
|
6002
|
-
const dir =
|
|
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 =
|
|
6454
|
+
const manifestPath = path19.resolve(args.target);
|
|
6005
6455
|
const loadedTarget = await loadManifest(manifestPath);
|
|
6006
|
-
const lockPath = args.explicitLock ?
|
|
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 ${
|
|
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 =
|
|
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 ${
|
|
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 ${
|
|
6511
|
+
log(` no projects registered in ${path19.relative(process.cwd(), workspacePath)}`);
|
|
6062
6512
|
} else {
|
|
6063
|
-
log(` ${ws.projects.length} project(s) in ${
|
|
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: ${
|
|
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(
|
|
6575
|
+
if (!existsSync15(path19.resolve(args.manifest))) {
|
|
6126
6576
|
throw new Error(
|
|
6127
|
-
`No manifest at ${
|
|
6577
|
+
`No manifest at ${path19.resolve(args.manifest)}. Pass --manifest, or run \`pixelkiln init --from <dir>\`.`
|
|
6128
6578
|
);
|
|
6129
6579
|
}
|
|
6130
|
-
const manifestDir =
|
|
6580
|
+
const manifestDir = path19.dirname(path19.resolve(args.manifest));
|
|
6131
6581
|
const envFiles = [...loadEnvFiles(manifestDir)];
|
|
6132
|
-
if (
|
|
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 =
|
|
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 ?
|
|
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(` ${
|
|
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 =
|
|
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 =
|
|
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" ||
|
|
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(` ${
|
|
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 =
|
|
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 =
|
|
6434
|
-
const base = args.out ?
|
|
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:
|
|
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:
|
|
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
|
-
` ${
|
|
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(
|
|
6956
|
+
const { filled, stillEmpty } = await writePromptsBack(path19.resolve(args.manifest), lock, {
|
|
6507
6957
|
onProgress: log
|
|
6508
6958
|
});
|
|
6509
|
-
log(` recovered ${filled} prompt(s) into ${
|
|
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 =
|
|
6988
|
+
const ownLock = path19.resolve(args.lock);
|
|
6539
6989
|
let workspaceProjects = [];
|
|
6540
6990
|
let workspaceDir = "";
|
|
6541
6991
|
if (args.workspace) {
|
|
6542
|
-
const workspacePath =
|
|
6543
|
-
workspaceDir =
|
|
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) =>
|
|
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(` ${
|
|
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 ===
|
|
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:
|
|
7099
|
+
importDir: path19.resolve(loaded.root, style.outDir),
|
|
6650
7100
|
lock,
|
|
6651
7101
|
lockPath: args.lock
|
|
6652
7102
|
},
|