pixelkiln 0.2.0 → 0.4.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/CONTRIBUTING.md +50 -1
- package/NAMING.md +15 -15
- package/PROVIDERS.md +128 -90
- package/README.md +57 -16
- package/SECURITY.md +4 -3
- package/dist/cli.d.ts +18 -1
- package/dist/cli.js +1248 -281
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +911 -102
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +547 -228
- package/dist/index.d.ts +547 -228
- package/dist/index.js +887 -102
- package/dist/index.js.map +1 -1
- package/docs/AGENTS.md +13 -6
- package/docs/ARCHITECTURE.md +27 -15
- package/docs/CLI.md +57 -4
- package/docs/ENDPOINTS.md +39 -38
- package/docs/GENERATORS.md +6 -1
- package/docs/GETTING_STARTED.md +24 -5
- package/docs/LIBRARY.md +4 -4
- package/docs/MANIFEST.md +103 -5
- package/docs/PIXELLAB.md +100 -0
- package/docs/PROVIDER_BENCHMARK.md +134 -0
- package/docs/README.md +4 -1
- package/docs/RECOVERY.md +46 -1
- package/docs/RETRO_DIFFUSION.md +110 -0
- package/docs/TILES.md +1 -1
- package/examples/minimal/README.md +2 -2
- package/package.json +3 -1
- package/schema/manifest.schema.json +15 -1
- package/schema/workspace.schema.json +54 -0
- package/skills/pixelkiln/SKILL.md +15 -8
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/types.ts
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
var
|
|
3
|
+
var MediaTypeSchema = z.enum(["image/png", "image/gif"]);
|
|
4
|
+
var GeneratorSchema = z.enum(["1dir", "map", "pixflux", "tiles", "animation"]);
|
|
4
5
|
function tileVariationCount(descriptions) {
|
|
5
6
|
return Math.max(1, descriptions) * 4;
|
|
6
7
|
}
|
|
@@ -32,7 +33,7 @@ function tilesCost(tileSize, variations) {
|
|
|
32
33
|
return 40;
|
|
33
34
|
}
|
|
34
35
|
var StyleImageSchema = z.object({
|
|
35
|
-
/** Path to a PNG/JPEG, relative to the manifest
|
|
36
|
+
/** Path to a PNG/JPEG, relative to the manifest; the active provider validates limits. */
|
|
36
37
|
path: z.string()
|
|
37
38
|
});
|
|
38
39
|
var StyleSchema = z.object({
|
|
@@ -143,7 +144,9 @@ var StyleSchema = z.object({
|
|
|
143
144
|
out: z.string()
|
|
144
145
|
}).strict().optional(),
|
|
145
146
|
/** Tags applied to every object generated in this style, for server-side filtering. */
|
|
146
|
-
tags: z.array(z.string()).default([])
|
|
147
|
+
tags: z.array(z.string()).default([]),
|
|
148
|
+
/** Adapter-owned settings, keyed by provider id. */
|
|
149
|
+
providerOptions: z.record(z.record(z.unknown())).default({})
|
|
147
150
|
}).strict().refine((s) => !(s.tileFeature && s.styleImages.length), {
|
|
148
151
|
message: "tileFeature and styleImages cannot be combined \u2014 a connectable set derives its own tile geometry, so remove one or the other",
|
|
149
152
|
path: ["tileFeature"]
|
|
@@ -158,7 +161,7 @@ var AssetSchema = z.object({
|
|
|
158
161
|
height: z.number().int().min(16).max(400).optional(),
|
|
159
162
|
/** Overrides the style default. `1dir` generator only. */
|
|
160
163
|
size: z.number().int().min(32).max(256).optional(),
|
|
161
|
-
/** Explicit output path relative to outDir.
|
|
164
|
+
/** Explicit output path relative to outDir. Media-aware providers may replace its extension. */
|
|
162
165
|
file: z.string().optional(),
|
|
163
166
|
/**
|
|
164
167
|
* Grid cell this asset owns in a mounted style, as [column, row].
|
|
@@ -210,6 +213,8 @@ var AssetSchema = z.object({
|
|
|
210
213
|
var ManifestSchema = z.object({
|
|
211
214
|
$schema: z.string().optional(),
|
|
212
215
|
name: z.string(),
|
|
216
|
+
/** Generation backend. Existing manifests remain PixelLab by default. */
|
|
217
|
+
provider: z.string().min(1).default("pixellab"),
|
|
213
218
|
styles: z.record(StyleSchema),
|
|
214
219
|
assets: z.record(AssetSchema)
|
|
215
220
|
}).strict();
|
|
@@ -247,7 +252,8 @@ var LockEntrySchema = z.object({
|
|
|
247
252
|
sourceUrls: z.array(
|
|
248
253
|
z.object({
|
|
249
254
|
url: z.string(),
|
|
250
|
-
role: z.string().optional()
|
|
255
|
+
role: z.string().optional(),
|
|
256
|
+
mediaType: MediaTypeSchema.optional()
|
|
251
257
|
})
|
|
252
258
|
).default([]),
|
|
253
259
|
/**
|
|
@@ -262,7 +268,8 @@ var LockEntrySchema = z.object({
|
|
|
262
268
|
z.object({
|
|
263
269
|
path: z.string(),
|
|
264
270
|
sha256: z.string(),
|
|
265
|
-
role: z.string().optional()
|
|
271
|
+
role: z.string().optional(),
|
|
272
|
+
mediaType: MediaTypeSchema.optional()
|
|
266
273
|
})
|
|
267
274
|
).default([]),
|
|
268
275
|
/**
|
|
@@ -276,7 +283,7 @@ var LockEntrySchema = z.object({
|
|
|
276
283
|
/** Successful-submission estimate in `costUnit`; may be fractional USD. */
|
|
277
284
|
cost: z.number().finite().nonnegative().default(0),
|
|
278
285
|
/** Unit for `cost`. Defaults preserve pre-unit PixelLab lockfiles. */
|
|
279
|
-
costUnit: z.
|
|
286
|
+
costUnit: z.string().min(1).default("generations"),
|
|
280
287
|
/** Which provider produced this. Absent on entries written before providers. */
|
|
281
288
|
provider: z.string().default("pixellab")
|
|
282
289
|
});
|
|
@@ -409,11 +416,11 @@ var PixelLabClient = class {
|
|
|
409
416
|
* common than the failure mode of retrying (a duplicate object), and a
|
|
410
417
|
* duplicate is visible and free to delete whereas a silent gap is neither.
|
|
411
418
|
*/
|
|
412
|
-
async request(
|
|
419
|
+
async request(path17, init, attempt = 0) {
|
|
413
420
|
const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
|
|
414
421
|
let res;
|
|
415
422
|
try {
|
|
416
|
-
res = await fetch(`${BASE}${
|
|
423
|
+
res = await fetch(`${BASE}${path17}`, {
|
|
417
424
|
...init,
|
|
418
425
|
signal: init?.signal ?? AbortSignal.timeout(this.timeoutMs),
|
|
419
426
|
headers: {
|
|
@@ -425,24 +432,24 @@ var PixelLabClient = class {
|
|
|
425
432
|
} catch (err) {
|
|
426
433
|
if (attempt < MAX_RETRIES) {
|
|
427
434
|
await sleep(backoffMs(attempt));
|
|
428
|
-
return this.request(
|
|
435
|
+
return this.request(path17, init, attempt + 1);
|
|
429
436
|
}
|
|
430
437
|
throw err;
|
|
431
438
|
}
|
|
432
439
|
if (!res.ok && shouldRetry(res.status) && attempt < MAX_RETRIES) {
|
|
433
440
|
const waitMs = retryAfterMs(res.headers.get("retry-after")) ?? backoffMs(attempt);
|
|
434
441
|
await sleep(waitMs);
|
|
435
|
-
return this.request(
|
|
442
|
+
return this.request(path17, init, attempt + 1);
|
|
436
443
|
}
|
|
437
444
|
const text = await res.text();
|
|
438
445
|
if (!res.ok) {
|
|
439
|
-
throw new PixelLabError(`${init?.method ?? "GET"} ${
|
|
446
|
+
throw new PixelLabError(`${init?.method ?? "GET"} ${path17} \u2192 ${res.status}`, res.status, text);
|
|
440
447
|
}
|
|
441
448
|
if (!text) return {};
|
|
442
449
|
try {
|
|
443
450
|
return JSON.parse(text);
|
|
444
451
|
} catch {
|
|
445
|
-
throw new Error(`${init?.method ?? "GET"} ${
|
|
452
|
+
throw new Error(`${init?.method ?? "GET"} ${path17} returned invalid JSON`);
|
|
446
453
|
}
|
|
447
454
|
}
|
|
448
455
|
async balance() {
|
|
@@ -650,7 +657,7 @@ function validateCostEstimate(providerId, value) {
|
|
|
650
657
|
throw new Error(`Provider "${providerId}" returned an invalid cost estimate`);
|
|
651
658
|
}
|
|
652
659
|
const estimate = value;
|
|
653
|
-
if (
|
|
660
|
+
if (typeof estimate.unit !== "string" || !estimate.unit.trim()) {
|
|
654
661
|
throw new Error(`Provider "${providerId}" returned an invalid cost unit`);
|
|
655
662
|
}
|
|
656
663
|
if (!Number.isFinite(estimate.amount) || estimate.amount < 0) {
|
|
@@ -692,18 +699,23 @@ function requireDelete(provider) {
|
|
|
692
699
|
if (!provider.delete) throw new UnsupportedCapabilityError(provider.id, "deleting remote assets");
|
|
693
700
|
return provider.delete.bind(provider);
|
|
694
701
|
}
|
|
702
|
+
function requireSelectCandidate(provider) {
|
|
703
|
+
if (!provider.selectCandidate) {
|
|
704
|
+
throw new UnsupportedCapabilityError(provider.id, "candidate selection");
|
|
705
|
+
}
|
|
706
|
+
return provider.selectCandidate.bind(provider);
|
|
707
|
+
}
|
|
708
|
+
function requireBalance(provider) {
|
|
709
|
+
if (!provider.balance) throw new UnsupportedCapabilityError(provider.id, "account balance");
|
|
710
|
+
return provider.balance.bind(provider);
|
|
711
|
+
}
|
|
695
712
|
function formatCost(unit, amount) {
|
|
696
713
|
if (unit === "free") return "free";
|
|
697
714
|
if (unit === "usd") return `$${amount.toFixed(2)}`;
|
|
698
|
-
return `${amount} generation${amount === 1 ? "" : "s"}`;
|
|
715
|
+
if (unit === "generations") return `${amount} generation${amount === 1 ? "" : "s"}`;
|
|
716
|
+
return `${amount} ${unit}`;
|
|
699
717
|
}
|
|
700
718
|
|
|
701
|
-
// src/providers/pixellab.ts
|
|
702
|
-
import { mkdirSync, existsSync, readFileSync, writeFileSync } from "fs";
|
|
703
|
-
import { randomUUID } from "crypto";
|
|
704
|
-
import os from "os";
|
|
705
|
-
import path from "path";
|
|
706
|
-
|
|
707
719
|
// src/png.ts
|
|
708
720
|
import { deflateSync, inflateSync } from "zlib";
|
|
709
721
|
var SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
@@ -1061,7 +1073,96 @@ function parseHex(hex2) {
|
|
|
1061
1073
|
return { r: n >> 16 & 255, g: n >> 8 & 255, b: n & 255 };
|
|
1062
1074
|
}
|
|
1063
1075
|
|
|
1076
|
+
// src/media.ts
|
|
1077
|
+
var MediaType = {
|
|
1078
|
+
PNG: "image/png",
|
|
1079
|
+
GIF: "image/gif"
|
|
1080
|
+
};
|
|
1081
|
+
var PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
1082
|
+
function mediaExtension(mediaType) {
|
|
1083
|
+
return mediaType === MediaType.GIF ? ".gif" : ".png";
|
|
1084
|
+
}
|
|
1085
|
+
function mediaTypeFromExtension(file) {
|
|
1086
|
+
const lower = file.toLowerCase();
|
|
1087
|
+
if (lower.endsWith(".png")) return MediaType.PNG;
|
|
1088
|
+
if (lower.endsWith(".gif")) return MediaType.GIF;
|
|
1089
|
+
return null;
|
|
1090
|
+
}
|
|
1091
|
+
function detectMediaType(bytes) {
|
|
1092
|
+
if (bytes.subarray(0, PNG_SIGNATURE.length).equals(PNG_SIGNATURE)) return MediaType.PNG;
|
|
1093
|
+
const header = bytes.subarray(0, 6).toString("ascii");
|
|
1094
|
+
if (header === "GIF87a" || header === "GIF89a") return MediaType.GIF;
|
|
1095
|
+
return null;
|
|
1096
|
+
}
|
|
1097
|
+
function validateMedia(bytes, expected) {
|
|
1098
|
+
const actual = detectMediaType(bytes);
|
|
1099
|
+
if (!actual) throw new Error(`response was not a supported PNG or GIF (${bytes.length} bytes)`);
|
|
1100
|
+
if (expected && actual !== expected) {
|
|
1101
|
+
throw new Error(`response was ${actual}, expected ${expected}`);
|
|
1102
|
+
}
|
|
1103
|
+
if (actual === MediaType.PNG) {
|
|
1104
|
+
decodePng(bytes);
|
|
1105
|
+
} else {
|
|
1106
|
+
validateGif(bytes);
|
|
1107
|
+
}
|
|
1108
|
+
return actual;
|
|
1109
|
+
}
|
|
1110
|
+
function validateGif(bytes) {
|
|
1111
|
+
if (bytes.length < 14) throw new Error("invalid GIF: truncated logical screen descriptor");
|
|
1112
|
+
const width = bytes.readUInt16LE(6);
|
|
1113
|
+
const height = bytes.readUInt16LE(8);
|
|
1114
|
+
if (!width || !height) throw new Error("invalid GIF: zero-sized logical screen");
|
|
1115
|
+
const packed = bytes[10];
|
|
1116
|
+
let offset = 13;
|
|
1117
|
+
if (packed & 128) offset += 3 * 2 ** ((packed & 7) + 1);
|
|
1118
|
+
if (offset > bytes.length) throw new Error("invalid GIF: truncated global color table");
|
|
1119
|
+
let sawImage = false;
|
|
1120
|
+
while (offset < bytes.length) {
|
|
1121
|
+
const marker = bytes[offset];
|
|
1122
|
+
if (marker === 59) {
|
|
1123
|
+
if (!sawImage) throw new Error("invalid GIF: contains no image frame");
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
if (marker === 44) {
|
|
1127
|
+
if (offset + 10 > bytes.length) throw new Error("invalid GIF: truncated image descriptor");
|
|
1128
|
+
const imagePacked = bytes[offset + 9];
|
|
1129
|
+
offset += 10;
|
|
1130
|
+
if (imagePacked & 128) offset += 3 * 2 ** ((imagePacked & 7) + 1);
|
|
1131
|
+
if (offset >= bytes.length) throw new Error("invalid GIF: missing image data");
|
|
1132
|
+
offset++;
|
|
1133
|
+
offset = skipSubBlocks(bytes, offset);
|
|
1134
|
+
sawImage = true;
|
|
1135
|
+
continue;
|
|
1136
|
+
}
|
|
1137
|
+
if (marker === 33) {
|
|
1138
|
+
if (offset + 2 > bytes.length) throw new Error("invalid GIF: truncated extension");
|
|
1139
|
+
offset = skipSubBlocks(bytes, offset + 2);
|
|
1140
|
+
continue;
|
|
1141
|
+
}
|
|
1142
|
+
throw new Error(`invalid GIF: unexpected block marker 0x${marker.toString(16)}`);
|
|
1143
|
+
}
|
|
1144
|
+
throw new Error("invalid GIF: missing trailer");
|
|
1145
|
+
}
|
|
1146
|
+
function skipSubBlocks(bytes, start) {
|
|
1147
|
+
let offset = start;
|
|
1148
|
+
for (; ; ) {
|
|
1149
|
+
if (offset >= bytes.length) throw new Error("invalid GIF: truncated data blocks");
|
|
1150
|
+
const size = bytes[offset];
|
|
1151
|
+
offset++;
|
|
1152
|
+
if (size === 0) return offset;
|
|
1153
|
+
offset += size;
|
|
1154
|
+
if (offset > bytes.length) throw new Error("invalid GIF: truncated data block");
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
function cacheFileName(hash, mediaType = MediaType.PNG) {
|
|
1158
|
+
return `${hash}${mediaExtension(mediaType)}`;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1064
1161
|
// src/providers/pixellab.ts
|
|
1162
|
+
import { mkdirSync, existsSync, readFileSync, writeFileSync } from "fs";
|
|
1163
|
+
import { randomUUID } from "crypto";
|
|
1164
|
+
import os from "os";
|
|
1165
|
+
import path from "path";
|
|
1065
1166
|
var PixelLabProvider = class _PixelLabProvider {
|
|
1066
1167
|
constructor(client) {
|
|
1067
1168
|
this.client = client;
|
|
@@ -1111,7 +1212,44 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
1111
1212
|
candidates: spec.generator === "1dir" ? candidateCount(spec.size) : 1
|
|
1112
1213
|
};
|
|
1113
1214
|
}
|
|
1215
|
+
validate(spec, styleImages) {
|
|
1216
|
+
if (spec.generator === "map") {
|
|
1217
|
+
requirePixelLabOption("view", spec.view, ["low top-down", "high top-down", "side"]);
|
|
1218
|
+
requirePixelLabOption("outline", spec.outline, [
|
|
1219
|
+
"single color outline",
|
|
1220
|
+
"selective outline",
|
|
1221
|
+
"lineless"
|
|
1222
|
+
]);
|
|
1223
|
+
requirePixelLabOption("shading", spec.shading, [
|
|
1224
|
+
"flat shading",
|
|
1225
|
+
"basic shading",
|
|
1226
|
+
"medium shading",
|
|
1227
|
+
"detailed shading"
|
|
1228
|
+
]);
|
|
1229
|
+
requirePixelLabOption("detail", spec.detail, [
|
|
1230
|
+
"low detail",
|
|
1231
|
+
"medium detail",
|
|
1232
|
+
"high detail"
|
|
1233
|
+
]);
|
|
1234
|
+
}
|
|
1235
|
+
if ((spec.generator === "map" || spec.generator === "pixflux") && styleImages.length) {
|
|
1236
|
+
throw new Error(`PixelLab ${spec.generator} does not support style images`);
|
|
1237
|
+
}
|
|
1238
|
+
for (const image of styleImages) {
|
|
1239
|
+
if (image.width > 256 || image.height > 256) {
|
|
1240
|
+
throw new Error(
|
|
1241
|
+
`Style image exceeds PixelLab's 256x256 limit (${image.width}x${image.height})`
|
|
1242
|
+
);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
if (spec.tags.length > 20) {
|
|
1246
|
+
throw new Error(
|
|
1247
|
+
`${spec.styleId}/${spec.assetId} resolves to ${spec.tags.length} tags, but PixelLab allows at most 20`
|
|
1248
|
+
);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1114
1251
|
async submit(spec, styleImages) {
|
|
1252
|
+
this.validate(spec, styleImages);
|
|
1115
1253
|
if (spec.generator === "pixflux") {
|
|
1116
1254
|
const swatch = spec.palette.length ? paletteSwatch(spec.palette).toString("base64") : void 0;
|
|
1117
1255
|
const { png } = await this.client.createImagePixflux({
|
|
@@ -1297,6 +1435,11 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
1297
1435
|
await this.client.deleteObject(assetId);
|
|
1298
1436
|
}
|
|
1299
1437
|
};
|
|
1438
|
+
function requirePixelLabOption(name, value, allowed) {
|
|
1439
|
+
if (value != null && !allowed.includes(value)) {
|
|
1440
|
+
throw new Error(`PixelLab map ${name} must be one of: ${allowed.join(", ")}`);
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1300
1443
|
function tilesInIndexOrder(urls) {
|
|
1301
1444
|
return Object.entries(urls).map(([key, url]) => ({ index: Number(key.replace(/^tile_/, "")), url })).filter((tile) => Number.isFinite(tile.index)).sort((a, b) => a.index - b.index);
|
|
1302
1445
|
}
|
|
@@ -1305,6 +1448,319 @@ function firstUrl(urls) {
|
|
|
1305
1448
|
return Object.values(urls).find((u) => typeof u === "string") ?? null;
|
|
1306
1449
|
}
|
|
1307
1450
|
|
|
1451
|
+
// src/providers/retrodiffusion.ts
|
|
1452
|
+
var DEFAULT_BASE_URL = "https://api.retrodiffusion.ai/v1";
|
|
1453
|
+
var RetroDiffusionClient = class {
|
|
1454
|
+
constructor(token, baseUrl = DEFAULT_BASE_URL, request = fetch) {
|
|
1455
|
+
this.token = token;
|
|
1456
|
+
this.baseUrl = baseUrl;
|
|
1457
|
+
this.request = request;
|
|
1458
|
+
}
|
|
1459
|
+
token;
|
|
1460
|
+
baseUrl;
|
|
1461
|
+
request;
|
|
1462
|
+
async submit(body) {
|
|
1463
|
+
const response = await this.call("/inferences", { method: "POST", body: JSON.stringify(body) });
|
|
1464
|
+
const taskId = response.task_id;
|
|
1465
|
+
if (typeof taskId !== "string" || !taskId) {
|
|
1466
|
+
throw new Error("Retro Diffusion did not return an async task id");
|
|
1467
|
+
}
|
|
1468
|
+
return taskId;
|
|
1469
|
+
}
|
|
1470
|
+
async quote(body) {
|
|
1471
|
+
const response = await this.call("/inferences", {
|
|
1472
|
+
method: "POST",
|
|
1473
|
+
body: JSON.stringify({ ...body, check_cost: true })
|
|
1474
|
+
});
|
|
1475
|
+
const amount = response.balance_cost;
|
|
1476
|
+
if (typeof amount !== "number" || !Number.isFinite(amount) || amount < 0) {
|
|
1477
|
+
throw new Error("Retro Diffusion returned an invalid cost quote");
|
|
1478
|
+
}
|
|
1479
|
+
return amount;
|
|
1480
|
+
}
|
|
1481
|
+
async task(id) {
|
|
1482
|
+
return await this.call(`/inferences/tasks/${encodeURIComponent(id)}`);
|
|
1483
|
+
}
|
|
1484
|
+
async balance() {
|
|
1485
|
+
const response = await this.call("/inferences/credits");
|
|
1486
|
+
const balance = response.balance;
|
|
1487
|
+
if (typeof balance !== "number" || !Number.isFinite(balance)) {
|
|
1488
|
+
throw new Error("Retro Diffusion returned an invalid balance");
|
|
1489
|
+
}
|
|
1490
|
+
return balance;
|
|
1491
|
+
}
|
|
1492
|
+
async call(path17, init = {}) {
|
|
1493
|
+
if (!this.token) throw new Error("RD_API_KEY is not set");
|
|
1494
|
+
const response = await this.request(`${this.baseUrl}${path17}`, {
|
|
1495
|
+
...init,
|
|
1496
|
+
headers: {
|
|
1497
|
+
"Content-Type": "application/json",
|
|
1498
|
+
"X-RD-Token": this.token,
|
|
1499
|
+
...init.headers
|
|
1500
|
+
}
|
|
1501
|
+
});
|
|
1502
|
+
const text = await response.text();
|
|
1503
|
+
let value = null;
|
|
1504
|
+
try {
|
|
1505
|
+
value = text ? JSON.parse(text) : null;
|
|
1506
|
+
} catch {
|
|
1507
|
+
value = text;
|
|
1508
|
+
}
|
|
1509
|
+
if (!response.ok) {
|
|
1510
|
+
const retry = response.headers.get("retry-after");
|
|
1511
|
+
const detail = retroError(value);
|
|
1512
|
+
throw new Error(
|
|
1513
|
+
`Retro Diffusion request failed (${response.status}): ${detail}` + (retry ? `; retry after ${retry}s` : "")
|
|
1514
|
+
);
|
|
1515
|
+
}
|
|
1516
|
+
return value;
|
|
1517
|
+
}
|
|
1518
|
+
};
|
|
1519
|
+
var RetroDiffusionProvider = class _RetroDiffusionProvider {
|
|
1520
|
+
constructor(client) {
|
|
1521
|
+
this.client = client;
|
|
1522
|
+
}
|
|
1523
|
+
client;
|
|
1524
|
+
id = "retrodiffusion";
|
|
1525
|
+
static fromEnv() {
|
|
1526
|
+
return new _RetroDiffusionProvider(new RetroDiffusionClient(process.env.RD_API_KEY));
|
|
1527
|
+
}
|
|
1528
|
+
static forOffline() {
|
|
1529
|
+
return new _RetroDiffusionProvider(new RetroDiffusionClient(void 0));
|
|
1530
|
+
}
|
|
1531
|
+
static forDownloads() {
|
|
1532
|
+
return _RetroDiffusionProvider.forOffline();
|
|
1533
|
+
}
|
|
1534
|
+
supports(generator) {
|
|
1535
|
+
return generator === "map" || generator === "pixflux" || generator === "tiles" || generator === "animation";
|
|
1536
|
+
}
|
|
1537
|
+
estimate(spec) {
|
|
1538
|
+
const options = retroOptions(spec);
|
|
1539
|
+
const style = resolvedPromptStyle(spec, options);
|
|
1540
|
+
const count = options.numImages ?? 1;
|
|
1541
|
+
const pixels = spec.width * spec.height;
|
|
1542
|
+
let each;
|
|
1543
|
+
if (style.startsWith("rd_advanced_animation__")) {
|
|
1544
|
+
each = /__(?:custom_action|subtle_motion)$/.test(style) ? 0.25 : 0.14;
|
|
1545
|
+
} else if (style.startsWith("rd_animation__")) {
|
|
1546
|
+
each = /__(?:any_animation|8_dir_rotation)$/.test(style) ? 0.25 : 0.07;
|
|
1547
|
+
} else if (/^rd_tile__tileset(?:_advanced)?$/.test(style)) {
|
|
1548
|
+
each = 0.1;
|
|
1549
|
+
} else if (style.startsWith("rd_pro__")) {
|
|
1550
|
+
each = 0.18;
|
|
1551
|
+
} else if (style.startsWith("rd_fast__")) {
|
|
1552
|
+
each = Math.max(0.015, (pixels + 1e5) / 6e6);
|
|
1553
|
+
} else if (isLowResolutionStyle(style)) {
|
|
1554
|
+
each = Math.max(0.02, (pixels + 13700) / 6e5);
|
|
1555
|
+
} else {
|
|
1556
|
+
each = Math.max(0.025, (pixels + 5e4) / 2e6);
|
|
1557
|
+
}
|
|
1558
|
+
return { unit: "usd", amount: roundUsdEstimate(each * count), candidates: count };
|
|
1559
|
+
}
|
|
1560
|
+
validate(spec, styleImages) {
|
|
1561
|
+
const options = retroOptions(spec);
|
|
1562
|
+
const promptStyle = resolvedPromptStyle(spec, options);
|
|
1563
|
+
const count = options.numImages ?? 1;
|
|
1564
|
+
const isAnimation = /^(?:rd_animation__|rd_advanced_animation__)/.test(promptStyle);
|
|
1565
|
+
const isTile = promptStyle.startsWith("rd_tile__");
|
|
1566
|
+
if (!promptStyle || spec.generator === "animation" && !isAnimation || spec.generator === "tiles" && !isTile || spec.generator !== "animation" && spec.generator !== "tiles" && (isAnimation || isTile)) {
|
|
1567
|
+
throw new Error(
|
|
1568
|
+
`Retro Diffusion style "${promptStyle}" does not match generator "${spec.generator}"`
|
|
1569
|
+
);
|
|
1570
|
+
}
|
|
1571
|
+
if (!Number.isInteger(count) || count < 1 || count > 16) {
|
|
1572
|
+
throw new Error("Retro Diffusion numImages must be a whole number from 1 to 16");
|
|
1573
|
+
}
|
|
1574
|
+
if (spec.width < 16 || spec.height < 16 || spec.width > 512 || spec.height > 512) {
|
|
1575
|
+
throw new Error("Retro Diffusion output dimensions must be between 16 and 512 pixels");
|
|
1576
|
+
}
|
|
1577
|
+
if (styleImages.length > 9) {
|
|
1578
|
+
throw new Error("Retro Diffusion accepts at most 9 reference images");
|
|
1579
|
+
}
|
|
1580
|
+
if (spec.generator === "animation") {
|
|
1581
|
+
if (count !== 1) throw new Error("Retro Diffusion animations currently require numImages: 1");
|
|
1582
|
+
validateAnimation(promptStyle, spec, styleImages, options);
|
|
1583
|
+
} else if (spec.generator === "tiles") {
|
|
1584
|
+
validateTile(promptStyle, spec, styleImages, count, options);
|
|
1585
|
+
} else if (styleImages.length && !/^(?:rd_pro__|user__)/.test(promptStyle)) {
|
|
1586
|
+
throw new Error(
|
|
1587
|
+
`Retro Diffusion style "${promptStyle}" does not accept reference_images; use an RD Pro or user style`
|
|
1588
|
+
);
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
async submit(spec, styleImages) {
|
|
1592
|
+
this.validate(spec, styleImages);
|
|
1593
|
+
const options = retroOptions(spec);
|
|
1594
|
+
const promptStyle = resolvedPromptStyle(spec, options);
|
|
1595
|
+
const animation = spec.generator === "animation";
|
|
1596
|
+
const tiles = spec.generator === "tiles";
|
|
1597
|
+
const body = {
|
|
1598
|
+
prompt: spec.prompt,
|
|
1599
|
+
prompt_style: promptStyle,
|
|
1600
|
+
width: spec.width,
|
|
1601
|
+
height: spec.height,
|
|
1602
|
+
num_images: options.numImages ?? 1,
|
|
1603
|
+
...!animation && !tiles ? { remove_bg: options.removeBg ?? spec.noBackground } : {},
|
|
1604
|
+
...spec.seed != null ? { seed: spec.seed } : {},
|
|
1605
|
+
...animation || tiles ? styleImages[0] ? { input_image: styleImages[0].base64 } : {} : styleImages.length ? { reference_images: styleImages.map((image) => image.base64) } : {},
|
|
1606
|
+
...tiles && styleImages[1] ? { extra_input_image: styleImages[1].base64 } : {},
|
|
1607
|
+
...tiles && options.extraPrompt ? { extra_prompt: options.extraPrompt } : {},
|
|
1608
|
+
...animation && options.framesDuration ? { frames_duration: options.framesDuration } : {},
|
|
1609
|
+
...animation && options.returnSpritesheet ? { return_spritesheet: true } : {},
|
|
1610
|
+
...!animation && options.tileX != null ? { tile_x: options.tileX } : {},
|
|
1611
|
+
...!animation && options.tileY != null ? { tile_y: options.tileY } : {},
|
|
1612
|
+
...spec.palette.length ? { input_palette: paletteSwatch(spec.palette).toString("base64") } : {}
|
|
1613
|
+
};
|
|
1614
|
+
const quoted = await this.client.quote(body);
|
|
1615
|
+
const estimated = this.estimate(spec).amount;
|
|
1616
|
+
if (quoted > estimated + 1e-6) {
|
|
1617
|
+
throw new Error(
|
|
1618
|
+
`Retro Diffusion quoted $${quoted.toFixed(6)}, above the offline estimate $${estimated.toFixed(6)}; no paid request was sent`
|
|
1619
|
+
);
|
|
1620
|
+
}
|
|
1621
|
+
return {
|
|
1622
|
+
jobId: await this.client.submit({ ...body, async: true, upload_outputs: true })
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1625
|
+
async poll(jobId, generator, context) {
|
|
1626
|
+
const task = await this.client.task(jobId);
|
|
1627
|
+
if (task.status === "pending" || task.status === "running") return { status: "processing" };
|
|
1628
|
+
if (task.status === "failed") return { status: "failed", error: retroError(task.error) };
|
|
1629
|
+
if (task.status !== "succeeded") {
|
|
1630
|
+
return { status: "failed", error: `Retro Diffusion returned unknown task status "${String(task.status)}"` };
|
|
1631
|
+
}
|
|
1632
|
+
const options = context?.spec ? retroOptions(context.spec) : {};
|
|
1633
|
+
const mediaType = generator === "animation" && !options.returnSpritesheet ? MediaType.GIF : MediaType.PNG;
|
|
1634
|
+
const sources = resultSources(task.result, mediaType);
|
|
1635
|
+
const urls = sources.map((source) => source.url);
|
|
1636
|
+
if (!urls.length) return { status: "failed", error: "Retro Diffusion task returned no images" };
|
|
1637
|
+
if (urls.length > 1) return { status: "review", candidateUrls: urls };
|
|
1638
|
+
return {
|
|
1639
|
+
status: "ready",
|
|
1640
|
+
objectId: `${jobId}#0`,
|
|
1641
|
+
sourceUrl: urls[0],
|
|
1642
|
+
sources,
|
|
1643
|
+
metadata: {
|
|
1644
|
+
balanceCost: task.result?.balance_cost ?? null,
|
|
1645
|
+
remainingBalance: task.result?.remaining_balance ?? null,
|
|
1646
|
+
mediaType,
|
|
1647
|
+
kind: generator === "animation" ? "animation" : generator === "tiles" ? "tileset" : "image",
|
|
1648
|
+
...context?.spec ? {
|
|
1649
|
+
promptStyle: resolvedPromptStyle(context.spec, options),
|
|
1650
|
+
width: context.spec.width,
|
|
1651
|
+
height: context.spec.height
|
|
1652
|
+
} : {}
|
|
1653
|
+
}
|
|
1654
|
+
};
|
|
1655
|
+
}
|
|
1656
|
+
async selectCandidate(jobId, index) {
|
|
1657
|
+
const task = await this.client.task(jobId);
|
|
1658
|
+
if (task.status !== "succeeded") throw new Error(`Retro Diffusion task ${jobId} is not ready`);
|
|
1659
|
+
const url = resultSources(task.result, MediaType.PNG)[index]?.url;
|
|
1660
|
+
if (!url) throw new Error(`Retro Diffusion task ${jobId} has no candidate at index ${index}`);
|
|
1661
|
+
return { objectId: `${jobId}#${index}`, sourceUrl: url };
|
|
1662
|
+
}
|
|
1663
|
+
async download(url) {
|
|
1664
|
+
const data = /^data:[^;]+;base64,(.+)$/.exec(url)?.[1];
|
|
1665
|
+
if (data) return Buffer.from(data, "base64");
|
|
1666
|
+
const response = await fetch(url);
|
|
1667
|
+
if (!response.ok) throw new Error(`Retro Diffusion download failed (${response.status})`);
|
|
1668
|
+
return Buffer.from(await response.arrayBuffer());
|
|
1669
|
+
}
|
|
1670
|
+
async balance() {
|
|
1671
|
+
return { unit: "usd", remaining: await this.client.balance() };
|
|
1672
|
+
}
|
|
1673
|
+
};
|
|
1674
|
+
function retroOptions(spec) {
|
|
1675
|
+
return spec.providerOptions;
|
|
1676
|
+
}
|
|
1677
|
+
function resultSources(result, mediaType) {
|
|
1678
|
+
const hosted = (result?.output_urls ?? []).filter((url) => typeof url === "string" && url.length > 0).map((url) => ({ url, mediaType }));
|
|
1679
|
+
if (hosted.length) return hosted;
|
|
1680
|
+
return (result?.base64_images ?? []).filter((data) => typeof data === "string" && data.length > 0).map((data) => ({
|
|
1681
|
+
url: `data:${mediaType};base64,${data}`,
|
|
1682
|
+
mediaType
|
|
1683
|
+
}));
|
|
1684
|
+
}
|
|
1685
|
+
function resolvedPromptStyle(spec, options) {
|
|
1686
|
+
return options.promptStyle ?? (spec.generator === "animation" ? "rd_animation__any_animation" : spec.generator === "tiles" ? "rd_tile__tileset" : "rd_plus__default");
|
|
1687
|
+
}
|
|
1688
|
+
function validateAnimation(style, spec, styleImages, options) {
|
|
1689
|
+
if (spec.width !== spec.height) throw new Error("Retro Diffusion animations must be square");
|
|
1690
|
+
if (style.startsWith("rd_advanced_animation__")) {
|
|
1691
|
+
if (styleImages.length !== 1) {
|
|
1692
|
+
throw new Error(`Retro Diffusion advanced animation style "${style}" requires one input image`);
|
|
1693
|
+
}
|
|
1694
|
+
if (spec.width < 32 || spec.width > 256) {
|
|
1695
|
+
throw new Error("Retro Diffusion advanced animations require dimensions from 32 to 256 pixels");
|
|
1696
|
+
}
|
|
1697
|
+
} else if (styleImages.length > 1) {
|
|
1698
|
+
throw new Error("Retro Diffusion prompt animations accept at most one input image");
|
|
1699
|
+
}
|
|
1700
|
+
const exact = style.includes("four_angle_walking") ? 48 : style.endsWith("__small_sprites") ? 32 : style.endsWith("__any_animation") ? 64 : style.endsWith("__big_animation") ? 128 : style.endsWith("__8_dir_rotation") ? 80 : null;
|
|
1701
|
+
if (exact && spec.width !== exact) {
|
|
1702
|
+
throw new Error(`Retro Diffusion style "${style}" requires ${exact}x${exact} dimensions`);
|
|
1703
|
+
}
|
|
1704
|
+
if (style.endsWith("__vfx") && (spec.width < 24 || spec.width > 96)) {
|
|
1705
|
+
throw new Error("Retro Diffusion VFX animations require dimensions from 24 to 96 pixels");
|
|
1706
|
+
}
|
|
1707
|
+
if (options.framesDuration != null && ![4, 6, 8, 10, 12, 16].includes(options.framesDuration)) {
|
|
1708
|
+
throw new Error("Retro Diffusion framesDuration must be 4, 6, 8, 10, 12, or 16");
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
function validateTile(style, spec, styleImages, count, options) {
|
|
1712
|
+
if (spec.width !== spec.height) throw new Error("Retro Diffusion tiles must be square");
|
|
1713
|
+
const size = spec.width;
|
|
1714
|
+
if (/^rd_tile__tileset(?:_advanced)?$/.test(style)) {
|
|
1715
|
+
if (size < 16 || size > 32) throw new Error("Retro Diffusion tilesets require 16\u201332px tiles");
|
|
1716
|
+
if (count !== 1) throw new Error("Retro Diffusion tilesets require numImages: 1");
|
|
1717
|
+
} else if (style === "rd_tile__single_tile" && (size < 16 || size > 64)) {
|
|
1718
|
+
throw new Error("Retro Diffusion single tiles require dimensions from 16 to 64 pixels");
|
|
1719
|
+
} else if (style === "rd_tile__tile_variation") {
|
|
1720
|
+
if (size < 16 || size > 128) throw new Error("Retro Diffusion tile variations require 16\u2013128px tiles");
|
|
1721
|
+
if (styleImages.length !== 1) throw new Error("Retro Diffusion tile variations require one input image");
|
|
1722
|
+
} else if (style === "rd_tile__tile_object" && (size < 16 || size > 96)) {
|
|
1723
|
+
throw new Error("Retro Diffusion tile objects require dimensions from 16 to 96 pixels");
|
|
1724
|
+
} else if (style === "rd_tile__scene_object" && (size < 64 || size > 384)) {
|
|
1725
|
+
throw new Error("Retro Diffusion tile scene objects require dimensions from 64 to 384 pixels");
|
|
1726
|
+
}
|
|
1727
|
+
if (style === "rd_tile__tileset" && styleImages.length > 1) {
|
|
1728
|
+
throw new Error("Retro Diffusion basic tilesets accept at most one input image");
|
|
1729
|
+
}
|
|
1730
|
+
if (style === "rd_tile__tileset_advanced") {
|
|
1731
|
+
if (styleImages.length > 2) throw new Error("Retro Diffusion advanced tilesets accept at most two input images");
|
|
1732
|
+
if (!options.extraPrompt && styleImages.length < 2) {
|
|
1733
|
+
throw new Error("Retro Diffusion advanced tilesets require extraPrompt or a second input image");
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
function isLowResolutionStyle(style) {
|
|
1738
|
+
return /(?:^|__)(?:mc_|low_res|classic|skill_icon|topdown_item)/.test(style);
|
|
1739
|
+
}
|
|
1740
|
+
function roundUsdEstimate(value) {
|
|
1741
|
+
return Math.ceil((value - 1e-9) * 1e3) / 1e3;
|
|
1742
|
+
}
|
|
1743
|
+
function retroError(value) {
|
|
1744
|
+
if (typeof value === "string") return value || "unknown error";
|
|
1745
|
+
if (!value || typeof value !== "object") return "unknown error";
|
|
1746
|
+
const record = value;
|
|
1747
|
+
if (typeof record.message === "string") return record.message;
|
|
1748
|
+
if (typeof record.detail === "string") return record.detail;
|
|
1749
|
+
if (Array.isArray(record.detail)) {
|
|
1750
|
+
const details = record.detail.map((item) => {
|
|
1751
|
+
if (!item || typeof item !== "object") return String(item);
|
|
1752
|
+
const detail = item;
|
|
1753
|
+
return typeof detail.msg === "string" ? detail.msg : JSON.stringify(item);
|
|
1754
|
+
}).filter(Boolean);
|
|
1755
|
+
if (details.length) return details.join("; ");
|
|
1756
|
+
}
|
|
1757
|
+
if (record.detail && typeof record.detail === "object") {
|
|
1758
|
+
const message2 = record.detail.message;
|
|
1759
|
+
if (typeof message2 === "string") return message2;
|
|
1760
|
+
}
|
|
1761
|
+
return JSON.stringify(value);
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1308
1764
|
// src/providers/fake.ts
|
|
1309
1765
|
import { createHash } from "crypto";
|
|
1310
1766
|
var FAKE_PNG = Buffer.from(
|
|
@@ -1449,6 +1905,47 @@ var FakeProvider = class {
|
|
|
1449
1905
|
}
|
|
1450
1906
|
};
|
|
1451
1907
|
|
|
1908
|
+
// src/providers/registry.ts
|
|
1909
|
+
var factories = /* @__PURE__ */ new Map();
|
|
1910
|
+
function registerProvider(factory) {
|
|
1911
|
+
const id = factory.id.trim();
|
|
1912
|
+
if (!id) throw new Error("Provider id cannot be empty");
|
|
1913
|
+
if (factories.has(id)) throw new Error(`Provider "${id}" is already registered`);
|
|
1914
|
+
factories.set(id, factory);
|
|
1915
|
+
}
|
|
1916
|
+
function providerFactory(id) {
|
|
1917
|
+
const factory = factories.get(id);
|
|
1918
|
+
if (!factory) {
|
|
1919
|
+
const available = [...factories.keys()].sort().join(", ") || "(none)";
|
|
1920
|
+
throw new Error(`Unknown provider "${id}". Available providers: ${available}`);
|
|
1921
|
+
}
|
|
1922
|
+
return factory;
|
|
1923
|
+
}
|
|
1924
|
+
function createProvider(id, mode2) {
|
|
1925
|
+
return providerFactory(id).create(mode2);
|
|
1926
|
+
}
|
|
1927
|
+
function availableProviders() {
|
|
1928
|
+
return [...factories.keys()].sort();
|
|
1929
|
+
}
|
|
1930
|
+
registerProvider({
|
|
1931
|
+
id: "pixellab",
|
|
1932
|
+
credentialEnv: "PIXELLAB_API_KEY",
|
|
1933
|
+
create(mode2) {
|
|
1934
|
+
if (mode2 === "online") return PixelLabProvider.fromEnv();
|
|
1935
|
+
if (mode2 === "downloads") return PixelLabProvider.forDownloads();
|
|
1936
|
+
return PixelLabProvider.forOffline();
|
|
1937
|
+
}
|
|
1938
|
+
});
|
|
1939
|
+
registerProvider({
|
|
1940
|
+
id: "retrodiffusion",
|
|
1941
|
+
credentialEnv: "RD_API_KEY",
|
|
1942
|
+
create(mode2) {
|
|
1943
|
+
if (mode2 === "online") return RetroDiffusionProvider.fromEnv();
|
|
1944
|
+
if (mode2 === "downloads") return RetroDiffusionProvider.forDownloads();
|
|
1945
|
+
return RetroDiffusionProvider.forOffline();
|
|
1946
|
+
}
|
|
1947
|
+
});
|
|
1948
|
+
|
|
1452
1949
|
// src/manifest.ts
|
|
1453
1950
|
import { readFile as readFile2 } from "fs/promises";
|
|
1454
1951
|
import { existsSync as existsSync2 } from "fs";
|
|
@@ -1460,12 +1957,16 @@ import { readFile } from "fs/promises";
|
|
|
1460
1957
|
function sha256(data) {
|
|
1461
1958
|
return createHash2("sha256").update(data).digest("hex");
|
|
1462
1959
|
}
|
|
1463
|
-
async function sha256File(
|
|
1464
|
-
return sha256(await readFile(
|
|
1960
|
+
async function sha256File(path17) {
|
|
1961
|
+
return sha256(await readFile(path17));
|
|
1465
1962
|
}
|
|
1466
1963
|
function specHash(spec, styleImageHashes) {
|
|
1467
1964
|
return sha256(
|
|
1468
1965
|
JSON.stringify({
|
|
1966
|
+
// Preserve every existing PixelLab hash while making a provider switch
|
|
1967
|
+
// invalidate the spec. Older manifests implicitly mean pixellab.
|
|
1968
|
+
provider: spec.provider === "pixellab" ? void 0 : spec.provider,
|
|
1969
|
+
providerOptions: Object.keys(spec.providerOptions).length > 0 ? spec.providerOptions : void 0,
|
|
1469
1970
|
generator: spec.generator,
|
|
1470
1971
|
prompt: spec.prompt,
|
|
1471
1972
|
width: spec.width,
|
|
@@ -1479,7 +1980,7 @@ function specHash(spec, styleImageHashes) {
|
|
|
1479
1980
|
// `noBackground` only reaches the wire for pixflux; the tile fields are
|
|
1480
1981
|
// undefined for every other generator. `tileSize` is intentionally
|
|
1481
1982
|
// absent — width/height are derived from it, so it is already covered.
|
|
1482
|
-
noBackground: spec.generator === "pixflux" ? spec.noBackground : void 0,
|
|
1983
|
+
noBackground: spec.generator === "pixflux" || spec.provider !== "pixellab" ? spec.noBackground : void 0,
|
|
1483
1984
|
tileType: spec.tileType,
|
|
1484
1985
|
tileView: spec.tileView,
|
|
1485
1986
|
tileFeature: spec.tileFeature,
|
|
@@ -1519,6 +2020,7 @@ ${unknownReferences.map((i) => ` ${i}`).join("\n")}`);
|
|
|
1519
2020
|
}
|
|
1520
2021
|
async function resolveSpecs(loaded, filter) {
|
|
1521
2022
|
const { manifest, root } = loaded;
|
|
2023
|
+
const activeProvider = filter?.provider ?? createProvider(manifest.provider, "offline");
|
|
1522
2024
|
const specs = [];
|
|
1523
2025
|
const styleIds = Object.keys(manifest.styles).filter(
|
|
1524
2026
|
(id) => !filter?.styles?.length || filter.styles.includes(id)
|
|
@@ -1544,10 +2046,8 @@ async function resolveSpecs(loaded, filter) {
|
|
|
1544
2046
|
const buf = await readFile2(abs);
|
|
1545
2047
|
const metadata = imageMetadata(buf);
|
|
1546
2048
|
if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${abs}`);
|
|
1547
|
-
if (metadata.width < 1 || metadata.height < 1
|
|
1548
|
-
throw new Error(
|
|
1549
|
-
`Style image exceeds the API's 256x256 limit: ${abs} (${metadata.width}x${metadata.height})`
|
|
1550
|
-
);
|
|
2049
|
+
if (metadata.width < 1 || metadata.height < 1) {
|
|
2050
|
+
throw new Error(`Style image has invalid dimensions: ${abs}`);
|
|
1551
2051
|
}
|
|
1552
2052
|
hit = { base64: buf.toString("base64"), hash: sha256(buf), ...metadata };
|
|
1553
2053
|
styleImageCache.set(abs, hit);
|
|
@@ -1567,8 +2067,8 @@ async function resolveSpecs(loaded, filter) {
|
|
|
1567
2067
|
if (filter?.assets?.length && !filter.assets.includes(assetId)) continue;
|
|
1568
2068
|
if (asset.styles.length && !asset.styles.includes(styleId)) continue;
|
|
1569
2069
|
const generator = style.generator;
|
|
1570
|
-
if (
|
|
1571
|
-
throw new Error(`Provider "${
|
|
2070
|
+
if (!activeProvider.supports(generator)) {
|
|
2071
|
+
throw new Error(`Provider "${activeProvider.id}" does not support generator "${generator}"`);
|
|
1572
2072
|
}
|
|
1573
2073
|
let width;
|
|
1574
2074
|
let height;
|
|
@@ -1595,6 +2095,8 @@ async function resolveSpecs(loaded, filter) {
|
|
|
1595
2095
|
const base = {
|
|
1596
2096
|
styleId,
|
|
1597
2097
|
assetId,
|
|
2098
|
+
provider: activeProvider.id,
|
|
2099
|
+
providerOptions: style.providerOptions[activeProvider.id] ?? {},
|
|
1598
2100
|
generator,
|
|
1599
2101
|
prompt,
|
|
1600
2102
|
width,
|
|
@@ -1626,11 +2128,6 @@ async function resolveSpecs(loaded, filter) {
|
|
|
1626
2128
|
`style:${styleId}`
|
|
1627
2129
|
])
|
|
1628
2130
|
];
|
|
1629
|
-
if (tags.length > 20) {
|
|
1630
|
-
throw new Error(
|
|
1631
|
-
`${styleId}/${assetId} resolves to ${tags.length} tags, but PixelLab allows at most 20`
|
|
1632
|
-
);
|
|
1633
|
-
}
|
|
1634
2131
|
const resolved = {
|
|
1635
2132
|
...base,
|
|
1636
2133
|
root,
|
|
@@ -1639,12 +2136,15 @@ async function resolveSpecs(loaded, filter) {
|
|
|
1639
2136
|
source: asset.source,
|
|
1640
2137
|
specHash: specHash(base, styleImageHashes)
|
|
1641
2138
|
};
|
|
1642
|
-
|
|
1643
|
-
const
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
2139
|
+
const resolvedImages = style.styleImages.map((image) => {
|
|
2140
|
+
const hit = styleImageCache.get(path2.resolve(root, image.path));
|
|
2141
|
+
return { base64: hit.base64, width: hit.width, height: hit.height, format: hit.format };
|
|
2142
|
+
});
|
|
2143
|
+
activeProvider.validate?.(resolved, resolvedImages);
|
|
2144
|
+
const estimate = validateCostEstimate(activeProvider.id, activeProvider.estimate(resolved));
|
|
2145
|
+
resolved.cost = estimate.amount;
|
|
2146
|
+
resolved.costUnit = estimate.unit;
|
|
2147
|
+
resolved.candidates = estimate.candidates;
|
|
1648
2148
|
specs.push(resolved);
|
|
1649
2149
|
}
|
|
1650
2150
|
}
|
|
@@ -1668,10 +2168,8 @@ async function resolveStyleImages(loaded, styleId) {
|
|
|
1668
2168
|
const buf = await readFile2(file);
|
|
1669
2169
|
const metadata = imageMetadata(buf);
|
|
1670
2170
|
if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${file}`);
|
|
1671
|
-
if (metadata.width < 1 || metadata.height < 1
|
|
1672
|
-
throw new Error(
|
|
1673
|
-
`Style image exceeds the API's 256x256 limit: ${file} (${metadata.width}x${metadata.height})`
|
|
1674
|
-
);
|
|
2171
|
+
if (metadata.width < 1 || metadata.height < 1) {
|
|
2172
|
+
throw new Error(`Style image has invalid dimensions: ${file}`);
|
|
1675
2173
|
}
|
|
1676
2174
|
out.push({ base64: buf.toString("base64"), ...metadata });
|
|
1677
2175
|
}
|
|
@@ -1873,12 +2371,13 @@ async function acquireFileLock(file) {
|
|
|
1873
2371
|
function spendByUnit(lock) {
|
|
1874
2372
|
const totals = { generations: 0, usd: 0, free: 0 };
|
|
1875
2373
|
for (const entry of Object.values(lock.entries)) {
|
|
1876
|
-
|
|
2374
|
+
const unit = entry.costUnit ?? "generations";
|
|
2375
|
+
totals[unit] = (totals[unit] ?? 0) + (entry.cost ?? 0);
|
|
1877
2376
|
}
|
|
1878
2377
|
return totals;
|
|
1879
2378
|
}
|
|
1880
2379
|
function totalSpend(lock, unit = "generations") {
|
|
1881
|
-
return spendByUnit(lock)[unit];
|
|
2380
|
+
return spendByUnit(lock)[unit] ?? 0;
|
|
1882
2381
|
}
|
|
1883
2382
|
|
|
1884
2383
|
// src/outputs.ts
|
|
@@ -1895,16 +2394,16 @@ function resolveOutputPath(recordedPath, manifestDir) {
|
|
|
1895
2394
|
if (path4.win32.isAbsolute(recordedPath)) return recordedPath;
|
|
1896
2395
|
return path4.resolve(manifestDir, recordedPath.split(/[\\/]/).join(path4.sep));
|
|
1897
2396
|
}
|
|
1898
|
-
function expectedOutputPath(spec, role, index, total) {
|
|
1899
|
-
if (total === 1) return spec.outFile;
|
|
2397
|
+
function expectedOutputPath(spec, role, index, total, mediaType) {
|
|
1900
2398
|
const originalExt = path4.extname(spec.outFile);
|
|
1901
|
-
const ext = originalExt || ".png";
|
|
2399
|
+
const ext = mediaType ? mediaExtension(mediaType) : originalExt || ".png";
|
|
1902
2400
|
const stem = originalExt ? spec.outFile.slice(0, -originalExt.length) : spec.outFile;
|
|
2401
|
+
if (total === 1) return `${stem}${ext}`;
|
|
1903
2402
|
const safeRole = (role ?? fallbackOutputRole(index)).replace(/[^a-zA-Z0-9_-]+/g, "-");
|
|
1904
2403
|
return `${stem}-${safeRole}${ext}`;
|
|
1905
2404
|
}
|
|
1906
2405
|
function currentOutputPath(output, spec, index, total) {
|
|
1907
|
-
return expectedOutputPath(spec, output.role, index, total);
|
|
2406
|
+
return expectedOutputPath(spec, output.role, index, total, output.mediaType);
|
|
1908
2407
|
}
|
|
1909
2408
|
function currentEntryOutputPath(entry, spec, index) {
|
|
1910
2409
|
const output = entry.outputs[index];
|
|
@@ -2705,10 +3204,10 @@ function isSha256Hash(value) {
|
|
|
2705
3204
|
function parseCache(value) {
|
|
2706
3205
|
return HashCacheSchema.parse(value);
|
|
2707
3206
|
}
|
|
2708
|
-
async function loadCache(
|
|
2709
|
-
if (!existsSync6(
|
|
3207
|
+
async function loadCache(path17) {
|
|
3208
|
+
if (!existsSync6(path17)) return { version: 1, hashes: {} };
|
|
2710
3209
|
try {
|
|
2711
|
-
const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile6(
|
|
3210
|
+
const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile6(path17, "utf8")));
|
|
2712
3211
|
if (!parsed.success) return { version: 1, hashes: {} };
|
|
2713
3212
|
return {
|
|
2714
3213
|
version: 1,
|
|
@@ -2720,18 +3219,18 @@ async function loadCache(path15) {
|
|
|
2720
3219
|
return { version: 1, hashes: {} };
|
|
2721
3220
|
}
|
|
2722
3221
|
}
|
|
2723
|
-
async function saveCache(
|
|
3222
|
+
async function saveCache(path17, cache) {
|
|
2724
3223
|
const sorted = {};
|
|
2725
3224
|
for (const key of Object.keys(cache.hashes).sort()) {
|
|
2726
3225
|
const hash = cache.hashes[key];
|
|
2727
3226
|
if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
|
|
2728
3227
|
sorted[key] = hash;
|
|
2729
3228
|
}
|
|
2730
|
-
await mkdir3(pathModule.dirname(pathModule.resolve(
|
|
2731
|
-
const tmp = `${
|
|
3229
|
+
await mkdir3(pathModule.dirname(pathModule.resolve(path17)), { recursive: true });
|
|
3230
|
+
const tmp = `${path17}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
2732
3231
|
try {
|
|
2733
3232
|
await writeFile3(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
|
|
2734
|
-
await rename3(tmp,
|
|
3233
|
+
await rename3(tmp, path17);
|
|
2735
3234
|
} finally {
|
|
2736
3235
|
await rm3(tmp, { force: true });
|
|
2737
3236
|
}
|
|
@@ -2752,7 +3251,6 @@ function cachePathFor(lockPath) {
|
|
|
2752
3251
|
}
|
|
2753
3252
|
|
|
2754
3253
|
// src/pipeline/cache-health.ts
|
|
2755
|
-
var PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
2756
3254
|
async function inspectCaches(lock, lockPath, options = {}) {
|
|
2757
3255
|
if (options.prune && !existsSync7(lockPath)) {
|
|
2758
3256
|
throw new Error(`Refusing to prune without an existing lockfile at ${path8.resolve(lockPath)}`);
|
|
@@ -2821,7 +3319,8 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
2821
3319
|
}
|
|
2822
3320
|
report.files++;
|
|
2823
3321
|
const file = path8.join(contentDir, entry.name);
|
|
2824
|
-
const
|
|
3322
|
+
const mediaType = mediaTypeFromExtension(entry.name);
|
|
3323
|
+
const expected = mediaType ? entry.name.slice(0, -4) : "";
|
|
2825
3324
|
let bytes;
|
|
2826
3325
|
try {
|
|
2827
3326
|
bytes = await readFile7(file);
|
|
@@ -2834,11 +3333,7 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
2834
3333
|
continue;
|
|
2835
3334
|
}
|
|
2836
3335
|
if (!isSha256Hash(expected)) {
|
|
2837
|
-
report.invalid.push({ name: entry.name, reason: "filename is not <sha256>.png" });
|
|
2838
|
-
continue;
|
|
2839
|
-
}
|
|
2840
|
-
if (!bytes.subarray(0, 8).equals(PNG_SIGNATURE)) {
|
|
2841
|
-
report.invalid.push({ name: entry.name, reason: "not a PNG" });
|
|
3336
|
+
report.invalid.push({ name: entry.name, reason: "filename is not <sha256>.png or <sha256>.gif" });
|
|
2842
3337
|
continue;
|
|
2843
3338
|
}
|
|
2844
3339
|
if (sha256(bytes) !== expected) {
|
|
@@ -2846,11 +3341,11 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
2846
3341
|
continue;
|
|
2847
3342
|
}
|
|
2848
3343
|
try {
|
|
2849
|
-
|
|
3344
|
+
validateMedia(bytes, mediaType);
|
|
2850
3345
|
} catch (err) {
|
|
2851
3346
|
report.invalid.push({
|
|
2852
3347
|
name: entry.name,
|
|
2853
|
-
reason: `invalid PNG: ${err instanceof Error ? err.message : String(err)}`
|
|
3348
|
+
reason: `invalid ${mediaType === "image/gif" ? "GIF" : "PNG"}: ${err instanceof Error ? err.message : String(err)}`
|
|
2854
3349
|
});
|
|
2855
3350
|
continue;
|
|
2856
3351
|
}
|
|
@@ -3439,7 +3934,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
3439
3934
|
async function pruneInFlight() {
|
|
3440
3935
|
for (const [id, spec] of [...inFlight]) {
|
|
3441
3936
|
try {
|
|
3442
|
-
const state = await provider.poll(id, spec.generator, spec);
|
|
3937
|
+
const state = await provider.poll(id, spec.generator, { spec, tileFeature: spec.tileFeature });
|
|
3443
3938
|
if (state.status !== "processing") inFlight.delete(id);
|
|
3444
3939
|
lastSlotError = null;
|
|
3445
3940
|
} catch (err) {
|
|
@@ -3491,7 +3986,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
3491
3986
|
await saveLock(lockPath, lock);
|
|
3492
3987
|
lastSubmitAt = Date.now();
|
|
3493
3988
|
try {
|
|
3494
|
-
const refs =
|
|
3989
|
+
const refs = styleImages.get(spec.styleId) ?? [];
|
|
3495
3990
|
const { jobId } = await provider.submit(spec, refs);
|
|
3496
3991
|
upsert(lock, key, {
|
|
3497
3992
|
jobId,
|
|
@@ -3541,7 +4036,8 @@ async function poll(provider, lock, lockPath, opts = {}) {
|
|
|
3541
4036
|
try {
|
|
3542
4037
|
const currentSpec = specByKey.get(key);
|
|
3543
4038
|
const state = await provider.poll(entry.jobId, entry.generator, {
|
|
3544
|
-
tileFeature: entry.tileFeature ?? currentSpec?.tileFeature
|
|
4039
|
+
tileFeature: entry.tileFeature ?? currentSpec?.tileFeature,
|
|
4040
|
+
spec: currentSpec
|
|
3545
4041
|
});
|
|
3546
4042
|
if (state.status === "review") {
|
|
3547
4043
|
upsert(lock, key, { status: "review", reviewObjectId: entry.jobId });
|
|
@@ -3579,7 +4075,6 @@ async function poll(provider, lock, lockPath, opts = {}) {
|
|
|
3579
4075
|
import { existsSync as existsSync8 } from "fs";
|
|
3580
4076
|
import { mkdir as mkdir4, readFile as readFile8, rename as rename4, rm as rm5, writeFile as writeFile4 } from "fs/promises";
|
|
3581
4077
|
import path10 from "path";
|
|
3582
|
-
var PNG_SIGNATURE2 = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
3583
4078
|
async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
3584
4079
|
const log = opts.onProgress ?? (() => {
|
|
3585
4080
|
});
|
|
@@ -3611,7 +4106,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
3611
4106
|
result.skipped++;
|
|
3612
4107
|
continue;
|
|
3613
4108
|
}
|
|
3614
|
-
const sources = entry.sourceUrls?.length ? entry.sourceUrls : entry.sourceUrl ? [{ url: entry.sourceUrl }] : opts.repair && entry.outputs.length ? entry.outputs.map((o) => ({ url: "", role: o.role })) : [];
|
|
4109
|
+
const sources = entry.sourceUrls?.length ? entry.sourceUrls : entry.sourceUrl ? [{ url: entry.sourceUrl }] : opts.repair && entry.outputs.length ? entry.outputs.map((o) => ({ url: "", role: o.role, mediaType: o.mediaType })) : [];
|
|
3615
4110
|
if (!sources.length) {
|
|
3616
4111
|
upsert(lock, key, {
|
|
3617
4112
|
status: "download-failed",
|
|
@@ -3627,7 +4122,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
3627
4122
|
const recorded = entry.outputs.find(
|
|
3628
4123
|
(o) => source.role ? o.role === source.role : !o.role && sources.length === 1
|
|
3629
4124
|
);
|
|
3630
|
-
const target = recorded ? resolveOutputPath(recorded.path, spec.root) : expectedOutputPath(spec, source.role, index, sources.length);
|
|
4125
|
+
const target = recorded ? resolveOutputPath(recorded.path, spec.root) : expectedOutputPath(spec, source.role, index, sources.length, source.mediaType);
|
|
3631
4126
|
if (existsSync8(target)) {
|
|
3632
4127
|
if (!recorded) {
|
|
3633
4128
|
throw new Error(`refusing to overwrite untracked output ${target}`);
|
|
@@ -3635,11 +4130,19 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
3635
4130
|
if (await sha256File(target) !== recorded.sha256) {
|
|
3636
4131
|
throw new Error(`refusing to overwrite modified output ${target}`);
|
|
3637
4132
|
}
|
|
3638
|
-
if (cacheDir)
|
|
4133
|
+
if (cacheDir) {
|
|
4134
|
+
await cacheMedia(
|
|
4135
|
+
cacheDir,
|
|
4136
|
+
await readFile8(target),
|
|
4137
|
+
recorded.mediaType ?? MediaType.PNG,
|
|
4138
|
+
recorded.sha256
|
|
4139
|
+
);
|
|
4140
|
+
}
|
|
3639
4141
|
outputs.push({ ...recorded, path: portableOutputPath(target, spec.root) });
|
|
3640
4142
|
continue;
|
|
3641
4143
|
}
|
|
3642
|
-
|
|
4144
|
+
const expectedMediaType = source.mediaType ?? recorded?.mediaType ?? MediaType.PNG;
|
|
4145
|
+
let buf = recorded && cacheDir ? await readCachedMedia(cacheDir, recorded.sha256, expectedMediaType) : null;
|
|
3643
4146
|
if (buf) {
|
|
3644
4147
|
log(` cached ${path10.relative(process.cwd(), target)}`);
|
|
3645
4148
|
} else {
|
|
@@ -3648,24 +4151,25 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
3648
4151
|
}
|
|
3649
4152
|
buf = await provider.download(source.url);
|
|
3650
4153
|
}
|
|
3651
|
-
|
|
3652
|
-
throw new Error(`response for ${source.role ?? "asset"} was not a PNG (${buf.length} bytes)`);
|
|
3653
|
-
}
|
|
4154
|
+
let mediaType;
|
|
3654
4155
|
try {
|
|
3655
|
-
|
|
4156
|
+
mediaType = validateMedia(buf, expectedMediaType);
|
|
3656
4157
|
} catch (err) {
|
|
4158
|
+
const label = expectedMediaType === MediaType.GIF ? "GIF" : "PNG";
|
|
4159
|
+
const mismatch = detectMediaType(buf) !== expectedMediaType;
|
|
3657
4160
|
throw new Error(
|
|
3658
|
-
`response for ${source.role ?? "asset"} was not a valid
|
|
4161
|
+
`response for ${source.role ?? "asset"} was not ${mismatch ? "a" : "a valid"} ${label}` + (mismatch ? ` (${buf.length} bytes)` : `: ${err instanceof Error ? err.message : String(err)}`)
|
|
3659
4162
|
);
|
|
3660
4163
|
}
|
|
3661
|
-
if (cacheDir) await
|
|
4164
|
+
if (cacheDir) await cacheMedia(cacheDir, buf, mediaType);
|
|
3662
4165
|
await mkdir4(path10.dirname(target), { recursive: true });
|
|
3663
4166
|
const tmp = `${target}.pixelkiln.tmp`;
|
|
3664
4167
|
await writeFile4(tmp, buf);
|
|
3665
4168
|
outputs.push({
|
|
3666
4169
|
path: portableOutputPath(target, spec.root),
|
|
3667
4170
|
sha256: sha256(buf),
|
|
3668
|
-
...source.role ? { role: source.role } : {}
|
|
4171
|
+
...source.role ? { role: source.role } : {},
|
|
4172
|
+
mediaType
|
|
3669
4173
|
});
|
|
3670
4174
|
upsert(lock, key, { outputs: mergeOutputs(entry.outputs, outputs) });
|
|
3671
4175
|
await saveLock(lockPath, lock);
|
|
@@ -3702,22 +4206,23 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
3702
4206
|
await saveLock(lockPath, lock);
|
|
3703
4207
|
return result;
|
|
3704
4208
|
}
|
|
3705
|
-
async function
|
|
3706
|
-
const file = path10.join(cacheDir,
|
|
4209
|
+
async function readCachedMedia(cacheDir, hash, mediaType) {
|
|
4210
|
+
const file = path10.join(cacheDir, cacheFileName(hash, mediaType));
|
|
3707
4211
|
if (!existsSync8(file)) return null;
|
|
3708
4212
|
try {
|
|
3709
4213
|
const buf = await readFile8(file);
|
|
3710
|
-
if (
|
|
3711
|
-
|
|
4214
|
+
if (sha256(buf) !== hash) return null;
|
|
4215
|
+
validateMedia(buf, mediaType);
|
|
3712
4216
|
return buf;
|
|
3713
4217
|
} catch {
|
|
3714
4218
|
return null;
|
|
3715
4219
|
}
|
|
3716
4220
|
}
|
|
3717
|
-
async function
|
|
4221
|
+
async function cacheMedia(cacheDir, buf, mediaType, knownHash) {
|
|
3718
4222
|
const hash = knownHash ?? sha256(buf);
|
|
3719
|
-
|
|
3720
|
-
|
|
4223
|
+
validateMedia(buf, mediaType);
|
|
4224
|
+
const file = path10.join(cacheDir, cacheFileName(hash, mediaType));
|
|
4225
|
+
if (await readCachedMedia(cacheDir, hash, mediaType)) return;
|
|
3721
4226
|
await mkdir4(cacheDir, { recursive: true });
|
|
3722
4227
|
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
3723
4228
|
await writeFile4(tmp, buf);
|
|
@@ -3822,7 +4327,10 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
3822
4327
|
const hasSource = Boolean(entry.sourceUrl || entry.sourceUrls?.length);
|
|
3823
4328
|
let hasCache = false;
|
|
3824
4329
|
for (const output of entry.outputs) {
|
|
3825
|
-
const cached = path11.join(
|
|
4330
|
+
const cached = path11.join(
|
|
4331
|
+
cacheDir,
|
|
4332
|
+
cacheFileName(output.sha256, output.mediaType ?? MediaType.PNG)
|
|
4333
|
+
);
|
|
3826
4334
|
if (existsSync9(cached) && await sha256File(cached) === output.sha256) {
|
|
3827
4335
|
hasCache = true;
|
|
3828
4336
|
break;
|
|
@@ -3852,11 +4360,14 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
3852
4360
|
add(
|
|
3853
4361
|
"provider",
|
|
3854
4362
|
"error",
|
|
3855
|
-
opts.apiKeyPresent === false ? "PIXELLAB_API_KEY is not configured
|
|
4363
|
+
opts.apiKeyPresent === false ? `${opts.credentialEnv ?? "PIXELLAB_API_KEY"} is not configured` : "provider is not configured"
|
|
3856
4364
|
);
|
|
4365
|
+
} else if (!opts.provider.balance) {
|
|
4366
|
+
add("provider", "ok", `${opts.provider.id} configured; balance reporting unavailable`);
|
|
3857
4367
|
} else {
|
|
4368
|
+
const balanceFn = opts.provider.balance.bind(opts.provider);
|
|
3858
4369
|
try {
|
|
3859
|
-
const balance = await
|
|
4370
|
+
const balance = await balanceFn();
|
|
3860
4371
|
add("provider", "ok", `${opts.provider.id} reachable; ${balance.remaining} ${balance.unit} remaining`);
|
|
3861
4372
|
} catch (err) {
|
|
3862
4373
|
add("provider", "error", `provider connectivity failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -4347,7 +4858,7 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
|
|
|
4347
4858
|
const entry = lock.entries[key];
|
|
4348
4859
|
if (!group || !entry?.reviewObjectId) continue;
|
|
4349
4860
|
if (!Number.isInteger(index) || index < 0 || index >= group.frameUrls.length) continue;
|
|
4350
|
-
const { objectId, sourceUrl } = await provider
|
|
4861
|
+
const { objectId, sourceUrl } = await requireSelectCandidate(provider)(
|
|
4351
4862
|
entry.reviewObjectId,
|
|
4352
4863
|
index,
|
|
4353
4864
|
`asset:${entry.assetId}`,
|
|
@@ -4437,6 +4948,7 @@ function buildManifest(name, styleId, generator, outDir, scanned) {
|
|
|
4437
4948
|
}
|
|
4438
4949
|
return {
|
|
4439
4950
|
name,
|
|
4951
|
+
provider: "pixellab",
|
|
4440
4952
|
styles: {
|
|
4441
4953
|
[styleId]: {
|
|
4442
4954
|
generator,
|
|
@@ -4449,7 +4961,8 @@ function buildManifest(name, styleId, generator, outDir, scanned) {
|
|
|
4449
4961
|
styleImages: [],
|
|
4450
4962
|
palette: [],
|
|
4451
4963
|
outDir,
|
|
4452
|
-
tags: [name]
|
|
4964
|
+
tags: [name],
|
|
4965
|
+
providerOptions: {}
|
|
4453
4966
|
}
|
|
4454
4967
|
},
|
|
4455
4968
|
assets
|
|
@@ -4464,6 +4977,7 @@ function mode(values) {
|
|
|
4464
4977
|
// src/pipeline/salvage.ts
|
|
4465
4978
|
import { readFile as readFile11 } from "fs/promises";
|
|
4466
4979
|
import { existsSync as existsSync12 } from "fs";
|
|
4980
|
+
import path14 from "path";
|
|
4467
4981
|
async function loadClaims(lockPaths) {
|
|
4468
4982
|
const claimed = /* @__PURE__ */ new Set();
|
|
4469
4983
|
for (const p of lockPaths) {
|
|
@@ -4516,6 +5030,26 @@ function matchOrphanStyle(prompt, manifest) {
|
|
|
4516
5030
|
if (styleIds.length <= 1) return styleIds[0] ?? null;
|
|
4517
5031
|
return matchStyleByPattern(prompt, manifest);
|
|
4518
5032
|
}
|
|
5033
|
+
async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, claimPaths) {
|
|
5034
|
+
const own = path14.resolve(ownManifestPath);
|
|
5035
|
+
const siblingManifestPaths = [
|
|
5036
|
+
.../* @__PURE__ */ new Set([
|
|
5037
|
+
...workspaceManifestPaths,
|
|
5038
|
+
...claimPaths.map((c) => path14.join(path14.dirname(path14.resolve(c)), "pixelkiln.manifest.json"))
|
|
5039
|
+
])
|
|
5040
|
+
];
|
|
5041
|
+
const siblings = [];
|
|
5042
|
+
for (const siblingManifestPath of siblingManifestPaths) {
|
|
5043
|
+
if (path14.resolve(siblingManifestPath) === own) continue;
|
|
5044
|
+
if (!existsSync12(siblingManifestPath)) continue;
|
|
5045
|
+
try {
|
|
5046
|
+
const { manifest } = await loadManifest(siblingManifestPath);
|
|
5047
|
+
siblings.push({ label: path14.basename(path14.dirname(siblingManifestPath)), manifest });
|
|
5048
|
+
} catch {
|
|
5049
|
+
}
|
|
5050
|
+
}
|
|
5051
|
+
return siblings;
|
|
5052
|
+
}
|
|
4519
5053
|
function groupOrphansByStyle(orphans, manifest, siblings = []) {
|
|
4520
5054
|
const matched = /* @__PURE__ */ new Map();
|
|
4521
5055
|
const elsewhere = /* @__PURE__ */ new Map();
|
|
@@ -4627,7 +5161,7 @@ async function applyTags(provider, decisions, existing, opts = {}) {
|
|
|
4627
5161
|
|
|
4628
5162
|
// src/pick/salvage-server.ts
|
|
4629
5163
|
import { mkdir as mkdir5, writeFile as writeFile7, readFile as readFile12 } from "fs/promises";
|
|
4630
|
-
import
|
|
5164
|
+
import path15 from "path";
|
|
4631
5165
|
|
|
4632
5166
|
// src/pick/salvage-sheet.ts
|
|
4633
5167
|
var escapeHtml2 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
@@ -4786,13 +5320,13 @@ refresh();
|
|
|
4786
5320
|
}
|
|
4787
5321
|
|
|
4788
5322
|
// src/pick/salvage-server.ts
|
|
4789
|
-
var
|
|
5323
|
+
var PNG_SIGNATURE2 = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
4790
5324
|
async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
4791
5325
|
const log = opts.onProgress ?? (() => {
|
|
4792
5326
|
});
|
|
4793
5327
|
const html = renderSalvageSheet(orphans, {
|
|
4794
5328
|
styleId: ctx.styleId,
|
|
4795
|
-
importDir:
|
|
5329
|
+
importDir: path15.relative(process.cwd(), ctx.importDir) || "."
|
|
4796
5330
|
});
|
|
4797
5331
|
const byId = new Map(orphans.map((o) => [o.id, o]));
|
|
4798
5332
|
const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
|
|
@@ -4818,12 +5352,12 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
4818
5352
|
if (decision.action === "import") {
|
|
4819
5353
|
try {
|
|
4820
5354
|
const buf = await provider.download(orphan.previewUrl);
|
|
4821
|
-
if (!buf.subarray(0, 8).equals(
|
|
5355
|
+
if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
|
|
4822
5356
|
decodePng(buf);
|
|
4823
5357
|
const assetId = idFromPrompt(orphan.prompt, taken);
|
|
4824
|
-
const rel =
|
|
4825
|
-
const outFile =
|
|
4826
|
-
await mkdir5(
|
|
5358
|
+
const rel = path15.join("_salvaged", `${assetId}.png`);
|
|
5359
|
+
const outFile = path15.resolve(ctx.importDir, rel);
|
|
5360
|
+
await mkdir5(path15.dirname(outFile), { recursive: true });
|
|
4827
5361
|
await writeFile7(outFile, buf);
|
|
4828
5362
|
ctx.manifest.assets[assetId] = {
|
|
4829
5363
|
prompt: orphan.prompt,
|
|
@@ -4850,7 +5384,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
4850
5384
|
error: null,
|
|
4851
5385
|
sourceUrl: orphan.previewUrl,
|
|
4852
5386
|
outputs: [{
|
|
4853
|
-
path: portableOutputPath(outFile,
|
|
5387
|
+
path: portableOutputPath(outFile, path15.dirname(ctx.manifestPath)),
|
|
4854
5388
|
sha256: sha256(buf)
|
|
4855
5389
|
}],
|
|
4856
5390
|
submittedAt: orphan.createdAt,
|
|
@@ -4882,6 +5416,233 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
4882
5416
|
}
|
|
4883
5417
|
});
|
|
4884
5418
|
}
|
|
5419
|
+
|
|
5420
|
+
// src/workspace.ts
|
|
5421
|
+
import { mkdir as mkdir6, readFile as readFile13, rename as rename5, rm as rm6, writeFile as writeFile8 } from "fs/promises";
|
|
5422
|
+
import { existsSync as existsSync13 } from "fs";
|
|
5423
|
+
import path16 from "path";
|
|
5424
|
+
import { z as z4 } from "zod";
|
|
5425
|
+
var WorkspaceProjectSchema = z4.object({
|
|
5426
|
+
id: z4.string().min(1),
|
|
5427
|
+
/** Manifest path, relative to the catalog file's own directory. */
|
|
5428
|
+
manifest: z4.string().min(1),
|
|
5429
|
+
/** Lockfile path, relative to the catalog file's own directory. */
|
|
5430
|
+
lock: z4.string().min(1),
|
|
5431
|
+
provider: z4.string().min(1).default("pixellab"),
|
|
5432
|
+
/** Free-form label for a shared account, e.g. distinguishing sandboxes. */
|
|
5433
|
+
account: z4.string().optional()
|
|
5434
|
+
}).strict();
|
|
5435
|
+
var WorkspaceSchema = z4.object({
|
|
5436
|
+
version: z4.literal(1),
|
|
5437
|
+
projects: z4.array(WorkspaceProjectSchema).default([])
|
|
5438
|
+
}).strict();
|
|
5439
|
+
function parseWorkspace(raw) {
|
|
5440
|
+
const parsed = WorkspaceSchema.safeParse(raw);
|
|
5441
|
+
if (parsed.success) return parsed.data;
|
|
5442
|
+
throw new Error(
|
|
5443
|
+
`Workspace catalog is not valid v1:
|
|
5444
|
+
${parsed.error.issues.slice(0, 5).map((i) => ` ${i.path.join(".")}: ${i.message}`).join("\n")}`
|
|
5445
|
+
);
|
|
5446
|
+
}
|
|
5447
|
+
async function loadWorkspace(workspacePath) {
|
|
5448
|
+
if (!existsSync13(workspacePath)) return { version: 1, projects: [] };
|
|
5449
|
+
let raw;
|
|
5450
|
+
try {
|
|
5451
|
+
raw = JSON.parse(await readFile13(workspacePath, "utf8"));
|
|
5452
|
+
} catch (err) {
|
|
5453
|
+
throw new Error(
|
|
5454
|
+
`Workspace catalog at ${workspacePath} is malformed:
|
|
5455
|
+
${err instanceof Error ? err.message : String(err)}`
|
|
5456
|
+
);
|
|
5457
|
+
}
|
|
5458
|
+
return parseWorkspace(raw);
|
|
5459
|
+
}
|
|
5460
|
+
async function saveWorkspace(workspacePath, ws) {
|
|
5461
|
+
const sorted = {
|
|
5462
|
+
version: 1,
|
|
5463
|
+
projects: [...ws.projects].sort((a, b) => a.id.localeCompare(b.id))
|
|
5464
|
+
};
|
|
5465
|
+
await mkdir6(path16.dirname(path16.resolve(workspacePath)), { recursive: true });
|
|
5466
|
+
const tmp = `${workspacePath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
5467
|
+
try {
|
|
5468
|
+
await writeFile8(tmp, JSON.stringify(sorted, null, 2) + "\n");
|
|
5469
|
+
await rename5(tmp, workspacePath);
|
|
5470
|
+
} finally {
|
|
5471
|
+
await rm6(tmp, { force: true });
|
|
5472
|
+
}
|
|
5473
|
+
}
|
|
5474
|
+
function toPortablePath(dir, absolute) {
|
|
5475
|
+
return path16.relative(dir, absolute).split(path16.sep).join("/");
|
|
5476
|
+
}
|
|
5477
|
+
function resolveProject(dir, project) {
|
|
5478
|
+
return {
|
|
5479
|
+
manifestPath: path16.resolve(dir, project.manifest.split("/").join(path16.sep)),
|
|
5480
|
+
lockPath: path16.resolve(dir, project.lock.split("/").join(path16.sep))
|
|
5481
|
+
};
|
|
5482
|
+
}
|
|
5483
|
+
function validateWorkspace(ws, dir) {
|
|
5484
|
+
const diagnostics = [];
|
|
5485
|
+
const idCounts = /* @__PURE__ */ new Map();
|
|
5486
|
+
const lockOwners = /* @__PURE__ */ new Map();
|
|
5487
|
+
const manifestOwners = /* @__PURE__ */ new Map();
|
|
5488
|
+
for (const project of ws.projects) {
|
|
5489
|
+
idCounts.set(project.id, (idCounts.get(project.id) ?? 0) + 1);
|
|
5490
|
+
const { manifestPath, lockPath } = resolveProject(dir, project);
|
|
5491
|
+
lockOwners.set(lockPath, [...lockOwners.get(lockPath) ?? [], project.id]);
|
|
5492
|
+
manifestOwners.set(manifestPath, [...manifestOwners.get(manifestPath) ?? [], project.id]);
|
|
5493
|
+
if (path16.isAbsolute(project.manifest) || path16.isAbsolute(project.lock)) {
|
|
5494
|
+
diagnostics.push({
|
|
5495
|
+
id: "absolute-path",
|
|
5496
|
+
level: "warning",
|
|
5497
|
+
message: `project "${project.id}" stores an absolute path \u2014 the catalog will not resolve correctly if this tree is cloned or moved elsewhere`
|
|
5498
|
+
});
|
|
5499
|
+
}
|
|
5500
|
+
if (!existsSync13(manifestPath)) {
|
|
5501
|
+
diagnostics.push({
|
|
5502
|
+
id: "missing-manifest",
|
|
5503
|
+
level: "error",
|
|
5504
|
+
message: `project "${project.id}" manifest not found: ${manifestPath}`
|
|
5505
|
+
});
|
|
5506
|
+
}
|
|
5507
|
+
if (!existsSync13(lockPath)) {
|
|
5508
|
+
diagnostics.push({
|
|
5509
|
+
id: "missing-lock",
|
|
5510
|
+
level: "error",
|
|
5511
|
+
message: `project "${project.id}" lockfile not found: ${lockPath}`
|
|
5512
|
+
});
|
|
5513
|
+
}
|
|
5514
|
+
}
|
|
5515
|
+
for (const [id, count] of idCounts) {
|
|
5516
|
+
if (count > 1) {
|
|
5517
|
+
diagnostics.push({
|
|
5518
|
+
id: "duplicate-id",
|
|
5519
|
+
level: "error",
|
|
5520
|
+
message: `project id "${id}" is registered ${count} times`
|
|
5521
|
+
});
|
|
5522
|
+
}
|
|
5523
|
+
}
|
|
5524
|
+
for (const [lockPath, ids] of lockOwners) {
|
|
5525
|
+
if (ids.length > 1) {
|
|
5526
|
+
diagnostics.push({
|
|
5527
|
+
id: "duplicate-lock",
|
|
5528
|
+
level: "error",
|
|
5529
|
+
message: `${ids.join(", ")} all register the same lockfile: ${lockPath}`
|
|
5530
|
+
});
|
|
5531
|
+
}
|
|
5532
|
+
}
|
|
5533
|
+
for (const [manifestPath, ids] of manifestOwners) {
|
|
5534
|
+
if (ids.length > 1) {
|
|
5535
|
+
diagnostics.push({
|
|
5536
|
+
id: "duplicate-manifest",
|
|
5537
|
+
level: "warning",
|
|
5538
|
+
message: `${ids.join(", ")} share manifest ${manifestPath} \u2014 expected only when they are variant lockfiles beside one manifest`
|
|
5539
|
+
});
|
|
5540
|
+
}
|
|
5541
|
+
}
|
|
5542
|
+
const providers = new Set(ws.projects.map((p) => p.provider));
|
|
5543
|
+
if (providers.size > 1) {
|
|
5544
|
+
diagnostics.push({
|
|
5545
|
+
id: "mixed-provider",
|
|
5546
|
+
level: "warning",
|
|
5547
|
+
message: `registered projects use different providers: ${[...providers].sort().join(", ")} \u2014 spend totals are kept separate per unit, but confirm this is intentional`
|
|
5548
|
+
});
|
|
5549
|
+
}
|
|
5550
|
+
return diagnostics;
|
|
5551
|
+
}
|
|
5552
|
+
|
|
5553
|
+
// src/pipeline/workspace.ts
|
|
5554
|
+
async function workspaceClaims(ws, dir) {
|
|
5555
|
+
const lockPaths = [];
|
|
5556
|
+
const byProject = {};
|
|
5557
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
5558
|
+
for (const project of ws.projects) {
|
|
5559
|
+
const { lockPath } = resolveProject(dir, project);
|
|
5560
|
+
lockPaths.push(lockPath);
|
|
5561
|
+
let projectClaims;
|
|
5562
|
+
try {
|
|
5563
|
+
projectClaims = await loadClaims([lockPath]);
|
|
5564
|
+
} catch (err) {
|
|
5565
|
+
throw new Error(
|
|
5566
|
+
`Project "${project.id}" lockfile is unreadable: ${err instanceof Error ? err.message : String(err)}`
|
|
5567
|
+
);
|
|
5568
|
+
}
|
|
5569
|
+
byProject[project.id] = projectClaims.size;
|
|
5570
|
+
for (const id of projectClaims) claimed.add(id);
|
|
5571
|
+
}
|
|
5572
|
+
return { claimed, byProject, lockPaths };
|
|
5573
|
+
}
|
|
5574
|
+
function emptyStateCounts() {
|
|
5575
|
+
return {
|
|
5576
|
+
ok: 0,
|
|
5577
|
+
missing: 0,
|
|
5578
|
+
untracked: 0,
|
|
5579
|
+
stale: 0,
|
|
5580
|
+
orphaned: 0,
|
|
5581
|
+
"in-flight": 0,
|
|
5582
|
+
recoverable: 0,
|
|
5583
|
+
failed: 0
|
|
5584
|
+
};
|
|
5585
|
+
}
|
|
5586
|
+
async function workspaceStatus(ws, dir) {
|
|
5587
|
+
const diagnostics = validateWorkspace(ws, dir);
|
|
5588
|
+
const projects = [];
|
|
5589
|
+
const totalsByState = emptyStateCounts();
|
|
5590
|
+
const totalsSpend = { generations: 0, usd: 0, free: 0 };
|
|
5591
|
+
for (const project of ws.projects) {
|
|
5592
|
+
const { manifestPath, lockPath } = resolveProject(dir, project);
|
|
5593
|
+
const base = {
|
|
5594
|
+
id: project.id,
|
|
5595
|
+
provider: project.provider,
|
|
5596
|
+
account: project.account ?? null,
|
|
5597
|
+
manifest: manifestPath,
|
|
5598
|
+
lock: lockPath
|
|
5599
|
+
};
|
|
5600
|
+
try {
|
|
5601
|
+
const provider = createProvider(project.provider, "offline");
|
|
5602
|
+
const loaded = await loadManifest(manifestPath);
|
|
5603
|
+
const specs = await resolveSpecs(loaded, { provider });
|
|
5604
|
+
const lock = await loadLock(lockPath);
|
|
5605
|
+
normalizeLockOutputPaths(lock, specs);
|
|
5606
|
+
const plan = await buildPlan(specs, lock);
|
|
5607
|
+
const byState = summarize(plan);
|
|
5608
|
+
const spend = spendByUnit(lock);
|
|
5609
|
+
for (const state of Object.keys(byState)) {
|
|
5610
|
+
totalsByState[state] += byState[state];
|
|
5611
|
+
}
|
|
5612
|
+
for (const unit of Object.keys(spend)) {
|
|
5613
|
+
totalsSpend[unit] = (totalsSpend[unit] ?? 0) + (spend[unit] ?? 0);
|
|
5614
|
+
}
|
|
5615
|
+
projects.push({
|
|
5616
|
+
...base,
|
|
5617
|
+
entries: Object.keys(lock.entries).length,
|
|
5618
|
+
byState,
|
|
5619
|
+
spendByUnit: spend,
|
|
5620
|
+
error: null
|
|
5621
|
+
});
|
|
5622
|
+
} catch (err) {
|
|
5623
|
+
projects.push({
|
|
5624
|
+
...base,
|
|
5625
|
+
entries: 0,
|
|
5626
|
+
byState: emptyStateCounts(),
|
|
5627
|
+
spendByUnit: { generations: 0, usd: 0, free: 0 },
|
|
5628
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5629
|
+
});
|
|
5630
|
+
}
|
|
5631
|
+
}
|
|
5632
|
+
let claims = 0;
|
|
5633
|
+
try {
|
|
5634
|
+
claims = (await workspaceClaims(ws, dir)).claimed.size;
|
|
5635
|
+
} catch {
|
|
5636
|
+
}
|
|
5637
|
+
return {
|
|
5638
|
+
version: 1,
|
|
5639
|
+
safe: !diagnostics.some((d) => d.level === "error") && projects.every((p) => !p.error),
|
|
5640
|
+
dir,
|
|
5641
|
+
projects,
|
|
5642
|
+
totals: { byState: totalsByState, spendByUnit: totalsSpend, claims },
|
|
5643
|
+
diagnostics
|
|
5644
|
+
};
|
|
5645
|
+
}
|
|
4885
5646
|
export {
|
|
4886
5647
|
AssetSchema,
|
|
4887
5648
|
DEFAULT_RATE_LIMIT,
|
|
@@ -4893,24 +5654,32 @@ export {
|
|
|
4893
5654
|
MAX_DOWNLOAD_BYTES,
|
|
4894
5655
|
MAX_RETRIES,
|
|
4895
5656
|
ManifestSchema,
|
|
5657
|
+
MediaType,
|
|
4896
5658
|
PixelLabClient,
|
|
4897
5659
|
PixelLabError,
|
|
4898
5660
|
PixelLabProvider,
|
|
5661
|
+
RetroDiffusionProvider,
|
|
4899
5662
|
StyleSchema,
|
|
4900
5663
|
UnsupportedCapabilityError,
|
|
5664
|
+
WorkspaceProjectSchema,
|
|
5665
|
+
WorkspaceSchema,
|
|
4901
5666
|
adopt,
|
|
4902
5667
|
applyTags,
|
|
4903
5668
|
auditStyle,
|
|
5669
|
+
availableProviders,
|
|
4904
5670
|
backoffMs,
|
|
4905
5671
|
buildManifest,
|
|
4906
5672
|
buildPlan,
|
|
5673
|
+
cacheFileName,
|
|
4907
5674
|
candidateCount,
|
|
4908
5675
|
clientFromEnv,
|
|
4909
5676
|
colorDistance,
|
|
4910
5677
|
countNumberedDescriptions,
|
|
4911
5678
|
createArtifactBundleManifest,
|
|
5679
|
+
createProvider,
|
|
4912
5680
|
currentEntryOutputPath,
|
|
4913
5681
|
currentOutputPath,
|
|
5682
|
+
detectMediaType,
|
|
4914
5683
|
doctor,
|
|
4915
5684
|
evaluateAudit,
|
|
4916
5685
|
expectedOutputPath,
|
|
@@ -4928,9 +5697,13 @@ export {
|
|
|
4928
5697
|
loadClaims,
|
|
4929
5698
|
loadLock,
|
|
4930
5699
|
loadManifest,
|
|
5700
|
+
loadSiblingManifests,
|
|
5701
|
+
loadWorkspace,
|
|
4931
5702
|
lockKey,
|
|
4932
5703
|
matchOrphanStyle,
|
|
4933
5704
|
measureBalanceChange,
|
|
5705
|
+
mediaExtension,
|
|
5706
|
+
mediaTypeFromExtension,
|
|
4934
5707
|
mergePalettes,
|
|
4935
5708
|
mountSprites,
|
|
4936
5709
|
mountStyle,
|
|
@@ -4942,19 +5715,25 @@ export {
|
|
|
4942
5715
|
packStyle,
|
|
4943
5716
|
paletteDistance,
|
|
4944
5717
|
parseLock,
|
|
5718
|
+
parseWorkspace,
|
|
4945
5719
|
pngSize,
|
|
4946
5720
|
poll,
|
|
4947
5721
|
portableOutputPath,
|
|
4948
5722
|
primaryOutput,
|
|
5723
|
+
providerFactory,
|
|
4949
5724
|
pushTags,
|
|
5725
|
+
registerProvider,
|
|
4950
5726
|
remove,
|
|
4951
5727
|
renderSalvageSheet,
|
|
4952
5728
|
renderSheet,
|
|
5729
|
+
requireBalance,
|
|
4953
5730
|
requireDelete,
|
|
4954
5731
|
requireList,
|
|
5732
|
+
requireSelectCandidate,
|
|
4955
5733
|
resolveEntryOutputs,
|
|
4956
5734
|
resolveOutputPath,
|
|
4957
5735
|
resolvePackInputs,
|
|
5736
|
+
resolveProject,
|
|
4958
5737
|
resolveSpecEntryOutputs,
|
|
4959
5738
|
resolveSpecOutputs,
|
|
4960
5739
|
resolveSpecs,
|
|
@@ -4964,6 +5743,7 @@ export {
|
|
|
4964
5743
|
runPicker,
|
|
4965
5744
|
runSalvage,
|
|
4966
5745
|
saveLock,
|
|
5746
|
+
saveWorkspace,
|
|
4967
5747
|
scanAssets,
|
|
4968
5748
|
selectEntryOutput,
|
|
4969
5749
|
sha256,
|
|
@@ -4979,11 +5759,16 @@ export {
|
|
|
4979
5759
|
tileFeatureOutputCount,
|
|
4980
5760
|
tileVariationCount,
|
|
4981
5761
|
tilesCost,
|
|
5762
|
+
toPortablePath,
|
|
4982
5763
|
totalSpend,
|
|
4983
5764
|
upsert,
|
|
4984
5765
|
validateCostEstimate,
|
|
5766
|
+
validateMedia,
|
|
5767
|
+
validateWorkspace,
|
|
4985
5768
|
verifyArtifactBundle,
|
|
4986
5769
|
withArtifactManifest,
|
|
5770
|
+
workspaceClaims,
|
|
5771
|
+
workspaceStatus,
|
|
4987
5772
|
writeArtifactBundle,
|
|
4988
5773
|
writeManagedArtifactBundle
|
|
4989
5774
|
};
|