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/cli.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import
|
|
5
|
-
import { existsSync as
|
|
6
|
-
import { readFile as
|
|
4
|
+
import path18 from "path";
|
|
5
|
+
import { existsSync as existsSync15 } from "fs";
|
|
6
|
+
import { readFile as readFile14 } from "fs/promises";
|
|
7
7
|
|
|
8
8
|
// src/env.ts
|
|
9
9
|
import { readFileSync, existsSync } from "fs";
|
|
@@ -38,6 +38,71 @@ function applyEnv(contents) {
|
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
// src/provider.ts
|
|
42
|
+
function validateCostEstimate(providerId, value) {
|
|
43
|
+
if (!value || typeof value !== "object") {
|
|
44
|
+
throw new Error(`Provider "${providerId}" returned an invalid cost estimate`);
|
|
45
|
+
}
|
|
46
|
+
const estimate = value;
|
|
47
|
+
if (typeof estimate.unit !== "string" || !estimate.unit.trim()) {
|
|
48
|
+
throw new Error(`Provider "${providerId}" returned an invalid cost unit`);
|
|
49
|
+
}
|
|
50
|
+
if (!Number.isFinite(estimate.amount) || estimate.amount < 0) {
|
|
51
|
+
throw new Error(`Provider "${providerId}" returned an invalid cost amount`);
|
|
52
|
+
}
|
|
53
|
+
if (estimate.unit === "free" && estimate.amount !== 0) {
|
|
54
|
+
throw new Error(`Provider "${providerId}" returned a nonzero amount with the free cost unit`);
|
|
55
|
+
}
|
|
56
|
+
if (!Number.isInteger(estimate.candidates) || estimate.candidates < 1) {
|
|
57
|
+
throw new Error(`Provider "${providerId}" returned an invalid candidate count`);
|
|
58
|
+
}
|
|
59
|
+
return estimate;
|
|
60
|
+
}
|
|
61
|
+
function measureBalanceChange(before, after) {
|
|
62
|
+
if (before.unit !== after.unit || !Number.isFinite(before.remaining) || !Number.isFinite(after.remaining)) return null;
|
|
63
|
+
const delta = before.remaining - after.remaining;
|
|
64
|
+
return {
|
|
65
|
+
unit: before.unit,
|
|
66
|
+
before: before.remaining,
|
|
67
|
+
after: after.remaining,
|
|
68
|
+
spent: Math.max(0, delta),
|
|
69
|
+
credited: Math.max(0, -delta)
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
var DEFAULT_RATE_LIMIT = { spacingMs: 2500, maxInFlight: 8 };
|
|
73
|
+
var UnsupportedCapabilityError = class extends Error {
|
|
74
|
+
constructor(providerId, capability) {
|
|
75
|
+
super(
|
|
76
|
+
`Provider "${providerId}" does not support ${capability}. That command is unavailable with this backend.`
|
|
77
|
+
);
|
|
78
|
+
this.name = "UnsupportedCapabilityError";
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
function requireList(provider) {
|
|
82
|
+
if (!provider.list) throw new UnsupportedCapabilityError(provider.id, "listing remote assets");
|
|
83
|
+
return provider.list.bind(provider);
|
|
84
|
+
}
|
|
85
|
+
function requireDelete(provider) {
|
|
86
|
+
if (!provider.delete) throw new UnsupportedCapabilityError(provider.id, "deleting remote assets");
|
|
87
|
+
return provider.delete.bind(provider);
|
|
88
|
+
}
|
|
89
|
+
function requireSelectCandidate(provider) {
|
|
90
|
+
if (!provider.selectCandidate) {
|
|
91
|
+
throw new UnsupportedCapabilityError(provider.id, "candidate selection");
|
|
92
|
+
}
|
|
93
|
+
return provider.selectCandidate.bind(provider);
|
|
94
|
+
}
|
|
95
|
+
function requireBalance(provider) {
|
|
96
|
+
if (!provider.balance) throw new UnsupportedCapabilityError(provider.id, "account balance");
|
|
97
|
+
return provider.balance.bind(provider);
|
|
98
|
+
}
|
|
99
|
+
function formatCost(unit, amount) {
|
|
100
|
+
if (unit === "free") return "free";
|
|
101
|
+
if (unit === "usd") return `$${amount.toFixed(2)}`;
|
|
102
|
+
if (unit === "generations") return `${amount} generation${amount === 1 ? "" : "s"}`;
|
|
103
|
+
return `${amount} ${unit}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
41
106
|
// src/providers/pixellab.ts
|
|
42
107
|
import { mkdirSync, existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
43
108
|
import { randomUUID } from "crypto";
|
|
@@ -154,11 +219,11 @@ var PixelLabClient = class {
|
|
|
154
219
|
* common than the failure mode of retrying (a duplicate object), and a
|
|
155
220
|
* duplicate is visible and free to delete whereas a silent gap is neither.
|
|
156
221
|
*/
|
|
157
|
-
async request(
|
|
222
|
+
async request(path19, init, attempt = 0) {
|
|
158
223
|
const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
|
|
159
224
|
let res;
|
|
160
225
|
try {
|
|
161
|
-
res = await fetch(`${BASE}${
|
|
226
|
+
res = await fetch(`${BASE}${path19}`, {
|
|
162
227
|
...init,
|
|
163
228
|
signal: init?.signal ?? AbortSignal.timeout(this.timeoutMs),
|
|
164
229
|
headers: {
|
|
@@ -170,24 +235,24 @@ var PixelLabClient = class {
|
|
|
170
235
|
} catch (err) {
|
|
171
236
|
if (attempt < MAX_RETRIES) {
|
|
172
237
|
await sleep(backoffMs(attempt));
|
|
173
|
-
return this.request(
|
|
238
|
+
return this.request(path19, init, attempt + 1);
|
|
174
239
|
}
|
|
175
240
|
throw err;
|
|
176
241
|
}
|
|
177
242
|
if (!res.ok && shouldRetry(res.status) && attempt < MAX_RETRIES) {
|
|
178
243
|
const waitMs = retryAfterMs(res.headers.get("retry-after")) ?? backoffMs(attempt);
|
|
179
244
|
await sleep(waitMs);
|
|
180
|
-
return this.request(
|
|
245
|
+
return this.request(path19, init, attempt + 1);
|
|
181
246
|
}
|
|
182
247
|
const text = await res.text();
|
|
183
248
|
if (!res.ok) {
|
|
184
|
-
throw new PixelLabError(`${init?.method ?? "GET"} ${
|
|
249
|
+
throw new PixelLabError(`${init?.method ?? "GET"} ${path19} \u2192 ${res.status}`, res.status, text);
|
|
185
250
|
}
|
|
186
251
|
if (!text) return {};
|
|
187
252
|
try {
|
|
188
253
|
return JSON.parse(text);
|
|
189
254
|
} catch {
|
|
190
|
-
throw new Error(`${init?.method ?? "GET"} ${
|
|
255
|
+
throw new Error(`${init?.method ?? "GET"} ${path19} returned invalid JSON`);
|
|
191
256
|
}
|
|
192
257
|
}
|
|
193
258
|
async balance() {
|
|
@@ -748,7 +813,8 @@ function parseHex(hex2) {
|
|
|
748
813
|
|
|
749
814
|
// src/types.ts
|
|
750
815
|
import { z as z2 } from "zod";
|
|
751
|
-
var
|
|
816
|
+
var MediaTypeSchema = z2.enum(["image/png", "image/gif"]);
|
|
817
|
+
var GeneratorSchema = z2.enum(["1dir", "map", "pixflux", "tiles", "animation"]);
|
|
752
818
|
function tileVariationCount(descriptions) {
|
|
753
819
|
return Math.max(1, descriptions) * 4;
|
|
754
820
|
}
|
|
@@ -780,7 +846,7 @@ function tilesCost(tileSize, variations) {
|
|
|
780
846
|
return 40;
|
|
781
847
|
}
|
|
782
848
|
var StyleImageSchema = z2.object({
|
|
783
|
-
/** Path to a PNG/JPEG, relative to the manifest
|
|
849
|
+
/** Path to a PNG/JPEG, relative to the manifest; the active provider validates limits. */
|
|
784
850
|
path: z2.string()
|
|
785
851
|
});
|
|
786
852
|
var StyleSchema = z2.object({
|
|
@@ -891,7 +957,9 @@ var StyleSchema = z2.object({
|
|
|
891
957
|
out: z2.string()
|
|
892
958
|
}).strict().optional(),
|
|
893
959
|
/** Tags applied to every object generated in this style, for server-side filtering. */
|
|
894
|
-
tags: z2.array(z2.string()).default([])
|
|
960
|
+
tags: z2.array(z2.string()).default([]),
|
|
961
|
+
/** Adapter-owned settings, keyed by provider id. */
|
|
962
|
+
providerOptions: z2.record(z2.record(z2.unknown())).default({})
|
|
895
963
|
}).strict().refine((s) => !(s.tileFeature && s.styleImages.length), {
|
|
896
964
|
message: "tileFeature and styleImages cannot be combined \u2014 a connectable set derives its own tile geometry, so remove one or the other",
|
|
897
965
|
path: ["tileFeature"]
|
|
@@ -906,7 +974,7 @@ var AssetSchema = z2.object({
|
|
|
906
974
|
height: z2.number().int().min(16).max(400).optional(),
|
|
907
975
|
/** Overrides the style default. `1dir` generator only. */
|
|
908
976
|
size: z2.number().int().min(32).max(256).optional(),
|
|
909
|
-
/** Explicit output path relative to outDir.
|
|
977
|
+
/** Explicit output path relative to outDir. Media-aware providers may replace its extension. */
|
|
910
978
|
file: z2.string().optional(),
|
|
911
979
|
/**
|
|
912
980
|
* Grid cell this asset owns in a mounted style, as [column, row].
|
|
@@ -958,6 +1026,8 @@ var AssetSchema = z2.object({
|
|
|
958
1026
|
var ManifestSchema = z2.object({
|
|
959
1027
|
$schema: z2.string().optional(),
|
|
960
1028
|
name: z2.string(),
|
|
1029
|
+
/** Generation backend. Existing manifests remain PixelLab by default. */
|
|
1030
|
+
provider: z2.string().min(1).default("pixellab"),
|
|
961
1031
|
styles: z2.record(StyleSchema),
|
|
962
1032
|
assets: z2.record(AssetSchema)
|
|
963
1033
|
}).strict();
|
|
@@ -995,7 +1065,8 @@ var LockEntrySchema = z2.object({
|
|
|
995
1065
|
sourceUrls: z2.array(
|
|
996
1066
|
z2.object({
|
|
997
1067
|
url: z2.string(),
|
|
998
|
-
role: z2.string().optional()
|
|
1068
|
+
role: z2.string().optional(),
|
|
1069
|
+
mediaType: MediaTypeSchema.optional()
|
|
999
1070
|
})
|
|
1000
1071
|
).default([]),
|
|
1001
1072
|
/**
|
|
@@ -1010,7 +1081,8 @@ var LockEntrySchema = z2.object({
|
|
|
1010
1081
|
z2.object({
|
|
1011
1082
|
path: z2.string(),
|
|
1012
1083
|
sha256: z2.string(),
|
|
1013
|
-
role: z2.string().optional()
|
|
1084
|
+
role: z2.string().optional(),
|
|
1085
|
+
mediaType: MediaTypeSchema.optional()
|
|
1014
1086
|
})
|
|
1015
1087
|
).default([]),
|
|
1016
1088
|
/**
|
|
@@ -1024,7 +1096,7 @@ var LockEntrySchema = z2.object({
|
|
|
1024
1096
|
/** Successful-submission estimate in `costUnit`; may be fractional USD. */
|
|
1025
1097
|
cost: z2.number().finite().nonnegative().default(0),
|
|
1026
1098
|
/** Unit for `cost`. Defaults preserve pre-unit PixelLab lockfiles. */
|
|
1027
|
-
costUnit: z2.
|
|
1099
|
+
costUnit: z2.string().min(1).default("generations"),
|
|
1028
1100
|
/** Which provider produced this. Absent on entries written before providers. */
|
|
1029
1101
|
provider: z2.string().default("pixellab")
|
|
1030
1102
|
});
|
|
@@ -1094,7 +1166,44 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
1094
1166
|
candidates: spec.generator === "1dir" ? candidateCount(spec.size) : 1
|
|
1095
1167
|
};
|
|
1096
1168
|
}
|
|
1169
|
+
validate(spec, styleImages) {
|
|
1170
|
+
if (spec.generator === "map") {
|
|
1171
|
+
requirePixelLabOption("view", spec.view, ["low top-down", "high top-down", "side"]);
|
|
1172
|
+
requirePixelLabOption("outline", spec.outline, [
|
|
1173
|
+
"single color outline",
|
|
1174
|
+
"selective outline",
|
|
1175
|
+
"lineless"
|
|
1176
|
+
]);
|
|
1177
|
+
requirePixelLabOption("shading", spec.shading, [
|
|
1178
|
+
"flat shading",
|
|
1179
|
+
"basic shading",
|
|
1180
|
+
"medium shading",
|
|
1181
|
+
"detailed shading"
|
|
1182
|
+
]);
|
|
1183
|
+
requirePixelLabOption("detail", spec.detail, [
|
|
1184
|
+
"low detail",
|
|
1185
|
+
"medium detail",
|
|
1186
|
+
"high detail"
|
|
1187
|
+
]);
|
|
1188
|
+
}
|
|
1189
|
+
if ((spec.generator === "map" || spec.generator === "pixflux") && styleImages.length) {
|
|
1190
|
+
throw new Error(`PixelLab ${spec.generator} does not support style images`);
|
|
1191
|
+
}
|
|
1192
|
+
for (const image of styleImages) {
|
|
1193
|
+
if (image.width > 256 || image.height > 256) {
|
|
1194
|
+
throw new Error(
|
|
1195
|
+
`Style image exceeds PixelLab's 256x256 limit (${image.width}x${image.height})`
|
|
1196
|
+
);
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
if (spec.tags.length > 20) {
|
|
1200
|
+
throw new Error(
|
|
1201
|
+
`${spec.styleId}/${spec.assetId} resolves to ${spec.tags.length} tags, but PixelLab allows at most 20`
|
|
1202
|
+
);
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1097
1205
|
async submit(spec, styleImages) {
|
|
1206
|
+
this.validate(spec, styleImages);
|
|
1098
1207
|
if (spec.generator === "pixflux") {
|
|
1099
1208
|
const swatch = spec.palette.length ? paletteSwatch(spec.palette).toString("base64") : void 0;
|
|
1100
1209
|
const { png } = await this.client.createImagePixflux({
|
|
@@ -1280,6 +1389,11 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
1280
1389
|
await this.client.deleteObject(assetId);
|
|
1281
1390
|
}
|
|
1282
1391
|
};
|
|
1392
|
+
function requirePixelLabOption(name, value, allowed) {
|
|
1393
|
+
if (value != null && !allowed.includes(value)) {
|
|
1394
|
+
throw new Error(`PixelLab map ${name} must be one of: ${allowed.join(", ")}`);
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1283
1397
|
function tilesInIndexOrder(urls) {
|
|
1284
1398
|
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);
|
|
1285
1399
|
}
|
|
@@ -1288,60 +1402,442 @@ function firstUrl(urls) {
|
|
|
1288
1402
|
return Object.values(urls).find((u) => typeof u === "string") ?? null;
|
|
1289
1403
|
}
|
|
1290
1404
|
|
|
1291
|
-
// src/
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
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}`);
|
|
1295
1431
|
}
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
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)}`);
|
|
1299
1472
|
}
|
|
1300
|
-
|
|
1301
|
-
|
|
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");
|
|
1302
1484
|
}
|
|
1303
|
-
|
|
1304
|
-
|
|
1485
|
+
}
|
|
1486
|
+
function cacheFileName(hash, mediaType = MediaType.PNG) {
|
|
1487
|
+
return `${hash}${mediaExtension(mediaType)}`;
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
// src/providers/retrodiffusion.ts
|
|
1491
|
+
var DEFAULT_BASE_URL = "https://api.retrodiffusion.ai/v1";
|
|
1492
|
+
var RetroDiffusionClient = class {
|
|
1493
|
+
constructor(token, baseUrl = DEFAULT_BASE_URL, request = fetch) {
|
|
1494
|
+
this.token = token;
|
|
1495
|
+
this.baseUrl = baseUrl;
|
|
1496
|
+
this.request = request;
|
|
1497
|
+
}
|
|
1498
|
+
token;
|
|
1499
|
+
baseUrl;
|
|
1500
|
+
request;
|
|
1501
|
+
async submit(body) {
|
|
1502
|
+
const response = await this.call("/inferences", { method: "POST", body: JSON.stringify(body) });
|
|
1503
|
+
const taskId = response.task_id;
|
|
1504
|
+
if (typeof taskId !== "string" || !taskId) {
|
|
1505
|
+
throw new Error("Retro Diffusion did not return an async task id");
|
|
1506
|
+
}
|
|
1507
|
+
return taskId;
|
|
1508
|
+
}
|
|
1509
|
+
async quote(body) {
|
|
1510
|
+
const response = await this.call("/inferences", {
|
|
1511
|
+
method: "POST",
|
|
1512
|
+
body: JSON.stringify({ ...body, check_cost: true })
|
|
1513
|
+
});
|
|
1514
|
+
const amount = response.balance_cost;
|
|
1515
|
+
if (typeof amount !== "number" || !Number.isFinite(amount) || amount < 0) {
|
|
1516
|
+
throw new Error("Retro Diffusion returned an invalid cost quote");
|
|
1517
|
+
}
|
|
1518
|
+
return amount;
|
|
1305
1519
|
}
|
|
1306
|
-
|
|
1307
|
-
|
|
1520
|
+
async task(id) {
|
|
1521
|
+
return await this.call(`/inferences/tasks/${encodeURIComponent(id)}`);
|
|
1308
1522
|
}
|
|
1309
|
-
|
|
1523
|
+
async balance() {
|
|
1524
|
+
const response = await this.call("/inferences/credits");
|
|
1525
|
+
const balance = response.balance;
|
|
1526
|
+
if (typeof balance !== "number" || !Number.isFinite(balance)) {
|
|
1527
|
+
throw new Error("Retro Diffusion returned an invalid balance");
|
|
1528
|
+
}
|
|
1529
|
+
return balance;
|
|
1530
|
+
}
|
|
1531
|
+
async call(path19, init = {}) {
|
|
1532
|
+
if (!this.token) throw new Error("RD_API_KEY is not set");
|
|
1533
|
+
const response = await this.request(`${this.baseUrl}${path19}`, {
|
|
1534
|
+
...init,
|
|
1535
|
+
headers: {
|
|
1536
|
+
"Content-Type": "application/json",
|
|
1537
|
+
"X-RD-Token": this.token,
|
|
1538
|
+
...init.headers
|
|
1539
|
+
}
|
|
1540
|
+
});
|
|
1541
|
+
const text = await response.text();
|
|
1542
|
+
let value = null;
|
|
1543
|
+
try {
|
|
1544
|
+
value = text ? JSON.parse(text) : null;
|
|
1545
|
+
} catch {
|
|
1546
|
+
value = text;
|
|
1547
|
+
}
|
|
1548
|
+
if (!response.ok) {
|
|
1549
|
+
const retry = response.headers.get("retry-after");
|
|
1550
|
+
const detail = retroError(value);
|
|
1551
|
+
throw new Error(
|
|
1552
|
+
`Retro Diffusion request failed (${response.status}): ${detail}` + (retry ? `; retry after ${retry}s` : "")
|
|
1553
|
+
);
|
|
1554
|
+
}
|
|
1555
|
+
return value;
|
|
1556
|
+
}
|
|
1557
|
+
};
|
|
1558
|
+
var RetroDiffusionProvider = class _RetroDiffusionProvider {
|
|
1559
|
+
constructor(client) {
|
|
1560
|
+
this.client = client;
|
|
1561
|
+
}
|
|
1562
|
+
client;
|
|
1563
|
+
id = "retrodiffusion";
|
|
1564
|
+
static fromEnv() {
|
|
1565
|
+
return new _RetroDiffusionProvider(new RetroDiffusionClient(process.env.RD_API_KEY));
|
|
1566
|
+
}
|
|
1567
|
+
static forOffline() {
|
|
1568
|
+
return new _RetroDiffusionProvider(new RetroDiffusionClient(void 0));
|
|
1569
|
+
}
|
|
1570
|
+
static forDownloads() {
|
|
1571
|
+
return _RetroDiffusionProvider.forOffline();
|
|
1572
|
+
}
|
|
1573
|
+
supports(generator) {
|
|
1574
|
+
return generator === "map" || generator === "pixflux" || generator === "tiles" || generator === "animation";
|
|
1575
|
+
}
|
|
1576
|
+
estimate(spec) {
|
|
1577
|
+
const options = retroOptions(spec);
|
|
1578
|
+
const style = resolvedPromptStyle(spec, options);
|
|
1579
|
+
const count = options.numImages ?? 1;
|
|
1580
|
+
const pixels = spec.width * spec.height;
|
|
1581
|
+
let each;
|
|
1582
|
+
if (style.startsWith("rd_advanced_animation__")) {
|
|
1583
|
+
each = /__(?:custom_action|subtle_motion)$/.test(style) ? 0.25 : 0.14;
|
|
1584
|
+
} else if (style.startsWith("rd_animation__")) {
|
|
1585
|
+
each = /__(?:any_animation|8_dir_rotation)$/.test(style) ? 0.25 : 0.07;
|
|
1586
|
+
} else if (/^rd_tile__tileset(?:_advanced)?$/.test(style)) {
|
|
1587
|
+
each = 0.1;
|
|
1588
|
+
} else if (style.startsWith("rd_pro__")) {
|
|
1589
|
+
each = 0.18;
|
|
1590
|
+
} else if (style.startsWith("rd_fast__")) {
|
|
1591
|
+
each = Math.max(0.015, (pixels + 1e5) / 6e6);
|
|
1592
|
+
} else if (isLowResolutionStyle(style)) {
|
|
1593
|
+
each = Math.max(0.02, (pixels + 13700) / 6e5);
|
|
1594
|
+
} else {
|
|
1595
|
+
each = Math.max(0.025, (pixels + 5e4) / 2e6);
|
|
1596
|
+
}
|
|
1597
|
+
return { unit: "usd", amount: roundUsdEstimate(each * count), candidates: count };
|
|
1598
|
+
}
|
|
1599
|
+
validate(spec, styleImages) {
|
|
1600
|
+
const options = retroOptions(spec);
|
|
1601
|
+
const promptStyle = resolvedPromptStyle(spec, options);
|
|
1602
|
+
const count = options.numImages ?? 1;
|
|
1603
|
+
const isAnimation = /^(?:rd_animation__|rd_advanced_animation__)/.test(promptStyle);
|
|
1604
|
+
const isTile = promptStyle.startsWith("rd_tile__");
|
|
1605
|
+
if (!promptStyle || spec.generator === "animation" && !isAnimation || spec.generator === "tiles" && !isTile || spec.generator !== "animation" && spec.generator !== "tiles" && (isAnimation || isTile)) {
|
|
1606
|
+
throw new Error(
|
|
1607
|
+
`Retro Diffusion style "${promptStyle}" does not match generator "${spec.generator}"`
|
|
1608
|
+
);
|
|
1609
|
+
}
|
|
1610
|
+
if (!Number.isInteger(count) || count < 1 || count > 16) {
|
|
1611
|
+
throw new Error("Retro Diffusion numImages must be a whole number from 1 to 16");
|
|
1612
|
+
}
|
|
1613
|
+
if (spec.width < 16 || spec.height < 16 || spec.width > 512 || spec.height > 512) {
|
|
1614
|
+
throw new Error("Retro Diffusion output dimensions must be between 16 and 512 pixels");
|
|
1615
|
+
}
|
|
1616
|
+
if (styleImages.length > 9) {
|
|
1617
|
+
throw new Error("Retro Diffusion accepts at most 9 reference images");
|
|
1618
|
+
}
|
|
1619
|
+
if (spec.generator === "animation") {
|
|
1620
|
+
if (count !== 1) throw new Error("Retro Diffusion animations currently require numImages: 1");
|
|
1621
|
+
validateAnimation(promptStyle, spec, styleImages, options);
|
|
1622
|
+
} else if (spec.generator === "tiles") {
|
|
1623
|
+
validateTile(promptStyle, spec, styleImages, count, options);
|
|
1624
|
+
} else if (styleImages.length && !/^(?:rd_pro__|user__)/.test(promptStyle)) {
|
|
1625
|
+
throw new Error(
|
|
1626
|
+
`Retro Diffusion style "${promptStyle}" does not accept reference_images; use an RD Pro or user style`
|
|
1627
|
+
);
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
async submit(spec, styleImages) {
|
|
1631
|
+
this.validate(spec, styleImages);
|
|
1632
|
+
const options = retroOptions(spec);
|
|
1633
|
+
const promptStyle = resolvedPromptStyle(spec, options);
|
|
1634
|
+
const animation = spec.generator === "animation";
|
|
1635
|
+
const tiles = spec.generator === "tiles";
|
|
1636
|
+
const body = {
|
|
1637
|
+
prompt: spec.prompt,
|
|
1638
|
+
prompt_style: promptStyle,
|
|
1639
|
+
width: spec.width,
|
|
1640
|
+
height: spec.height,
|
|
1641
|
+
num_images: options.numImages ?? 1,
|
|
1642
|
+
...!animation && !tiles ? { remove_bg: options.removeBg ?? spec.noBackground } : {},
|
|
1643
|
+
...spec.seed != null ? { seed: spec.seed } : {},
|
|
1644
|
+
...animation || tiles ? styleImages[0] ? { input_image: styleImages[0].base64 } : {} : styleImages.length ? { reference_images: styleImages.map((image) => image.base64) } : {},
|
|
1645
|
+
...tiles && styleImages[1] ? { extra_input_image: styleImages[1].base64 } : {},
|
|
1646
|
+
...tiles && options.extraPrompt ? { extra_prompt: options.extraPrompt } : {},
|
|
1647
|
+
...animation && options.framesDuration ? { frames_duration: options.framesDuration } : {},
|
|
1648
|
+
...animation && options.returnSpritesheet ? { return_spritesheet: true } : {},
|
|
1649
|
+
...!animation && options.tileX != null ? { tile_x: options.tileX } : {},
|
|
1650
|
+
...!animation && options.tileY != null ? { tile_y: options.tileY } : {},
|
|
1651
|
+
...spec.palette.length ? { input_palette: paletteSwatch(spec.palette).toString("base64") } : {}
|
|
1652
|
+
};
|
|
1653
|
+
const quoted = await this.client.quote(body);
|
|
1654
|
+
const estimated = this.estimate(spec).amount;
|
|
1655
|
+
if (quoted > estimated + 1e-6) {
|
|
1656
|
+
throw new Error(
|
|
1657
|
+
`Retro Diffusion quoted $${quoted.toFixed(6)}, above the offline estimate $${estimated.toFixed(6)}; no paid request was sent`
|
|
1658
|
+
);
|
|
1659
|
+
}
|
|
1660
|
+
return {
|
|
1661
|
+
jobId: await this.client.submit({ ...body, async: true, upload_outputs: true })
|
|
1662
|
+
};
|
|
1663
|
+
}
|
|
1664
|
+
async poll(jobId, generator, context) {
|
|
1665
|
+
const task = await this.client.task(jobId);
|
|
1666
|
+
if (task.status === "pending" || task.status === "running") return { status: "processing" };
|
|
1667
|
+
if (task.status === "failed") return { status: "failed", error: retroError(task.error) };
|
|
1668
|
+
if (task.status !== "succeeded") {
|
|
1669
|
+
return { status: "failed", error: `Retro Diffusion returned unknown task status "${String(task.status)}"` };
|
|
1670
|
+
}
|
|
1671
|
+
const options = context?.spec ? retroOptions(context.spec) : {};
|
|
1672
|
+
const mediaType = generator === "animation" && !options.returnSpritesheet ? MediaType.GIF : MediaType.PNG;
|
|
1673
|
+
const sources = resultSources(task.result, mediaType);
|
|
1674
|
+
const urls = sources.map((source) => source.url);
|
|
1675
|
+
if (!urls.length) return { status: "failed", error: "Retro Diffusion task returned no images" };
|
|
1676
|
+
if (urls.length > 1) return { status: "review", candidateUrls: urls };
|
|
1677
|
+
return {
|
|
1678
|
+
status: "ready",
|
|
1679
|
+
objectId: `${jobId}#0`,
|
|
1680
|
+
sourceUrl: urls[0],
|
|
1681
|
+
sources,
|
|
1682
|
+
metadata: {
|
|
1683
|
+
balanceCost: task.result?.balance_cost ?? null,
|
|
1684
|
+
remainingBalance: task.result?.remaining_balance ?? null,
|
|
1685
|
+
mediaType,
|
|
1686
|
+
kind: generator === "animation" ? "animation" : generator === "tiles" ? "tileset" : "image",
|
|
1687
|
+
...context?.spec ? {
|
|
1688
|
+
promptStyle: resolvedPromptStyle(context.spec, options),
|
|
1689
|
+
width: context.spec.width,
|
|
1690
|
+
height: context.spec.height
|
|
1691
|
+
} : {}
|
|
1692
|
+
}
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1695
|
+
async selectCandidate(jobId, index) {
|
|
1696
|
+
const task = await this.client.task(jobId);
|
|
1697
|
+
if (task.status !== "succeeded") throw new Error(`Retro Diffusion task ${jobId} is not ready`);
|
|
1698
|
+
const url = resultSources(task.result, MediaType.PNG)[index]?.url;
|
|
1699
|
+
if (!url) throw new Error(`Retro Diffusion task ${jobId} has no candidate at index ${index}`);
|
|
1700
|
+
return { objectId: `${jobId}#${index}`, sourceUrl: url };
|
|
1701
|
+
}
|
|
1702
|
+
async download(url) {
|
|
1703
|
+
const data = /^data:[^;]+;base64,(.+)$/.exec(url)?.[1];
|
|
1704
|
+
if (data) return Buffer.from(data, "base64");
|
|
1705
|
+
const response = await fetch(url);
|
|
1706
|
+
if (!response.ok) throw new Error(`Retro Diffusion download failed (${response.status})`);
|
|
1707
|
+
return Buffer.from(await response.arrayBuffer());
|
|
1708
|
+
}
|
|
1709
|
+
async balance() {
|
|
1710
|
+
return { unit: "usd", remaining: await this.client.balance() };
|
|
1711
|
+
}
|
|
1712
|
+
};
|
|
1713
|
+
function retroOptions(spec) {
|
|
1714
|
+
return spec.providerOptions;
|
|
1715
|
+
}
|
|
1716
|
+
function resultSources(result, mediaType) {
|
|
1717
|
+
const hosted = (result?.output_urls ?? []).filter((url) => typeof url === "string" && url.length > 0).map((url) => ({ url, mediaType }));
|
|
1718
|
+
if (hosted.length) return hosted;
|
|
1719
|
+
return (result?.base64_images ?? []).filter((data) => typeof data === "string" && data.length > 0).map((data) => ({
|
|
1720
|
+
url: `data:${mediaType};base64,${data}`,
|
|
1721
|
+
mediaType
|
|
1722
|
+
}));
|
|
1310
1723
|
}
|
|
1311
|
-
function
|
|
1312
|
-
|
|
1313
|
-
const delta = before.remaining - after.remaining;
|
|
1314
|
-
return {
|
|
1315
|
-
unit: before.unit,
|
|
1316
|
-
before: before.remaining,
|
|
1317
|
-
after: after.remaining,
|
|
1318
|
-
spent: Math.max(0, delta),
|
|
1319
|
-
credited: Math.max(0, -delta)
|
|
1320
|
-
};
|
|
1724
|
+
function resolvedPromptStyle(spec, options) {
|
|
1725
|
+
return options.promptStyle ?? (spec.generator === "animation" ? "rd_animation__any_animation" : spec.generator === "tiles" ? "rd_tile__tileset" : "rd_plus__default");
|
|
1321
1726
|
}
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
`
|
|
1327
|
-
|
|
1328
|
-
|
|
1727
|
+
function validateAnimation(style, spec, styleImages, options) {
|
|
1728
|
+
if (spec.width !== spec.height) throw new Error("Retro Diffusion animations must be square");
|
|
1729
|
+
if (style.startsWith("rd_advanced_animation__")) {
|
|
1730
|
+
if (styleImages.length !== 1) {
|
|
1731
|
+
throw new Error(`Retro Diffusion advanced animation style "${style}" requires one input image`);
|
|
1732
|
+
}
|
|
1733
|
+
if (spec.width < 32 || spec.width > 256) {
|
|
1734
|
+
throw new Error("Retro Diffusion advanced animations require dimensions from 32 to 256 pixels");
|
|
1735
|
+
}
|
|
1736
|
+
} else if (styleImages.length > 1) {
|
|
1737
|
+
throw new Error("Retro Diffusion prompt animations accept at most one input image");
|
|
1738
|
+
}
|
|
1739
|
+
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;
|
|
1740
|
+
if (exact && spec.width !== exact) {
|
|
1741
|
+
throw new Error(`Retro Diffusion style "${style}" requires ${exact}x${exact} dimensions`);
|
|
1742
|
+
}
|
|
1743
|
+
if (style.endsWith("__vfx") && (spec.width < 24 || spec.width > 96)) {
|
|
1744
|
+
throw new Error("Retro Diffusion VFX animations require dimensions from 24 to 96 pixels");
|
|
1745
|
+
}
|
|
1746
|
+
if (options.framesDuration != null && ![4, 6, 8, 10, 12, 16].includes(options.framesDuration)) {
|
|
1747
|
+
throw new Error("Retro Diffusion framesDuration must be 4, 6, 8, 10, 12, or 16");
|
|
1329
1748
|
}
|
|
1330
|
-
};
|
|
1331
|
-
function requireList(provider) {
|
|
1332
|
-
if (!provider.list) throw new UnsupportedCapabilityError(provider.id, "listing remote assets");
|
|
1333
|
-
return provider.list.bind(provider);
|
|
1334
1749
|
}
|
|
1335
|
-
function
|
|
1336
|
-
if (
|
|
1337
|
-
|
|
1750
|
+
function validateTile(style, spec, styleImages, count, options) {
|
|
1751
|
+
if (spec.width !== spec.height) throw new Error("Retro Diffusion tiles must be square");
|
|
1752
|
+
const size = spec.width;
|
|
1753
|
+
if (/^rd_tile__tileset(?:_advanced)?$/.test(style)) {
|
|
1754
|
+
if (size < 16 || size > 32) throw new Error("Retro Diffusion tilesets require 16\u201332px tiles");
|
|
1755
|
+
if (count !== 1) throw new Error("Retro Diffusion tilesets require numImages: 1");
|
|
1756
|
+
} else if (style === "rd_tile__single_tile" && (size < 16 || size > 64)) {
|
|
1757
|
+
throw new Error("Retro Diffusion single tiles require dimensions from 16 to 64 pixels");
|
|
1758
|
+
} else if (style === "rd_tile__tile_variation") {
|
|
1759
|
+
if (size < 16 || size > 128) throw new Error("Retro Diffusion tile variations require 16\u2013128px tiles");
|
|
1760
|
+
if (styleImages.length !== 1) throw new Error("Retro Diffusion tile variations require one input image");
|
|
1761
|
+
} else if (style === "rd_tile__tile_object" && (size < 16 || size > 96)) {
|
|
1762
|
+
throw new Error("Retro Diffusion tile objects require dimensions from 16 to 96 pixels");
|
|
1763
|
+
} else if (style === "rd_tile__scene_object" && (size < 64 || size > 384)) {
|
|
1764
|
+
throw new Error("Retro Diffusion tile scene objects require dimensions from 64 to 384 pixels");
|
|
1765
|
+
}
|
|
1766
|
+
if (style === "rd_tile__tileset" && styleImages.length > 1) {
|
|
1767
|
+
throw new Error("Retro Diffusion basic tilesets accept at most one input image");
|
|
1768
|
+
}
|
|
1769
|
+
if (style === "rd_tile__tileset_advanced") {
|
|
1770
|
+
if (styleImages.length > 2) throw new Error("Retro Diffusion advanced tilesets accept at most two input images");
|
|
1771
|
+
if (!options.extraPrompt && styleImages.length < 2) {
|
|
1772
|
+
throw new Error("Retro Diffusion advanced tilesets require extraPrompt or a second input image");
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1338
1775
|
}
|
|
1339
|
-
function
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1776
|
+
function isLowResolutionStyle(style) {
|
|
1777
|
+
return /(?:^|__)(?:mc_|low_res|classic|skill_icon|topdown_item)/.test(style);
|
|
1778
|
+
}
|
|
1779
|
+
function roundUsdEstimate(value) {
|
|
1780
|
+
return Math.ceil((value - 1e-9) * 1e3) / 1e3;
|
|
1781
|
+
}
|
|
1782
|
+
function retroError(value) {
|
|
1783
|
+
if (typeof value === "string") return value || "unknown error";
|
|
1784
|
+
if (!value || typeof value !== "object") return "unknown error";
|
|
1785
|
+
const record = value;
|
|
1786
|
+
if (typeof record.message === "string") return record.message;
|
|
1787
|
+
if (typeof record.detail === "string") return record.detail;
|
|
1788
|
+
if (Array.isArray(record.detail)) {
|
|
1789
|
+
const details = record.detail.map((item) => {
|
|
1790
|
+
if (!item || typeof item !== "object") return String(item);
|
|
1791
|
+
const detail = item;
|
|
1792
|
+
return typeof detail.msg === "string" ? detail.msg : JSON.stringify(item);
|
|
1793
|
+
}).filter(Boolean);
|
|
1794
|
+
if (details.length) return details.join("; ");
|
|
1795
|
+
}
|
|
1796
|
+
if (record.detail && typeof record.detail === "object") {
|
|
1797
|
+
const message2 = record.detail.message;
|
|
1798
|
+
if (typeof message2 === "string") return message2;
|
|
1799
|
+
}
|
|
1800
|
+
return JSON.stringify(value);
|
|
1343
1801
|
}
|
|
1344
1802
|
|
|
1803
|
+
// src/providers/registry.ts
|
|
1804
|
+
var factories = /* @__PURE__ */ new Map();
|
|
1805
|
+
function registerProvider(factory) {
|
|
1806
|
+
const id = factory.id.trim();
|
|
1807
|
+
if (!id) throw new Error("Provider id cannot be empty");
|
|
1808
|
+
if (factories.has(id)) throw new Error(`Provider "${id}" is already registered`);
|
|
1809
|
+
factories.set(id, factory);
|
|
1810
|
+
}
|
|
1811
|
+
function providerFactory(id) {
|
|
1812
|
+
const factory = factories.get(id);
|
|
1813
|
+
if (!factory) {
|
|
1814
|
+
const available = [...factories.keys()].sort().join(", ") || "(none)";
|
|
1815
|
+
throw new Error(`Unknown provider "${id}". Available providers: ${available}`);
|
|
1816
|
+
}
|
|
1817
|
+
return factory;
|
|
1818
|
+
}
|
|
1819
|
+
function createProvider(id, mode2) {
|
|
1820
|
+
return providerFactory(id).create(mode2);
|
|
1821
|
+
}
|
|
1822
|
+
registerProvider({
|
|
1823
|
+
id: "pixellab",
|
|
1824
|
+
credentialEnv: "PIXELLAB_API_KEY",
|
|
1825
|
+
create(mode2) {
|
|
1826
|
+
if (mode2 === "online") return PixelLabProvider.fromEnv();
|
|
1827
|
+
if (mode2 === "downloads") return PixelLabProvider.forDownloads();
|
|
1828
|
+
return PixelLabProvider.forOffline();
|
|
1829
|
+
}
|
|
1830
|
+
});
|
|
1831
|
+
registerProvider({
|
|
1832
|
+
id: "retrodiffusion",
|
|
1833
|
+
credentialEnv: "RD_API_KEY",
|
|
1834
|
+
create(mode2) {
|
|
1835
|
+
if (mode2 === "online") return RetroDiffusionProvider.fromEnv();
|
|
1836
|
+
if (mode2 === "downloads") return RetroDiffusionProvider.forDownloads();
|
|
1837
|
+
return RetroDiffusionProvider.forOffline();
|
|
1838
|
+
}
|
|
1839
|
+
});
|
|
1840
|
+
|
|
1345
1841
|
// src/manifest.ts
|
|
1346
1842
|
import { readFile as readFile2 } from "fs/promises";
|
|
1347
1843
|
import { existsSync as existsSync3 } from "fs";
|
|
@@ -1353,12 +1849,16 @@ import { readFile } from "fs/promises";
|
|
|
1353
1849
|
function sha256(data) {
|
|
1354
1850
|
return createHash("sha256").update(data).digest("hex");
|
|
1355
1851
|
}
|
|
1356
|
-
async function sha256File(
|
|
1357
|
-
return sha256(await readFile(
|
|
1852
|
+
async function sha256File(path19) {
|
|
1853
|
+
return sha256(await readFile(path19));
|
|
1358
1854
|
}
|
|
1359
1855
|
function specHash(spec, styleImageHashes) {
|
|
1360
1856
|
return sha256(
|
|
1361
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,
|
|
1362
1862
|
generator: spec.generator,
|
|
1363
1863
|
prompt: spec.prompt,
|
|
1364
1864
|
width: spec.width,
|
|
@@ -1372,7 +1872,7 @@ function specHash(spec, styleImageHashes) {
|
|
|
1372
1872
|
// `noBackground` only reaches the wire for pixflux; the tile fields are
|
|
1373
1873
|
// undefined for every other generator. `tileSize` is intentionally
|
|
1374
1874
|
// absent — width/height are derived from it, so it is already covered.
|
|
1375
|
-
noBackground: spec.generator === "pixflux" ? spec.noBackground : void 0,
|
|
1875
|
+
noBackground: spec.generator === "pixflux" || spec.provider !== "pixellab" ? spec.noBackground : void 0,
|
|
1376
1876
|
tileType: spec.tileType,
|
|
1377
1877
|
tileView: spec.tileView,
|
|
1378
1878
|
tileFeature: spec.tileFeature,
|
|
@@ -1412,6 +1912,7 @@ ${unknownReferences.map((i) => ` ${i}`).join("\n")}`);
|
|
|
1412
1912
|
}
|
|
1413
1913
|
async function resolveSpecs(loaded, filter) {
|
|
1414
1914
|
const { manifest, root } = loaded;
|
|
1915
|
+
const activeProvider = filter?.provider ?? createProvider(manifest.provider, "offline");
|
|
1415
1916
|
const specs = [];
|
|
1416
1917
|
const styleIds = Object.keys(manifest.styles).filter(
|
|
1417
1918
|
(id) => !filter?.styles?.length || filter.styles.includes(id)
|
|
@@ -1437,10 +1938,8 @@ async function resolveSpecs(loaded, filter) {
|
|
|
1437
1938
|
const buf = await readFile2(abs);
|
|
1438
1939
|
const metadata = imageMetadata(buf);
|
|
1439
1940
|
if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${abs}`);
|
|
1440
|
-
if (metadata.width < 1 || metadata.height < 1
|
|
1441
|
-
throw new Error(
|
|
1442
|
-
`Style image exceeds the API's 256x256 limit: ${abs} (${metadata.width}x${metadata.height})`
|
|
1443
|
-
);
|
|
1941
|
+
if (metadata.width < 1 || metadata.height < 1) {
|
|
1942
|
+
throw new Error(`Style image has invalid dimensions: ${abs}`);
|
|
1444
1943
|
}
|
|
1445
1944
|
hit = { base64: buf.toString("base64"), hash: sha256(buf), ...metadata };
|
|
1446
1945
|
styleImageCache.set(abs, hit);
|
|
@@ -1460,8 +1959,8 @@ async function resolveSpecs(loaded, filter) {
|
|
|
1460
1959
|
if (filter?.assets?.length && !filter.assets.includes(assetId)) continue;
|
|
1461
1960
|
if (asset.styles.length && !asset.styles.includes(styleId)) continue;
|
|
1462
1961
|
const generator = style.generator;
|
|
1463
|
-
if (
|
|
1464
|
-
throw new Error(`Provider "${
|
|
1962
|
+
if (!activeProvider.supports(generator)) {
|
|
1963
|
+
throw new Error(`Provider "${activeProvider.id}" does not support generator "${generator}"`);
|
|
1465
1964
|
}
|
|
1466
1965
|
let width;
|
|
1467
1966
|
let height;
|
|
@@ -1488,6 +1987,8 @@ async function resolveSpecs(loaded, filter) {
|
|
|
1488
1987
|
const base = {
|
|
1489
1988
|
styleId,
|
|
1490
1989
|
assetId,
|
|
1990
|
+
provider: activeProvider.id,
|
|
1991
|
+
providerOptions: style.providerOptions[activeProvider.id] ?? {},
|
|
1491
1992
|
generator,
|
|
1492
1993
|
prompt,
|
|
1493
1994
|
width,
|
|
@@ -1519,11 +2020,6 @@ async function resolveSpecs(loaded, filter) {
|
|
|
1519
2020
|
`style:${styleId}`
|
|
1520
2021
|
])
|
|
1521
2022
|
];
|
|
1522
|
-
if (tags.length > 20) {
|
|
1523
|
-
throw new Error(
|
|
1524
|
-
`${styleId}/${assetId} resolves to ${tags.length} tags, but PixelLab allows at most 20`
|
|
1525
|
-
);
|
|
1526
|
-
}
|
|
1527
2023
|
const resolved = {
|
|
1528
2024
|
...base,
|
|
1529
2025
|
root,
|
|
@@ -1532,12 +2028,15 @@ async function resolveSpecs(loaded, filter) {
|
|
|
1532
2028
|
source: asset.source,
|
|
1533
2029
|
specHash: specHash(base, styleImageHashes)
|
|
1534
2030
|
};
|
|
1535
|
-
|
|
1536
|
-
const
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
2031
|
+
const resolvedImages = style.styleImages.map((image) => {
|
|
2032
|
+
const hit = styleImageCache.get(path3.resolve(root, image.path));
|
|
2033
|
+
return { base64: hit.base64, width: hit.width, height: hit.height, format: hit.format };
|
|
2034
|
+
});
|
|
2035
|
+
activeProvider.validate?.(resolved, resolvedImages);
|
|
2036
|
+
const estimate = validateCostEstimate(activeProvider.id, activeProvider.estimate(resolved));
|
|
2037
|
+
resolved.cost = estimate.amount;
|
|
2038
|
+
resolved.costUnit = estimate.unit;
|
|
2039
|
+
resolved.candidates = estimate.candidates;
|
|
1541
2040
|
specs.push(resolved);
|
|
1542
2041
|
}
|
|
1543
2042
|
}
|
|
@@ -1561,10 +2060,8 @@ async function resolveStyleImages(loaded, styleId) {
|
|
|
1561
2060
|
const buf = await readFile2(file);
|
|
1562
2061
|
const metadata = imageMetadata(buf);
|
|
1563
2062
|
if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${file}`);
|
|
1564
|
-
if (metadata.width < 1 || metadata.height < 1
|
|
1565
|
-
throw new Error(
|
|
1566
|
-
`Style image exceeds the API's 256x256 limit: ${file} (${metadata.width}x${metadata.height})`
|
|
1567
|
-
);
|
|
2063
|
+
if (metadata.width < 1 || metadata.height < 1) {
|
|
2064
|
+
throw new Error(`Style image has invalid dimensions: ${file}`);
|
|
1568
2065
|
}
|
|
1569
2066
|
out.push({ base64: buf.toString("base64"), ...metadata });
|
|
1570
2067
|
}
|
|
@@ -1770,7 +2267,8 @@ async function acquireFileLock(file) {
|
|
|
1770
2267
|
function spendByUnit(lock) {
|
|
1771
2268
|
const totals = { generations: 0, usd: 0, free: 0 };
|
|
1772
2269
|
for (const entry of Object.values(lock.entries)) {
|
|
1773
|
-
|
|
2270
|
+
const unit = entry.costUnit ?? "generations";
|
|
2271
|
+
totals[unit] = (totals[unit] ?? 0) + (entry.cost ?? 0);
|
|
1774
2272
|
}
|
|
1775
2273
|
return totals;
|
|
1776
2274
|
}
|
|
@@ -1788,16 +2286,16 @@ function resolveOutputPath(recordedPath, manifestDir) {
|
|
|
1788
2286
|
if (path5.win32.isAbsolute(recordedPath)) return recordedPath;
|
|
1789
2287
|
return path5.resolve(manifestDir, recordedPath.split(/[\\/]/).join(path5.sep));
|
|
1790
2288
|
}
|
|
1791
|
-
function expectedOutputPath(spec, role, index, total) {
|
|
1792
|
-
if (total === 1) return spec.outFile;
|
|
2289
|
+
function expectedOutputPath(spec, role, index, total, mediaType) {
|
|
1793
2290
|
const originalExt = path5.extname(spec.outFile);
|
|
1794
|
-
const ext = originalExt || ".png";
|
|
2291
|
+
const ext = mediaType ? mediaExtension(mediaType) : originalExt || ".png";
|
|
1795
2292
|
const stem = originalExt ? spec.outFile.slice(0, -originalExt.length) : spec.outFile;
|
|
2293
|
+
if (total === 1) return `${stem}${ext}`;
|
|
1796
2294
|
const safeRole = (role ?? fallbackOutputRole(index)).replace(/[^a-zA-Z0-9_-]+/g, "-");
|
|
1797
2295
|
return `${stem}-${safeRole}${ext}`;
|
|
1798
2296
|
}
|
|
1799
2297
|
function currentOutputPath(output, spec, index, total) {
|
|
1800
|
-
return expectedOutputPath(spec, output.role, index, total);
|
|
2298
|
+
return expectedOutputPath(spec, output.role, index, total, output.mediaType);
|
|
1801
2299
|
}
|
|
1802
2300
|
function currentEntryOutputPath(entry, spec, index) {
|
|
1803
2301
|
const output = entry.outputs[index];
|
|
@@ -2255,7 +2753,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
2255
2753
|
async function pruneInFlight() {
|
|
2256
2754
|
for (const [id, spec] of [...inFlight]) {
|
|
2257
2755
|
try {
|
|
2258
|
-
const state = await provider.poll(id, spec.generator, spec);
|
|
2756
|
+
const state = await provider.poll(id, spec.generator, { spec, tileFeature: spec.tileFeature });
|
|
2259
2757
|
if (state.status !== "processing") inFlight.delete(id);
|
|
2260
2758
|
lastSlotError = null;
|
|
2261
2759
|
} catch (err) {
|
|
@@ -2307,7 +2805,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
2307
2805
|
await saveLock(lockPath, lock);
|
|
2308
2806
|
lastSubmitAt = Date.now();
|
|
2309
2807
|
try {
|
|
2310
|
-
const refs =
|
|
2808
|
+
const refs = styleImages.get(spec.styleId) ?? [];
|
|
2311
2809
|
const { jobId } = await provider.submit(spec, refs);
|
|
2312
2810
|
upsert(lock, key, {
|
|
2313
2811
|
jobId,
|
|
@@ -2357,7 +2855,8 @@ async function poll(provider, lock, lockPath, opts = {}) {
|
|
|
2357
2855
|
try {
|
|
2358
2856
|
const currentSpec = specByKey.get(key);
|
|
2359
2857
|
const state = await provider.poll(entry.jobId, entry.generator, {
|
|
2360
|
-
tileFeature: entry.tileFeature ?? currentSpec?.tileFeature
|
|
2858
|
+
tileFeature: entry.tileFeature ?? currentSpec?.tileFeature,
|
|
2859
|
+
spec: currentSpec
|
|
2361
2860
|
});
|
|
2362
2861
|
if (state.status === "review") {
|
|
2363
2862
|
upsert(lock, key, { status: "review", reviewObjectId: entry.jobId });
|
|
@@ -2395,7 +2894,6 @@ async function poll(provider, lock, lockPath, opts = {}) {
|
|
|
2395
2894
|
import { existsSync as existsSync6 } from "fs";
|
|
2396
2895
|
import { mkdir as mkdir2, readFile as readFile4, rename as rename2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
2397
2896
|
import path8 from "path";
|
|
2398
|
-
var PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
2399
2897
|
async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
2400
2898
|
const log2 = opts.onProgress ?? (() => {
|
|
2401
2899
|
});
|
|
@@ -2427,7 +2925,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
2427
2925
|
result.skipped++;
|
|
2428
2926
|
continue;
|
|
2429
2927
|
}
|
|
2430
|
-
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 })) : [];
|
|
2928
|
+
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 })) : [];
|
|
2431
2929
|
if (!sources.length) {
|
|
2432
2930
|
upsert(lock, key, {
|
|
2433
2931
|
status: "download-failed",
|
|
@@ -2443,7 +2941,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
2443
2941
|
const recorded = entry.outputs.find(
|
|
2444
2942
|
(o) => source.role ? o.role === source.role : !o.role && sources.length === 1
|
|
2445
2943
|
);
|
|
2446
|
-
const target = recorded ? resolveOutputPath(recorded.path, spec.root) : expectedOutputPath(spec, source.role, index, sources.length);
|
|
2944
|
+
const target = recorded ? resolveOutputPath(recorded.path, spec.root) : expectedOutputPath(spec, source.role, index, sources.length, source.mediaType);
|
|
2447
2945
|
if (existsSync6(target)) {
|
|
2448
2946
|
if (!recorded) {
|
|
2449
2947
|
throw new Error(`refusing to overwrite untracked output ${target}`);
|
|
@@ -2451,11 +2949,19 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
2451
2949
|
if (await sha256File(target) !== recorded.sha256) {
|
|
2452
2950
|
throw new Error(`refusing to overwrite modified output ${target}`);
|
|
2453
2951
|
}
|
|
2454
|
-
if (cacheDir)
|
|
2952
|
+
if (cacheDir) {
|
|
2953
|
+
await cacheMedia(
|
|
2954
|
+
cacheDir,
|
|
2955
|
+
await readFile4(target),
|
|
2956
|
+
recorded.mediaType ?? MediaType.PNG,
|
|
2957
|
+
recorded.sha256
|
|
2958
|
+
);
|
|
2959
|
+
}
|
|
2455
2960
|
outputs.push({ ...recorded, path: portableOutputPath(target, spec.root) });
|
|
2456
2961
|
continue;
|
|
2457
2962
|
}
|
|
2458
|
-
|
|
2963
|
+
const expectedMediaType = source.mediaType ?? recorded?.mediaType ?? MediaType.PNG;
|
|
2964
|
+
let buf = recorded && cacheDir ? await readCachedMedia(cacheDir, recorded.sha256, expectedMediaType) : null;
|
|
2459
2965
|
if (buf) {
|
|
2460
2966
|
log2(` cached ${path8.relative(process.cwd(), target)}`);
|
|
2461
2967
|
} else {
|
|
@@ -2464,24 +2970,25 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
2464
2970
|
}
|
|
2465
2971
|
buf = await provider.download(source.url);
|
|
2466
2972
|
}
|
|
2467
|
-
|
|
2468
|
-
throw new Error(`response for ${source.role ?? "asset"} was not a PNG (${buf.length} bytes)`);
|
|
2469
|
-
}
|
|
2973
|
+
let mediaType;
|
|
2470
2974
|
try {
|
|
2471
|
-
|
|
2975
|
+
mediaType = validateMedia(buf, expectedMediaType);
|
|
2472
2976
|
} catch (err) {
|
|
2977
|
+
const label = expectedMediaType === MediaType.GIF ? "GIF" : "PNG";
|
|
2978
|
+
const mismatch = detectMediaType(buf) !== expectedMediaType;
|
|
2473
2979
|
throw new Error(
|
|
2474
|
-
`response for ${source.role ?? "asset"} was not a valid
|
|
2980
|
+
`response for ${source.role ?? "asset"} was not ${mismatch ? "a" : "a valid"} ${label}` + (mismatch ? ` (${buf.length} bytes)` : `: ${err instanceof Error ? err.message : String(err)}`)
|
|
2475
2981
|
);
|
|
2476
2982
|
}
|
|
2477
|
-
if (cacheDir) await
|
|
2983
|
+
if (cacheDir) await cacheMedia(cacheDir, buf, mediaType);
|
|
2478
2984
|
await mkdir2(path8.dirname(target), { recursive: true });
|
|
2479
2985
|
const tmp = `${target}.pixelkiln.tmp`;
|
|
2480
2986
|
await writeFile2(tmp, buf);
|
|
2481
2987
|
outputs.push({
|
|
2482
2988
|
path: portableOutputPath(target, spec.root),
|
|
2483
2989
|
sha256: sha256(buf),
|
|
2484
|
-
...source.role ? { role: source.role } : {}
|
|
2990
|
+
...source.role ? { role: source.role } : {},
|
|
2991
|
+
mediaType
|
|
2485
2992
|
});
|
|
2486
2993
|
upsert(lock, key, { outputs: mergeOutputs(entry.outputs, outputs) });
|
|
2487
2994
|
await saveLock(lockPath, lock);
|
|
@@ -2518,22 +3025,23 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
2518
3025
|
await saveLock(lockPath, lock);
|
|
2519
3026
|
return result;
|
|
2520
3027
|
}
|
|
2521
|
-
async function
|
|
2522
|
-
const file = path8.join(cacheDir,
|
|
3028
|
+
async function readCachedMedia(cacheDir, hash, mediaType) {
|
|
3029
|
+
const file = path8.join(cacheDir, cacheFileName(hash, mediaType));
|
|
2523
3030
|
if (!existsSync6(file)) return null;
|
|
2524
3031
|
try {
|
|
2525
3032
|
const buf = await readFile4(file);
|
|
2526
|
-
if (
|
|
2527
|
-
|
|
3033
|
+
if (sha256(buf) !== hash) return null;
|
|
3034
|
+
validateMedia(buf, mediaType);
|
|
2528
3035
|
return buf;
|
|
2529
3036
|
} catch {
|
|
2530
3037
|
return null;
|
|
2531
3038
|
}
|
|
2532
3039
|
}
|
|
2533
|
-
async function
|
|
3040
|
+
async function cacheMedia(cacheDir, buf, mediaType, knownHash) {
|
|
2534
3041
|
const hash = knownHash ?? sha256(buf);
|
|
2535
|
-
|
|
2536
|
-
|
|
3042
|
+
validateMedia(buf, mediaType);
|
|
3043
|
+
const file = path8.join(cacheDir, cacheFileName(hash, mediaType));
|
|
3044
|
+
if (await readCachedMedia(cacheDir, hash, mediaType)) return;
|
|
2537
3045
|
await mkdir2(cacheDir, { recursive: true });
|
|
2538
3046
|
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
2539
3047
|
await writeFile2(tmp, buf);
|
|
@@ -2638,7 +3146,10 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
2638
3146
|
const hasSource = Boolean(entry.sourceUrl || entry.sourceUrls?.length);
|
|
2639
3147
|
let hasCache = false;
|
|
2640
3148
|
for (const output of entry.outputs) {
|
|
2641
|
-
const cached = path9.join(
|
|
3149
|
+
const cached = path9.join(
|
|
3150
|
+
cacheDir,
|
|
3151
|
+
cacheFileName(output.sha256, output.mediaType ?? MediaType.PNG)
|
|
3152
|
+
);
|
|
2642
3153
|
if (existsSync7(cached) && await sha256File(cached) === output.sha256) {
|
|
2643
3154
|
hasCache = true;
|
|
2644
3155
|
break;
|
|
@@ -2668,11 +3179,14 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
2668
3179
|
add(
|
|
2669
3180
|
"provider",
|
|
2670
3181
|
"error",
|
|
2671
|
-
opts.apiKeyPresent === false ? "PIXELLAB_API_KEY is not configured
|
|
3182
|
+
opts.apiKeyPresent === false ? `${opts.credentialEnv ?? "PIXELLAB_API_KEY"} is not configured` : "provider is not configured"
|
|
2672
3183
|
);
|
|
3184
|
+
} else if (!opts.provider.balance) {
|
|
3185
|
+
add("provider", "ok", `${opts.provider.id} configured; balance reporting unavailable`);
|
|
2673
3186
|
} else {
|
|
3187
|
+
const balanceFn = opts.provider.balance.bind(opts.provider);
|
|
2674
3188
|
try {
|
|
2675
|
-
const balance = await
|
|
3189
|
+
const balance = await balanceFn();
|
|
2676
3190
|
add("provider", "ok", `${opts.provider.id} reachable; ${balance.remaining} ${balance.unit} remaining`);
|
|
2677
3191
|
} catch (err) {
|
|
2678
3192
|
add("provider", "error", `provider connectivity failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -2715,10 +3229,10 @@ function isSha256Hash(value) {
|
|
|
2715
3229
|
function parseCache(value) {
|
|
2716
3230
|
return HashCacheSchema.parse(value);
|
|
2717
3231
|
}
|
|
2718
|
-
async function loadCache(
|
|
2719
|
-
if (!existsSync8(
|
|
3232
|
+
async function loadCache(path19) {
|
|
3233
|
+
if (!existsSync8(path19)) return { version: 1, hashes: {} };
|
|
2720
3234
|
try {
|
|
2721
|
-
const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile5(
|
|
3235
|
+
const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile5(path19, "utf8")));
|
|
2722
3236
|
if (!parsed.success) return { version: 1, hashes: {} };
|
|
2723
3237
|
return {
|
|
2724
3238
|
version: 1,
|
|
@@ -2730,18 +3244,18 @@ async function loadCache(path17) {
|
|
|
2730
3244
|
return { version: 1, hashes: {} };
|
|
2731
3245
|
}
|
|
2732
3246
|
}
|
|
2733
|
-
async function saveCache(
|
|
3247
|
+
async function saveCache(path19, cache) {
|
|
2734
3248
|
const sorted = {};
|
|
2735
3249
|
for (const key of Object.keys(cache.hashes).sort()) {
|
|
2736
3250
|
const hash = cache.hashes[key];
|
|
2737
3251
|
if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
|
|
2738
3252
|
sorted[key] = hash;
|
|
2739
3253
|
}
|
|
2740
|
-
await mkdir3(pathModule.dirname(pathModule.resolve(
|
|
2741
|
-
const tmp = `${
|
|
3254
|
+
await mkdir3(pathModule.dirname(pathModule.resolve(path19)), { recursive: true });
|
|
3255
|
+
const tmp = `${path19}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
2742
3256
|
try {
|
|
2743
3257
|
await writeFile3(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
|
|
2744
|
-
await rename3(tmp,
|
|
3258
|
+
await rename3(tmp, path19);
|
|
2745
3259
|
} finally {
|
|
2746
3260
|
await rm3(tmp, { force: true });
|
|
2747
3261
|
}
|
|
@@ -3259,7 +3773,7 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
|
|
|
3259
3773
|
const entry = lock.entries[key];
|
|
3260
3774
|
if (!group || !entry?.reviewObjectId) continue;
|
|
3261
3775
|
if (!Number.isInteger(index) || index < 0 || index >= group.frameUrls.length) continue;
|
|
3262
|
-
const { objectId, sourceUrl } = await provider
|
|
3776
|
+
const { objectId, sourceUrl } = await requireSelectCandidate(provider)(
|
|
3263
3777
|
entry.reviewObjectId,
|
|
3264
3778
|
index,
|
|
3265
3779
|
`asset:${entry.assetId}`,
|
|
@@ -3349,6 +3863,7 @@ function buildManifest(name, styleId, generator, outDir, scanned) {
|
|
|
3349
3863
|
}
|
|
3350
3864
|
return {
|
|
3351
3865
|
name,
|
|
3866
|
+
provider: "pixellab",
|
|
3352
3867
|
styles: {
|
|
3353
3868
|
[styleId]: {
|
|
3354
3869
|
generator,
|
|
@@ -3361,7 +3876,8 @@ function buildManifest(name, styleId, generator, outDir, scanned) {
|
|
|
3361
3876
|
styleImages: [],
|
|
3362
3877
|
palette: [],
|
|
3363
3878
|
outDir,
|
|
3364
|
-
tags: [name]
|
|
3879
|
+
tags: [name],
|
|
3880
|
+
providerOptions: {}
|
|
3365
3881
|
}
|
|
3366
3882
|
},
|
|
3367
3883
|
assets
|
|
@@ -3382,6 +3898,7 @@ async function writeManifestFile(target, manifest) {
|
|
|
3382
3898
|
// src/pipeline/salvage.ts
|
|
3383
3899
|
import { readFile as readFile8 } from "fs/promises";
|
|
3384
3900
|
import { existsSync as existsSync11 } from "fs";
|
|
3901
|
+
import path12 from "path";
|
|
3385
3902
|
async function loadClaims(lockPaths) {
|
|
3386
3903
|
const claimed = /* @__PURE__ */ new Set();
|
|
3387
3904
|
for (const p of lockPaths) {
|
|
@@ -3429,6 +3946,26 @@ function matchStyleByPattern(prompt, manifest) {
|
|
|
3429
3946
|
}
|
|
3430
3947
|
return null;
|
|
3431
3948
|
}
|
|
3949
|
+
async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, claimPaths) {
|
|
3950
|
+
const own = path12.resolve(ownManifestPath);
|
|
3951
|
+
const siblingManifestPaths = [
|
|
3952
|
+
.../* @__PURE__ */ new Set([
|
|
3953
|
+
...workspaceManifestPaths,
|
|
3954
|
+
...claimPaths.map((c) => path12.join(path12.dirname(path12.resolve(c)), "pixelkiln.manifest.json"))
|
|
3955
|
+
])
|
|
3956
|
+
];
|
|
3957
|
+
const siblings = [];
|
|
3958
|
+
for (const siblingManifestPath of siblingManifestPaths) {
|
|
3959
|
+
if (path12.resolve(siblingManifestPath) === own) continue;
|
|
3960
|
+
if (!existsSync11(siblingManifestPath)) continue;
|
|
3961
|
+
try {
|
|
3962
|
+
const { manifest } = await loadManifest(siblingManifestPath);
|
|
3963
|
+
siblings.push({ label: path12.basename(path12.dirname(siblingManifestPath)), manifest });
|
|
3964
|
+
} catch {
|
|
3965
|
+
}
|
|
3966
|
+
}
|
|
3967
|
+
return siblings;
|
|
3968
|
+
}
|
|
3432
3969
|
function groupOrphansByStyle(orphans, manifest, siblings = []) {
|
|
3433
3970
|
const matched = /* @__PURE__ */ new Map();
|
|
3434
3971
|
const elsewhere = /* @__PURE__ */ new Map();
|
|
@@ -3538,10 +4075,237 @@ async function applyTags(provider, decisions, existing, opts = {}) {
|
|
|
3538
4075
|
return { tagged, failed };
|
|
3539
4076
|
}
|
|
3540
4077
|
|
|
3541
|
-
// src/
|
|
3542
|
-
import { readFile as readFile9 } from "fs/promises";
|
|
4078
|
+
// src/workspace.ts
|
|
4079
|
+
import { mkdir as mkdir4, readFile as readFile9, rename as rename4, rm as rm4, writeFile as writeFile6 } from "fs/promises";
|
|
3543
4080
|
import { existsSync as existsSync12 } from "fs";
|
|
3544
|
-
import
|
|
4081
|
+
import path13 from "path";
|
|
4082
|
+
import { z as z4 } from "zod";
|
|
4083
|
+
var WorkspaceProjectSchema = z4.object({
|
|
4084
|
+
id: z4.string().min(1),
|
|
4085
|
+
/** Manifest path, relative to the catalog file's own directory. */
|
|
4086
|
+
manifest: z4.string().min(1),
|
|
4087
|
+
/** Lockfile path, relative to the catalog file's own directory. */
|
|
4088
|
+
lock: z4.string().min(1),
|
|
4089
|
+
provider: z4.string().min(1).default("pixellab"),
|
|
4090
|
+
/** Free-form label for a shared account, e.g. distinguishing sandboxes. */
|
|
4091
|
+
account: z4.string().optional()
|
|
4092
|
+
}).strict();
|
|
4093
|
+
var WorkspaceSchema = z4.object({
|
|
4094
|
+
version: z4.literal(1),
|
|
4095
|
+
projects: z4.array(WorkspaceProjectSchema).default([])
|
|
4096
|
+
}).strict();
|
|
4097
|
+
function parseWorkspace(raw) {
|
|
4098
|
+
const parsed = WorkspaceSchema.safeParse(raw);
|
|
4099
|
+
if (parsed.success) return parsed.data;
|
|
4100
|
+
throw new Error(
|
|
4101
|
+
`Workspace catalog is not valid v1:
|
|
4102
|
+
${parsed.error.issues.slice(0, 5).map((i) => ` ${i.path.join(".")}: ${i.message}`).join("\n")}`
|
|
4103
|
+
);
|
|
4104
|
+
}
|
|
4105
|
+
async function loadWorkspace(workspacePath) {
|
|
4106
|
+
if (!existsSync12(workspacePath)) return { version: 1, projects: [] };
|
|
4107
|
+
let raw;
|
|
4108
|
+
try {
|
|
4109
|
+
raw = JSON.parse(await readFile9(workspacePath, "utf8"));
|
|
4110
|
+
} catch (err) {
|
|
4111
|
+
throw new Error(
|
|
4112
|
+
`Workspace catalog at ${workspacePath} is malformed:
|
|
4113
|
+
${err instanceof Error ? err.message : String(err)}`
|
|
4114
|
+
);
|
|
4115
|
+
}
|
|
4116
|
+
return parseWorkspace(raw);
|
|
4117
|
+
}
|
|
4118
|
+
async function saveWorkspace(workspacePath, ws) {
|
|
4119
|
+
const sorted = {
|
|
4120
|
+
version: 1,
|
|
4121
|
+
projects: [...ws.projects].sort((a, b) => a.id.localeCompare(b.id))
|
|
4122
|
+
};
|
|
4123
|
+
await mkdir4(path13.dirname(path13.resolve(workspacePath)), { recursive: true });
|
|
4124
|
+
const tmp = `${workspacePath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
4125
|
+
try {
|
|
4126
|
+
await writeFile6(tmp, JSON.stringify(sorted, null, 2) + "\n");
|
|
4127
|
+
await rename4(tmp, workspacePath);
|
|
4128
|
+
} finally {
|
|
4129
|
+
await rm4(tmp, { force: true });
|
|
4130
|
+
}
|
|
4131
|
+
}
|
|
4132
|
+
function toPortablePath(dir, absolute) {
|
|
4133
|
+
return path13.relative(dir, absolute).split(path13.sep).join("/");
|
|
4134
|
+
}
|
|
4135
|
+
function resolveProject(dir, project) {
|
|
4136
|
+
return {
|
|
4137
|
+
manifestPath: path13.resolve(dir, project.manifest.split("/").join(path13.sep)),
|
|
4138
|
+
lockPath: path13.resolve(dir, project.lock.split("/").join(path13.sep))
|
|
4139
|
+
};
|
|
4140
|
+
}
|
|
4141
|
+
function validateWorkspace(ws, dir) {
|
|
4142
|
+
const diagnostics = [];
|
|
4143
|
+
const idCounts = /* @__PURE__ */ new Map();
|
|
4144
|
+
const lockOwners = /* @__PURE__ */ new Map();
|
|
4145
|
+
const manifestOwners = /* @__PURE__ */ new Map();
|
|
4146
|
+
for (const project of ws.projects) {
|
|
4147
|
+
idCounts.set(project.id, (idCounts.get(project.id) ?? 0) + 1);
|
|
4148
|
+
const { manifestPath, lockPath } = resolveProject(dir, project);
|
|
4149
|
+
lockOwners.set(lockPath, [...lockOwners.get(lockPath) ?? [], project.id]);
|
|
4150
|
+
manifestOwners.set(manifestPath, [...manifestOwners.get(manifestPath) ?? [], project.id]);
|
|
4151
|
+
if (path13.isAbsolute(project.manifest) || path13.isAbsolute(project.lock)) {
|
|
4152
|
+
diagnostics.push({
|
|
4153
|
+
id: "absolute-path",
|
|
4154
|
+
level: "warning",
|
|
4155
|
+
message: `project "${project.id}" stores an absolute path \u2014 the catalog will not resolve correctly if this tree is cloned or moved elsewhere`
|
|
4156
|
+
});
|
|
4157
|
+
}
|
|
4158
|
+
if (!existsSync12(manifestPath)) {
|
|
4159
|
+
diagnostics.push({
|
|
4160
|
+
id: "missing-manifest",
|
|
4161
|
+
level: "error",
|
|
4162
|
+
message: `project "${project.id}" manifest not found: ${manifestPath}`
|
|
4163
|
+
});
|
|
4164
|
+
}
|
|
4165
|
+
if (!existsSync12(lockPath)) {
|
|
4166
|
+
diagnostics.push({
|
|
4167
|
+
id: "missing-lock",
|
|
4168
|
+
level: "error",
|
|
4169
|
+
message: `project "${project.id}" lockfile not found: ${lockPath}`
|
|
4170
|
+
});
|
|
4171
|
+
}
|
|
4172
|
+
}
|
|
4173
|
+
for (const [id, count] of idCounts) {
|
|
4174
|
+
if (count > 1) {
|
|
4175
|
+
diagnostics.push({
|
|
4176
|
+
id: "duplicate-id",
|
|
4177
|
+
level: "error",
|
|
4178
|
+
message: `project id "${id}" is registered ${count} times`
|
|
4179
|
+
});
|
|
4180
|
+
}
|
|
4181
|
+
}
|
|
4182
|
+
for (const [lockPath, ids] of lockOwners) {
|
|
4183
|
+
if (ids.length > 1) {
|
|
4184
|
+
diagnostics.push({
|
|
4185
|
+
id: "duplicate-lock",
|
|
4186
|
+
level: "error",
|
|
4187
|
+
message: `${ids.join(", ")} all register the same lockfile: ${lockPath}`
|
|
4188
|
+
});
|
|
4189
|
+
}
|
|
4190
|
+
}
|
|
4191
|
+
for (const [manifestPath, ids] of manifestOwners) {
|
|
4192
|
+
if (ids.length > 1) {
|
|
4193
|
+
diagnostics.push({
|
|
4194
|
+
id: "duplicate-manifest",
|
|
4195
|
+
level: "warning",
|
|
4196
|
+
message: `${ids.join(", ")} share manifest ${manifestPath} \u2014 expected only when they are variant lockfiles beside one manifest`
|
|
4197
|
+
});
|
|
4198
|
+
}
|
|
4199
|
+
}
|
|
4200
|
+
const providers = new Set(ws.projects.map((p) => p.provider));
|
|
4201
|
+
if (providers.size > 1) {
|
|
4202
|
+
diagnostics.push({
|
|
4203
|
+
id: "mixed-provider",
|
|
4204
|
+
level: "warning",
|
|
4205
|
+
message: `registered projects use different providers: ${[...providers].sort().join(", ")} \u2014 spend totals are kept separate per unit, but confirm this is intentional`
|
|
4206
|
+
});
|
|
4207
|
+
}
|
|
4208
|
+
return diagnostics;
|
|
4209
|
+
}
|
|
4210
|
+
|
|
4211
|
+
// src/pipeline/workspace.ts
|
|
4212
|
+
async function workspaceClaims(ws, dir) {
|
|
4213
|
+
const lockPaths = [];
|
|
4214
|
+
const byProject = {};
|
|
4215
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
4216
|
+
for (const project of ws.projects) {
|
|
4217
|
+
const { lockPath } = resolveProject(dir, project);
|
|
4218
|
+
lockPaths.push(lockPath);
|
|
4219
|
+
let projectClaims;
|
|
4220
|
+
try {
|
|
4221
|
+
projectClaims = await loadClaims([lockPath]);
|
|
4222
|
+
} catch (err) {
|
|
4223
|
+
throw new Error(
|
|
4224
|
+
`Project "${project.id}" lockfile is unreadable: ${err instanceof Error ? err.message : String(err)}`
|
|
4225
|
+
);
|
|
4226
|
+
}
|
|
4227
|
+
byProject[project.id] = projectClaims.size;
|
|
4228
|
+
for (const id of projectClaims) claimed.add(id);
|
|
4229
|
+
}
|
|
4230
|
+
return { claimed, byProject, lockPaths };
|
|
4231
|
+
}
|
|
4232
|
+
function emptyStateCounts() {
|
|
4233
|
+
return {
|
|
4234
|
+
ok: 0,
|
|
4235
|
+
missing: 0,
|
|
4236
|
+
untracked: 0,
|
|
4237
|
+
stale: 0,
|
|
4238
|
+
orphaned: 0,
|
|
4239
|
+
"in-flight": 0,
|
|
4240
|
+
recoverable: 0,
|
|
4241
|
+
failed: 0
|
|
4242
|
+
};
|
|
4243
|
+
}
|
|
4244
|
+
async function workspaceStatus(ws, dir) {
|
|
4245
|
+
const diagnostics = validateWorkspace(ws, dir);
|
|
4246
|
+
const projects = [];
|
|
4247
|
+
const totalsByState = emptyStateCounts();
|
|
4248
|
+
const totalsSpend = { generations: 0, usd: 0, free: 0 };
|
|
4249
|
+
for (const project of ws.projects) {
|
|
4250
|
+
const { manifestPath, lockPath } = resolveProject(dir, project);
|
|
4251
|
+
const base = {
|
|
4252
|
+
id: project.id,
|
|
4253
|
+
provider: project.provider,
|
|
4254
|
+
account: project.account ?? null,
|
|
4255
|
+
manifest: manifestPath,
|
|
4256
|
+
lock: lockPath
|
|
4257
|
+
};
|
|
4258
|
+
try {
|
|
4259
|
+
const provider = createProvider(project.provider, "offline");
|
|
4260
|
+
const loaded = await loadManifest(manifestPath);
|
|
4261
|
+
const specs = await resolveSpecs(loaded, { provider });
|
|
4262
|
+
const lock = await loadLock(lockPath);
|
|
4263
|
+
normalizeLockOutputPaths(lock, specs);
|
|
4264
|
+
const plan = await buildPlan(specs, lock);
|
|
4265
|
+
const byState = summarize(plan);
|
|
4266
|
+
const spend = spendByUnit(lock);
|
|
4267
|
+
for (const state of Object.keys(byState)) {
|
|
4268
|
+
totalsByState[state] += byState[state];
|
|
4269
|
+
}
|
|
4270
|
+
for (const unit of Object.keys(spend)) {
|
|
4271
|
+
totalsSpend[unit] = (totalsSpend[unit] ?? 0) + (spend[unit] ?? 0);
|
|
4272
|
+
}
|
|
4273
|
+
projects.push({
|
|
4274
|
+
...base,
|
|
4275
|
+
entries: Object.keys(lock.entries).length,
|
|
4276
|
+
byState,
|
|
4277
|
+
spendByUnit: spend,
|
|
4278
|
+
error: null
|
|
4279
|
+
});
|
|
4280
|
+
} catch (err) {
|
|
4281
|
+
projects.push({
|
|
4282
|
+
...base,
|
|
4283
|
+
entries: 0,
|
|
4284
|
+
byState: emptyStateCounts(),
|
|
4285
|
+
spendByUnit: { generations: 0, usd: 0, free: 0 },
|
|
4286
|
+
error: err instanceof Error ? err.message : String(err)
|
|
4287
|
+
});
|
|
4288
|
+
}
|
|
4289
|
+
}
|
|
4290
|
+
let claims = 0;
|
|
4291
|
+
try {
|
|
4292
|
+
claims = (await workspaceClaims(ws, dir)).claimed.size;
|
|
4293
|
+
} catch {
|
|
4294
|
+
}
|
|
4295
|
+
return {
|
|
4296
|
+
version: 1,
|
|
4297
|
+
safe: !diagnostics.some((d) => d.level === "error") && projects.every((p) => !p.error),
|
|
4298
|
+
dir,
|
|
4299
|
+
projects,
|
|
4300
|
+
totals: { byState: totalsByState, spendByUnit: totalsSpend, claims },
|
|
4301
|
+
diagnostics
|
|
4302
|
+
};
|
|
4303
|
+
}
|
|
4304
|
+
|
|
4305
|
+
// src/pipeline/audit.ts
|
|
4306
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
4307
|
+
import { existsSync as existsSync13 } from "fs";
|
|
4308
|
+
import path14 from "path";
|
|
3545
4309
|
function colorDistance(a, b) {
|
|
3546
4310
|
const rmean = (a.r + b.r) / 2;
|
|
3547
4311
|
const dr = a.r - b.r;
|
|
@@ -3586,12 +4350,12 @@ async function auditStyle(loaded, specs, styleId, lock) {
|
|
|
3586
4350
|
const unreadable = [];
|
|
3587
4351
|
for (const spec of mine) {
|
|
3588
4352
|
for (const output of resolveSpecOutputs(spec, lock, loaded.root)) {
|
|
3589
|
-
if (!
|
|
4353
|
+
if (!existsSync13(output.absolutePath)) {
|
|
3590
4354
|
missing.push(output.id);
|
|
3591
4355
|
continue;
|
|
3592
4356
|
}
|
|
3593
4357
|
try {
|
|
3594
|
-
const png = decodePng(await
|
|
4358
|
+
const png = decodePng(await readFile10(output.absolutePath));
|
|
3595
4359
|
const palette = extractPalette(png, 12);
|
|
3596
4360
|
assets.push({
|
|
3597
4361
|
assetId: spec.assetId,
|
|
@@ -3615,10 +4379,10 @@ async function auditStyle(loaded, specs, styleId, lock) {
|
|
|
3615
4379
|
let referenceFromStyleImages = false;
|
|
3616
4380
|
const refPalettes = [];
|
|
3617
4381
|
for (const img of style.styleImages) {
|
|
3618
|
-
const abs =
|
|
3619
|
-
if (!
|
|
4382
|
+
const abs = path14.resolve(loaded.root, img.path);
|
|
4383
|
+
if (!existsSync13(abs)) continue;
|
|
3620
4384
|
try {
|
|
3621
|
-
refPalettes.push(extractPalette(decodePng(await
|
|
4385
|
+
refPalettes.push(extractPalette(decodePng(await readFile10(abs)), 12));
|
|
3622
4386
|
} catch {
|
|
3623
4387
|
}
|
|
3624
4388
|
}
|
|
@@ -3694,16 +4458,15 @@ function hex(c) {
|
|
|
3694
4458
|
}
|
|
3695
4459
|
|
|
3696
4460
|
// src/pipeline/cache-health.ts
|
|
3697
|
-
import { existsSync as
|
|
3698
|
-
import { readFile as
|
|
3699
|
-
import
|
|
3700
|
-
var PNG_SIGNATURE2 = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
4461
|
+
import { existsSync as existsSync14 } from "fs";
|
|
4462
|
+
import { readFile as readFile11, readdir as readdir2, rm as rm5 } from "fs/promises";
|
|
4463
|
+
import path15 from "path";
|
|
3701
4464
|
async function inspectCaches(lock, lockPath, options = {}) {
|
|
3702
|
-
if (options.prune && !
|
|
3703
|
-
throw new Error(`Refusing to prune without an existing lockfile at ${
|
|
4465
|
+
if (options.prune && !existsSync14(lockPath)) {
|
|
4466
|
+
throw new Error(`Refusing to prune without an existing lockfile at ${path15.resolve(lockPath)}`);
|
|
3704
4467
|
}
|
|
3705
|
-
const contentDir =
|
|
3706
|
-
const remotePath =
|
|
4468
|
+
const contentDir = path15.resolve(path15.dirname(lockPath), ".pixelkiln", "cache");
|
|
4469
|
+
const remotePath = path15.resolve(cachePathFor(lockPath));
|
|
3707
4470
|
const referenced = new Set(
|
|
3708
4471
|
Object.values(lock.entries).flatMap((entry) => entry.outputs.map((output) => output.sha256))
|
|
3709
4472
|
);
|
|
@@ -3717,7 +4480,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
3717
4480
|
]);
|
|
3718
4481
|
for (const name of names) {
|
|
3719
4482
|
try {
|
|
3720
|
-
await
|
|
4483
|
+
await rm5(path15.join(contentDir, name), { force: true });
|
|
3721
4484
|
removed.contentFiles++;
|
|
3722
4485
|
} catch {
|
|
3723
4486
|
}
|
|
@@ -3726,7 +4489,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
3726
4489
|
await saveCache(remotePath, { version: 1, hashes: {} });
|
|
3727
4490
|
removed.resetRemoteHashCache = true;
|
|
3728
4491
|
} else if (remoteHashes.invalidIds.length) {
|
|
3729
|
-
const cache = parseCache(JSON.parse(await
|
|
4492
|
+
const cache = parseCache(JSON.parse(await readFile11(remotePath, "utf8")));
|
|
3730
4493
|
for (const id of remoteHashes.invalidIds) delete cache.hashes[id];
|
|
3731
4494
|
removed.remoteHashEntries = remoteHashes.invalidIds.length;
|
|
3732
4495
|
await saveCache(remotePath, cache);
|
|
@@ -3744,7 +4507,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
3744
4507
|
async function inspectContentCache(contentDir, referenced) {
|
|
3745
4508
|
const report = {
|
|
3746
4509
|
path: contentDir,
|
|
3747
|
-
exists:
|
|
4510
|
+
exists: existsSync14(contentDir),
|
|
3748
4511
|
files: 0,
|
|
3749
4512
|
bytes: 0,
|
|
3750
4513
|
valid: 0,
|
|
@@ -3765,11 +4528,12 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
3765
4528
|
continue;
|
|
3766
4529
|
}
|
|
3767
4530
|
report.files++;
|
|
3768
|
-
const file =
|
|
3769
|
-
const
|
|
4531
|
+
const file = path15.join(contentDir, entry.name);
|
|
4532
|
+
const mediaType = mediaTypeFromExtension(entry.name);
|
|
4533
|
+
const expected = mediaType ? entry.name.slice(0, -4) : "";
|
|
3770
4534
|
let bytes;
|
|
3771
4535
|
try {
|
|
3772
|
-
bytes = await
|
|
4536
|
+
bytes = await readFile11(file);
|
|
3773
4537
|
report.bytes += bytes.length;
|
|
3774
4538
|
} catch (err) {
|
|
3775
4539
|
report.invalid.push({
|
|
@@ -3779,11 +4543,7 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
3779
4543
|
continue;
|
|
3780
4544
|
}
|
|
3781
4545
|
if (!isSha256Hash(expected)) {
|
|
3782
|
-
report.invalid.push({ name: entry.name, reason: "filename is not <sha256>.png" });
|
|
3783
|
-
continue;
|
|
3784
|
-
}
|
|
3785
|
-
if (!bytes.subarray(0, 8).equals(PNG_SIGNATURE2)) {
|
|
3786
|
-
report.invalid.push({ name: entry.name, reason: "not a PNG" });
|
|
4546
|
+
report.invalid.push({ name: entry.name, reason: "filename is not <sha256>.png or <sha256>.gif" });
|
|
3787
4547
|
continue;
|
|
3788
4548
|
}
|
|
3789
4549
|
if (sha256(bytes) !== expected) {
|
|
@@ -3791,11 +4551,11 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
3791
4551
|
continue;
|
|
3792
4552
|
}
|
|
3793
4553
|
try {
|
|
3794
|
-
|
|
4554
|
+
validateMedia(bytes, mediaType);
|
|
3795
4555
|
} catch (err) {
|
|
3796
4556
|
report.invalid.push({
|
|
3797
4557
|
name: entry.name,
|
|
3798
|
-
reason: `invalid PNG: ${err instanceof Error ? err.message : String(err)}`
|
|
4558
|
+
reason: `invalid ${mediaType === "image/gif" ? "GIF" : "PNG"}: ${err instanceof Error ? err.message : String(err)}`
|
|
3799
4559
|
});
|
|
3800
4560
|
continue;
|
|
3801
4561
|
}
|
|
@@ -3811,7 +4571,7 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
3811
4571
|
async function inspectRemoteHashCache(remotePath) {
|
|
3812
4572
|
const report = {
|
|
3813
4573
|
path: remotePath,
|
|
3814
|
-
exists:
|
|
4574
|
+
exists: existsSync14(remotePath),
|
|
3815
4575
|
entries: 0,
|
|
3816
4576
|
valid: 0,
|
|
3817
4577
|
invalidIds: [],
|
|
@@ -3820,7 +4580,7 @@ async function inspectRemoteHashCache(remotePath) {
|
|
|
3820
4580
|
if (!report.exists) return report;
|
|
3821
4581
|
let cache;
|
|
3822
4582
|
try {
|
|
3823
|
-
cache = parseCache(JSON.parse(await
|
|
4583
|
+
cache = parseCache(JSON.parse(await readFile11(remotePath, "utf8")));
|
|
3824
4584
|
} catch (err) {
|
|
3825
4585
|
report.error = err instanceof Error ? err.message : String(err);
|
|
3826
4586
|
return report;
|
|
@@ -4091,8 +4851,8 @@ function isRecord(value) {
|
|
|
4091
4851
|
}
|
|
4092
4852
|
|
|
4093
4853
|
// src/pick/salvage-server.ts
|
|
4094
|
-
import { mkdir as
|
|
4095
|
-
import
|
|
4854
|
+
import { mkdir as mkdir5, writeFile as writeFile7, readFile as readFile12 } from "fs/promises";
|
|
4855
|
+
import path16 from "path";
|
|
4096
4856
|
|
|
4097
4857
|
// src/pick/salvage-sheet.ts
|
|
4098
4858
|
var escapeHtml2 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
@@ -4251,13 +5011,13 @@ refresh();
|
|
|
4251
5011
|
}
|
|
4252
5012
|
|
|
4253
5013
|
// src/pick/salvage-server.ts
|
|
4254
|
-
var
|
|
5014
|
+
var PNG_SIGNATURE2 = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
4255
5015
|
async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
4256
5016
|
const log2 = opts.onProgress ?? (() => {
|
|
4257
5017
|
});
|
|
4258
5018
|
const html = renderSalvageSheet(orphans, {
|
|
4259
5019
|
styleId: ctx.styleId,
|
|
4260
|
-
importDir:
|
|
5020
|
+
importDir: path16.relative(process.cwd(), ctx.importDir) || "."
|
|
4261
5021
|
});
|
|
4262
5022
|
const byId = new Map(orphans.map((o) => [o.id, o]));
|
|
4263
5023
|
const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
|
|
@@ -4283,13 +5043,13 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
4283
5043
|
if (decision.action === "import") {
|
|
4284
5044
|
try {
|
|
4285
5045
|
const buf = await provider.download(orphan.previewUrl);
|
|
4286
|
-
if (!buf.subarray(0, 8).equals(
|
|
5046
|
+
if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
|
|
4287
5047
|
decodePng(buf);
|
|
4288
5048
|
const assetId = idFromPrompt(orphan.prompt, taken);
|
|
4289
|
-
const rel =
|
|
4290
|
-
const outFile =
|
|
4291
|
-
await
|
|
4292
|
-
await
|
|
5049
|
+
const rel = path16.join("_salvaged", `${assetId}.png`);
|
|
5050
|
+
const outFile = path16.resolve(ctx.importDir, rel);
|
|
5051
|
+
await mkdir5(path16.dirname(outFile), { recursive: true });
|
|
5052
|
+
await writeFile7(outFile, buf);
|
|
4293
5053
|
ctx.manifest.assets[assetId] = {
|
|
4294
5054
|
prompt: orphan.prompt,
|
|
4295
5055
|
promptByStyle: {},
|
|
@@ -4315,7 +5075,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
4315
5075
|
error: null,
|
|
4316
5076
|
sourceUrl: orphan.previewUrl,
|
|
4317
5077
|
outputs: [{
|
|
4318
|
-
path: portableOutputPath(outFile,
|
|
5078
|
+
path: portableOutputPath(outFile, path16.dirname(ctx.manifestPath)),
|
|
4319
5079
|
sha256: sha256(buf)
|
|
4320
5080
|
}],
|
|
4321
5081
|
submittedAt: orphan.createdAt,
|
|
@@ -4339,9 +5099,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
4339
5099
|
}
|
|
4340
5100
|
}
|
|
4341
5101
|
await applyTags(provider, decisions, existingTags, { onProgress: log2 });
|
|
4342
|
-
const raw = JSON.parse(await
|
|
5102
|
+
const raw = JSON.parse(await readFile12(ctx.manifestPath, "utf8"));
|
|
4343
5103
|
for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
|
|
4344
|
-
await
|
|
5104
|
+
await writeFile7(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
|
|
4345
5105
|
await saveLock(ctx.lockPath, ctx.lock);
|
|
4346
5106
|
return result;
|
|
4347
5107
|
}
|
|
@@ -4349,9 +5109,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
4349
5109
|
}
|
|
4350
5110
|
|
|
4351
5111
|
// src/artifacts.ts
|
|
4352
|
-
import
|
|
5112
|
+
import path17 from "path";
|
|
4353
5113
|
import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
|
|
4354
|
-
import { mkdir as
|
|
5114
|
+
import { mkdir as mkdir6, readFile as readFile13, rename as rename5, rm as rm6, writeFile as writeFile8 } from "fs/promises";
|
|
4355
5115
|
var activeTransactions = /* @__PURE__ */ new Set();
|
|
4356
5116
|
function message(error) {
|
|
4357
5117
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -4360,7 +5120,7 @@ function digest(data) {
|
|
|
4360
5120
|
return createHash2("sha256").update(data).digest("hex");
|
|
4361
5121
|
}
|
|
4362
5122
|
function portableRelative(from, to) {
|
|
4363
|
-
return
|
|
5123
|
+
return path17.relative(from, path17.resolve(to)).split(path17.sep).join("/") || ".";
|
|
4364
5124
|
}
|
|
4365
5125
|
function canonical(value) {
|
|
4366
5126
|
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
@@ -4411,7 +5171,7 @@ function parseArtifactManifest(absolute, data) {
|
|
|
4411
5171
|
}
|
|
4412
5172
|
async function readOptional(file) {
|
|
4413
5173
|
try {
|
|
4414
|
-
return await
|
|
5174
|
+
return await readFile13(file);
|
|
4415
5175
|
} catch (error) {
|
|
4416
5176
|
if (isCode(error, "ENOENT")) return null;
|
|
4417
5177
|
throw error;
|
|
@@ -4430,8 +5190,8 @@ function processIsAlive(pid) {
|
|
|
4430
5190
|
}
|
|
4431
5191
|
}
|
|
4432
5192
|
function validTemporaryPath(candidate, destination, type) {
|
|
4433
|
-
return
|
|
4434
|
-
`.${
|
|
5193
|
+
return path17.dirname(candidate) === path17.dirname(destination) && path17.basename(candidate).startsWith(
|
|
5194
|
+
`.${path17.basename(destination)}.pixelkiln-${type}-`
|
|
4435
5195
|
);
|
|
4436
5196
|
}
|
|
4437
5197
|
function parseTransaction(journal, data) {
|
|
@@ -4447,14 +5207,14 @@ function parseTransaction(journal, data) {
|
|
|
4447
5207
|
return raw;
|
|
4448
5208
|
}
|
|
4449
5209
|
async function removeTransactionFiles(journal) {
|
|
4450
|
-
await
|
|
4451
|
-
await
|
|
5210
|
+
await rm6(journal, { force: true });
|
|
5211
|
+
await rm6(transactionMarker(journal), { force: true });
|
|
4452
5212
|
}
|
|
4453
5213
|
async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
|
|
4454
|
-
const journal =
|
|
5214
|
+
const journal = path17.resolve(recoveryFile);
|
|
4455
5215
|
const bytes = await readOptional(journal);
|
|
4456
5216
|
if (!bytes) {
|
|
4457
|
-
await
|
|
5217
|
+
await rm6(transactionMarker(journal), { force: true });
|
|
4458
5218
|
return;
|
|
4459
5219
|
}
|
|
4460
5220
|
if (activeTransactions.has(journal)) {
|
|
@@ -4467,7 +5227,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
|
|
|
4467
5227
|
);
|
|
4468
5228
|
}
|
|
4469
5229
|
for (const entry of transaction.entries) {
|
|
4470
|
-
if (typeof entry.destination !== "string" || typeof entry.stage !== "string" || typeof entry.sha256 !== "string" || entry.backup !== void 0 && typeof entry.backup !== "string" || !allowedDestinations.has(
|
|
5230
|
+
if (typeof entry.destination !== "string" || typeof entry.stage !== "string" || typeof entry.sha256 !== "string" || entry.backup !== void 0 && typeof entry.backup !== "string" || !allowedDestinations.has(path17.resolve(entry.destination)) || !validTemporaryPath(entry.stage, entry.destination, "stage") || entry.backup !== void 0 && !validTemporaryPath(entry.backup, entry.destination, "backup")) {
|
|
4471
5231
|
throw new Error(
|
|
4472
5232
|
`Refusing unsafe artifact recovery from ${journal}; its destinations or temporary paths do not match the current bundle.`
|
|
4473
5233
|
);
|
|
@@ -4493,7 +5253,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
|
|
|
4493
5253
|
}
|
|
4494
5254
|
await remove2(entry.destination, errors);
|
|
4495
5255
|
try {
|
|
4496
|
-
await
|
|
5256
|
+
await rename5(entry.backup, entry.destination);
|
|
4497
5257
|
} catch (error) {
|
|
4498
5258
|
errors.push(
|
|
4499
5259
|
`could not restore ${entry.destination}; previous file remains at ${entry.backup}: ${message(error)}`
|
|
@@ -4513,7 +5273,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
|
|
|
4513
5273
|
await removeTransactionFiles(journal);
|
|
4514
5274
|
}
|
|
4515
5275
|
function createArtifactBundleManifest(manifestPath, outputs, provenance) {
|
|
4516
|
-
const root =
|
|
5276
|
+
const root = path17.dirname(path17.resolve(manifestPath));
|
|
4517
5277
|
const sources = provenance.sources.map((source) => ({
|
|
4518
5278
|
...source,
|
|
4519
5279
|
path: portableRelative(root, source.path)
|
|
@@ -4543,11 +5303,11 @@ function withArtifactManifest(manifestPath, outputs, provenance) {
|
|
|
4543
5303
|
];
|
|
4544
5304
|
}
|
|
4545
5305
|
async function writeManagedArtifactBundle(manifestPath, outputs, provenance, options = {}) {
|
|
4546
|
-
const absoluteManifest =
|
|
5306
|
+
const absoluteManifest = path17.resolve(manifestPath);
|
|
4547
5307
|
const recoveryFile = `${absoluteManifest}.transaction`;
|
|
4548
5308
|
const allowedDestinations = /* @__PURE__ */ new Set([
|
|
4549
5309
|
absoluteManifest,
|
|
4550
|
-
...outputs.map((output) =>
|
|
5310
|
+
...outputs.map((output) => path17.resolve(output.path))
|
|
4551
5311
|
]);
|
|
4552
5312
|
await recoverArtifactTransaction(recoveryFile, allowedDestinations);
|
|
4553
5313
|
const existingManifestBytes = await readOptional(absoluteManifest);
|
|
@@ -4565,13 +5325,13 @@ async function writeManagedArtifactBundle(manifestPath, outputs, provenance, opt
|
|
|
4565
5325
|
}
|
|
4566
5326
|
}
|
|
4567
5327
|
if (!options.force) {
|
|
4568
|
-
const root =
|
|
5328
|
+
const root = path17.dirname(absoluteManifest);
|
|
4569
5329
|
const recorded = new Map(
|
|
4570
|
-
previous?.outputs.map((output) => [
|
|
5330
|
+
previous?.outputs.map((output) => [path17.resolve(root, output.path), output.sha256]) ?? []
|
|
4571
5331
|
);
|
|
4572
5332
|
const conflicts = [];
|
|
4573
5333
|
for (const output of outputs) {
|
|
4574
|
-
const destination =
|
|
5334
|
+
const destination = path17.resolve(output.path);
|
|
4575
5335
|
const current = await readOptional(destination);
|
|
4576
5336
|
if (!current || digest(current) === digest(output.data)) continue;
|
|
4577
5337
|
const expected = recorded.get(destination);
|
|
@@ -4596,7 +5356,7 @@ function isCode(error, code) {
|
|
|
4596
5356
|
}
|
|
4597
5357
|
async function remove2(file, errors) {
|
|
4598
5358
|
try {
|
|
4599
|
-
await
|
|
5359
|
+
await rm6(file, { force: true });
|
|
4600
5360
|
} catch (error) {
|
|
4601
5361
|
errors?.push(`could not remove ${file}: ${message(error)}`);
|
|
4602
5362
|
}
|
|
@@ -4609,7 +5369,7 @@ async function rollback(prepared) {
|
|
|
4609
5369
|
for (const artifact of [...prepared].reverse()) {
|
|
4610
5370
|
if (!artifact.backup || !artifact.backedUp) continue;
|
|
4611
5371
|
try {
|
|
4612
|
-
await
|
|
5372
|
+
await rename5(artifact.backup, artifact.destination);
|
|
4613
5373
|
artifact.backedUp = false;
|
|
4614
5374
|
} catch (error) {
|
|
4615
5375
|
errors.push(
|
|
@@ -4626,7 +5386,7 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
4626
5386
|
if (!files.length) throw new Error("An artifact bundle must contain at least one file.");
|
|
4627
5387
|
const normalized = files.map((file) => {
|
|
4628
5388
|
if (!file.path.trim()) throw new Error("Artifact paths cannot be empty.");
|
|
4629
|
-
return { destination:
|
|
5389
|
+
return { destination: path17.resolve(file.path), data: Buffer.from(file.data) };
|
|
4630
5390
|
});
|
|
4631
5391
|
const destinations = /* @__PURE__ */ new Set();
|
|
4632
5392
|
for (const artifact of normalized) {
|
|
@@ -4642,7 +5402,7 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
4642
5402
|
const unchanged = [];
|
|
4643
5403
|
for (const artifact of normalized) {
|
|
4644
5404
|
try {
|
|
4645
|
-
const current = await
|
|
5405
|
+
const current = await readFile13(artifact.destination);
|
|
4646
5406
|
if (current.equals(artifact.data)) {
|
|
4647
5407
|
unchanged.push(artifact.destination);
|
|
4648
5408
|
} else {
|
|
@@ -4656,21 +5416,21 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
4656
5416
|
if (!changed.length) return { changed: [], unchanged };
|
|
4657
5417
|
const token = randomUUID2();
|
|
4658
5418
|
for (const [index, artifact] of changed.entries()) {
|
|
4659
|
-
const basename =
|
|
4660
|
-
artifact.stage =
|
|
4661
|
-
|
|
5419
|
+
const basename = path17.basename(artifact.destination);
|
|
5420
|
+
artifact.stage = path17.join(
|
|
5421
|
+
path17.dirname(artifact.destination),
|
|
4662
5422
|
`.${basename}.pixelkiln-stage-${token}-${index}`
|
|
4663
5423
|
);
|
|
4664
5424
|
if (artifact.existed) {
|
|
4665
|
-
artifact.backup =
|
|
4666
|
-
|
|
5425
|
+
artifact.backup = path17.join(
|
|
5426
|
+
path17.dirname(artifact.destination),
|
|
4667
5427
|
`.${basename}.pixelkiln-backup-${token}-${index}`
|
|
4668
5428
|
);
|
|
4669
5429
|
}
|
|
4670
5430
|
}
|
|
4671
|
-
const journal = options.recoveryFile ?
|
|
5431
|
+
const journal = options.recoveryFile ? path17.resolve(options.recoveryFile) : null;
|
|
4672
5432
|
if (journal) {
|
|
4673
|
-
await
|
|
5433
|
+
await mkdir6(path17.dirname(journal), { recursive: true });
|
|
4674
5434
|
const transaction = {
|
|
4675
5435
|
format: "pixelkiln-artifact-transaction",
|
|
4676
5436
|
version: 1,
|
|
@@ -4684,7 +5444,7 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
4684
5444
|
}))
|
|
4685
5445
|
};
|
|
4686
5446
|
try {
|
|
4687
|
-
await
|
|
5447
|
+
await writeFile8(journal, JSON.stringify(transaction, null, 2) + "\n", { flag: "wx" });
|
|
4688
5448
|
activeTransactions.add(journal);
|
|
4689
5449
|
} catch (error) {
|
|
4690
5450
|
throw new Error(
|
|
@@ -4695,9 +5455,9 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
4695
5455
|
}
|
|
4696
5456
|
try {
|
|
4697
5457
|
for (const [index, artifact] of changed.entries()) {
|
|
4698
|
-
await
|
|
5458
|
+
await mkdir6(path17.dirname(artifact.destination), { recursive: true });
|
|
4699
5459
|
await options.beforeStage?.(artifact.destination, index);
|
|
4700
|
-
await
|
|
5460
|
+
await writeFile8(artifact.stage, artifact.data, { flag: "wx" });
|
|
4701
5461
|
}
|
|
4702
5462
|
} catch (error) {
|
|
4703
5463
|
const cleanupErrors2 = await rollback(changed);
|
|
@@ -4710,17 +5470,17 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
4710
5470
|
try {
|
|
4711
5471
|
for (const artifact of changed) {
|
|
4712
5472
|
if (!artifact.existed) continue;
|
|
4713
|
-
await
|
|
5473
|
+
await rename5(artifact.destination, artifact.backup);
|
|
4714
5474
|
artifact.backedUp = true;
|
|
4715
5475
|
}
|
|
4716
5476
|
for (const [index, artifact] of changed.entries()) {
|
|
4717
5477
|
await options.beforePromote?.(artifact.destination, index);
|
|
4718
|
-
await
|
|
5478
|
+
await rename5(artifact.stage, artifact.destination);
|
|
4719
5479
|
artifact.stage = void 0;
|
|
4720
5480
|
artifact.promoted = true;
|
|
4721
5481
|
}
|
|
4722
5482
|
if (journal) {
|
|
4723
|
-
await
|
|
5483
|
+
await writeFile8(transactionMarker(journal), `${token}
|
|
4724
5484
|
`, { flag: "wx" });
|
|
4725
5485
|
durableCommit = true;
|
|
4726
5486
|
await options.afterCommit?.();
|
|
@@ -4765,11 +5525,11 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
4765
5525
|
// src/cli.ts
|
|
4766
5526
|
var log = (msg = "") => console.log(msg);
|
|
4767
5527
|
async function provenanceFile(id, file) {
|
|
4768
|
-
const absolute =
|
|
5528
|
+
const absolute = path18.resolve(file);
|
|
4769
5529
|
return {
|
|
4770
5530
|
id,
|
|
4771
5531
|
path: absolute,
|
|
4772
|
-
sha256:
|
|
5532
|
+
sha256: existsSync15(absolute) ? await sha256File(absolute) : null,
|
|
4773
5533
|
included: true
|
|
4774
5534
|
};
|
|
4775
5535
|
}
|
|
@@ -4793,7 +5553,10 @@ var VALUE_FLAGS = [
|
|
|
4793
5553
|
"--max-distance",
|
|
4794
5554
|
"--min-transparency",
|
|
4795
5555
|
"--max-colors",
|
|
4796
|
-
"--sigma"
|
|
5556
|
+
"--sigma",
|
|
5557
|
+
"--workspace",
|
|
5558
|
+
"--provider",
|
|
5559
|
+
"--account"
|
|
4797
5560
|
];
|
|
4798
5561
|
var BOOL_FLAGS = [
|
|
4799
5562
|
"--force",
|
|
@@ -4832,18 +5595,43 @@ var COMMANDS = [
|
|
|
4832
5595
|
"tag",
|
|
4833
5596
|
"balance",
|
|
4834
5597
|
"status",
|
|
5598
|
+
"workspace",
|
|
4835
5599
|
"help",
|
|
4836
5600
|
"--help",
|
|
4837
5601
|
"-h",
|
|
4838
5602
|
"--version",
|
|
4839
5603
|
"-v"
|
|
4840
5604
|
];
|
|
5605
|
+
var WORKSPACE_SUBCOMMANDS = ["add", "remove", "list", "status", "claims"];
|
|
4841
5606
|
function parseArgs(argv) {
|
|
4842
5607
|
const [command = "help"] = argv;
|
|
4843
5608
|
if (!COMMANDS.includes(command)) {
|
|
4844
5609
|
throw new Error(`Unknown command "${command}". Run \`pixelkiln help\` for the list.`);
|
|
4845
5610
|
}
|
|
4846
|
-
|
|
5611
|
+
let rest = argv.slice(1);
|
|
5612
|
+
let subcommand;
|
|
5613
|
+
let target;
|
|
5614
|
+
if (command === "workspace") {
|
|
5615
|
+
subcommand = rest[0];
|
|
5616
|
+
if (subcommand === void 0 || subcommand.startsWith("-")) {
|
|
5617
|
+
throw new Error(`workspace needs a subcommand: ${WORKSPACE_SUBCOMMANDS.join(", ")}`);
|
|
5618
|
+
}
|
|
5619
|
+
if (!WORKSPACE_SUBCOMMANDS.includes(subcommand)) {
|
|
5620
|
+
throw new Error(
|
|
5621
|
+
`Unknown workspace subcommand "${subcommand}". Known: ${WORKSPACE_SUBCOMMANDS.join(", ")}`
|
|
5622
|
+
);
|
|
5623
|
+
}
|
|
5624
|
+
rest = rest.slice(1);
|
|
5625
|
+
if (subcommand === "add" || subcommand === "remove") {
|
|
5626
|
+
target = rest[0];
|
|
5627
|
+
if (target === void 0 || target.startsWith("-")) {
|
|
5628
|
+
throw new Error(
|
|
5629
|
+
subcommand === "add" ? "workspace add needs a manifest path." : "workspace remove needs a project id or manifest path."
|
|
5630
|
+
);
|
|
5631
|
+
}
|
|
5632
|
+
rest = rest.slice(1);
|
|
5633
|
+
}
|
|
5634
|
+
}
|
|
4847
5635
|
for (let i = 0; i < rest.length; i++) {
|
|
4848
5636
|
const token = rest[i];
|
|
4849
5637
|
if (!token.startsWith("-")) {
|
|
@@ -4920,7 +5708,8 @@ function parseArgs(argv) {
|
|
|
4920
5708
|
return {
|
|
4921
5709
|
command,
|
|
4922
5710
|
manifest,
|
|
4923
|
-
lock: get("--lock") ??
|
|
5711
|
+
lock: get("--lock") ?? path18.join(path18.dirname(path18.resolve(manifest)), "pixelkiln.lock.json"),
|
|
5712
|
+
explicitLock: get("--lock"),
|
|
4924
5713
|
styles: list("--style"),
|
|
4925
5714
|
assets: list("--only"),
|
|
4926
5715
|
force: rest.includes("--force"),
|
|
@@ -4949,10 +5738,15 @@ function parseArgs(argv) {
|
|
|
4949
5738
|
maxDistance: numberOption("--max-distance", { min: 0 }),
|
|
4950
5739
|
minTransparency: numberOption("--min-transparency", { min: 0, max: 1 }),
|
|
4951
5740
|
maxColors: numberOption("--max-colors", { min: 1, integer: true }),
|
|
4952
|
-
sigma: numberOption("--sigma", { min: Number.EPSILON })
|
|
5741
|
+
sigma: numberOption("--sigma", { min: Number.EPSILON }),
|
|
5742
|
+
subcommand,
|
|
5743
|
+
workspace: get("--workspace"),
|
|
5744
|
+
target,
|
|
5745
|
+
provider: get("--provider"),
|
|
5746
|
+
account: get("--account")
|
|
4953
5747
|
};
|
|
4954
5748
|
}
|
|
4955
|
-
var HELP = `pixelkiln \u2014 manifest-driven pixel art generation
|
|
5749
|
+
var HELP = `pixelkiln \u2014 manifest-driven pixel art generation
|
|
4956
5750
|
|
|
4957
5751
|
pixelkiln <command> [options]
|
|
4958
5752
|
|
|
@@ -4982,6 +5776,8 @@ Commands
|
|
|
4982
5776
|
tag Push manifest tags to the objects upstream (free).
|
|
4983
5777
|
balance Show the provider's remaining balance.
|
|
4984
5778
|
status Summarise the lockfile.
|
|
5779
|
+
workspace Register sibling projects and derive account-wide claims/status.
|
|
5780
|
+
add/remove/list/status/claims. Offline.
|
|
4985
5781
|
|
|
4986
5782
|
Options
|
|
4987
5783
|
--columns <n> pack/export: sprites or tiles per row (default: near-square)
|
|
@@ -5009,6 +5805,8 @@ Options
|
|
|
5009
5805
|
--no-open Do not auto-open the browser during pick
|
|
5010
5806
|
--tag Also push tags upstream after fetch
|
|
5011
5807
|
--claims a.json,b Other projects' lockfiles (salvage; required if account is shared)
|
|
5808
|
+
--workspace <path> Workspace catalog (default: pixelkiln.workspace.json). Also
|
|
5809
|
+
derives salvage's claim set instead of repeated --claims.
|
|
5012
5810
|
--from <dir> Source tree for init
|
|
5013
5811
|
--write-prompts adopt: recover prompts into the manifest
|
|
5014
5812
|
|
|
@@ -5021,6 +5819,9 @@ Examples
|
|
|
5021
5819
|
pixelkiln pack --inputs sprites.json --out dist/sheet # no manifest needed
|
|
5022
5820
|
pixelkiln mount --style ground
|
|
5023
5821
|
pixelkiln export --style ground --only terrain --format tiled
|
|
5822
|
+
pixelkiln workspace add ../other-game/pixelkiln.manifest.json
|
|
5823
|
+
pixelkiln workspace status --json
|
|
5824
|
+
pixelkiln salvage --workspace pixelkiln.workspace.json
|
|
5024
5825
|
`;
|
|
5025
5826
|
function printPlan(plan) {
|
|
5026
5827
|
const counts = summarize(plan);
|
|
@@ -5061,6 +5862,23 @@ async function confirm(question, auto) {
|
|
|
5061
5862
|
process.stdin.pause();
|
|
5062
5863
|
return answer === "y" || answer === "yes";
|
|
5063
5864
|
}
|
|
5865
|
+
async function requireCompleteWorkspaceClaims(workspacePath) {
|
|
5866
|
+
if (!existsSync15(workspacePath)) {
|
|
5867
|
+
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
5868
|
+
}
|
|
5869
|
+
const dir = path18.dirname(path18.resolve(workspacePath));
|
|
5870
|
+
const ws = await loadWorkspace(workspacePath);
|
|
5871
|
+
const diagnostics = validateWorkspace(ws, dir);
|
|
5872
|
+
const errors = diagnostics.filter((d) => d.level === "error");
|
|
5873
|
+
if (errors.length) {
|
|
5874
|
+
throw new Error(
|
|
5875
|
+
`Workspace catalog at ${workspacePath} is not safe to derive a claim set from:
|
|
5876
|
+
` + errors.map((d) => ` ${d.id}: ${d.message}`).join("\n")
|
|
5877
|
+
);
|
|
5878
|
+
}
|
|
5879
|
+
const claims = await workspaceClaims(ws, dir);
|
|
5880
|
+
return { ws, dir, diagnostics, claims };
|
|
5881
|
+
}
|
|
5064
5882
|
async function main() {
|
|
5065
5883
|
const args = parseArgs(process.argv.slice(2));
|
|
5066
5884
|
if (args.command === "help" || args.command === "--help" || args.command === "-h") {
|
|
@@ -5069,16 +5887,17 @@ async function main() {
|
|
|
5069
5887
|
}
|
|
5070
5888
|
if (args.command === "--version" || args.command === "-v") {
|
|
5071
5889
|
const pkg = JSON.parse(
|
|
5072
|
-
await
|
|
5890
|
+
await readFile14(new URL("../package.json", import.meta.url), "utf8")
|
|
5073
5891
|
);
|
|
5074
5892
|
log(`${pkg.name} ${pkg.version}`);
|
|
5075
5893
|
return;
|
|
5076
5894
|
}
|
|
5077
5895
|
if (args.command === "balance") {
|
|
5078
|
-
loadEnvFiles(
|
|
5896
|
+
loadEnvFiles(path18.dirname(path18.resolve(args.manifest)));
|
|
5079
5897
|
loadEnvFiles(process.cwd());
|
|
5080
|
-
const
|
|
5081
|
-
const
|
|
5898
|
+
const loaded2 = await loadManifest(args.manifest);
|
|
5899
|
+
const p = createProvider(loaded2.manifest.provider, "online");
|
|
5900
|
+
const b = await requireBalance(p)();
|
|
5082
5901
|
log(` provider: ${p.id}`);
|
|
5083
5902
|
log(` plan: ${b.plan ?? "n/a"}`);
|
|
5084
5903
|
log(` remaining: ${formatCost(b.unit, b.remaining)}${b.total ? ` of ${b.total}` : ""}`);
|
|
@@ -5086,31 +5905,31 @@ async function main() {
|
|
|
5086
5905
|
}
|
|
5087
5906
|
if (args.command === "init") {
|
|
5088
5907
|
if (!args.from) throw new Error("init needs --from <dir> pointing at your existing PNGs.");
|
|
5089
|
-
const root =
|
|
5090
|
-
if (!
|
|
5908
|
+
const root = path18.resolve(args.from);
|
|
5909
|
+
if (!existsSync15(root)) throw new Error(`No directory at ${root}`);
|
|
5091
5910
|
const generator = args.generator ?? "map";
|
|
5092
5911
|
if (generator !== "1dir" && generator !== "map") {
|
|
5093
5912
|
throw new Error(`--generator must be "1dir" or "map", got "${args.generator}".`);
|
|
5094
5913
|
}
|
|
5095
|
-
const target =
|
|
5914
|
+
const target = path18.resolve(args.out ?? "pixelkiln.manifest.json");
|
|
5096
5915
|
const { assets, skipped } = await scanAssets(root, { exclude: args.exclude });
|
|
5097
5916
|
if (!assets.length) throw new Error(`No PNGs found under ${root}`);
|
|
5098
5917
|
const manifest = buildManifest(
|
|
5099
|
-
args.name ??
|
|
5918
|
+
args.name ?? path18.basename(path18.dirname(target)),
|
|
5100
5919
|
args.styles[0] ?? "base",
|
|
5101
5920
|
generator,
|
|
5102
|
-
|
|
5921
|
+
path18.relative(path18.dirname(target), root) || ".",
|
|
5103
5922
|
assets
|
|
5104
5923
|
);
|
|
5105
5924
|
await writeManifestFile(target, manifest);
|
|
5106
|
-
log(` scanned ${assets.length} PNG(s) under ${
|
|
5925
|
+
log(` scanned ${assets.length} PNG(s) under ${path18.relative(process.cwd(), root)}`);
|
|
5107
5926
|
if (skipped.length) log(` skipped ${skipped.length} unreadable file(s)`);
|
|
5108
|
-
log(` wrote ${
|
|
5927
|
+
log(` wrote ${path18.relative(process.cwd(), target)}`);
|
|
5109
5928
|
log(`
|
|
5110
5929
|
Prompts are intentionally empty. To recover the real ones from your`);
|
|
5111
5930
|
log(` PixelLab account instead of inventing them:`);
|
|
5112
5931
|
log(`
|
|
5113
|
-
pixelkiln adopt --manifest ${
|
|
5932
|
+
pixelkiln adopt --manifest ${path18.relative(process.cwd(), target)} --write-prompts
|
|
5114
5933
|
`);
|
|
5115
5934
|
return;
|
|
5116
5935
|
}
|
|
@@ -5158,10 +5977,10 @@ async function main() {
|
|
|
5158
5977
|
if (args.primaryOnly || args.outputRoles.length) {
|
|
5159
5978
|
throw new Error("--primary-only and --output-role require manifest-driven pack");
|
|
5160
5979
|
}
|
|
5161
|
-
const raw = JSON.parse(await
|
|
5980
|
+
const raw = JSON.parse(await readFile14(path18.resolve(args.inputs), "utf8"));
|
|
5162
5981
|
const inputs = resolvePackInputs(raw, args.inputs);
|
|
5163
5982
|
const { png, atlas, skipped, sources } = packSprites(inputs, { columns: args.columns });
|
|
5164
|
-
const base =
|
|
5983
|
+
const base = path18.resolve(args.out.replace(/\.png$/, ""));
|
|
5165
5984
|
const outputs = [
|
|
5166
5985
|
{ path: `${base}.png`, data: png },
|
|
5167
5986
|
{ path: `${base}.json`, data: JSON.stringify(atlas, null, 2) + "\n" }
|
|
@@ -5174,20 +5993,145 @@ async function main() {
|
|
|
5174
5993
|
log(
|
|
5175
5994
|
` ${atlas.frames.length} sprite(s), ${atlas.sheet.width}x${atlas.sheet.height} in ${atlas.columns} column(s) \u2014 ${(png.length / 1024).toFixed(1)} KB`
|
|
5176
5995
|
);
|
|
5177
|
-
log(` ${
|
|
5996
|
+
log(` ${path18.relative(process.cwd(), base)}.png + .json + .pixelkiln.json`);
|
|
5178
5997
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
5179
5998
|
return;
|
|
5180
5999
|
}
|
|
5181
|
-
if (
|
|
6000
|
+
if (args.command === "workspace") {
|
|
6001
|
+
const workspacePath = path18.resolve(args.workspace ?? "pixelkiln.workspace.json");
|
|
6002
|
+
const dir = path18.dirname(workspacePath);
|
|
6003
|
+
if (args.subcommand === "add") {
|
|
6004
|
+
const manifestPath = path18.resolve(args.target);
|
|
6005
|
+
const loadedTarget = await loadManifest(manifestPath);
|
|
6006
|
+
const lockPath = args.explicitLock ? path18.resolve(args.explicitLock) : path18.join(path18.dirname(manifestPath), "pixelkiln.lock.json");
|
|
6007
|
+
const ws = await loadWorkspace(workspacePath);
|
|
6008
|
+
const id = args.name ?? loadedTarget.manifest.name;
|
|
6009
|
+
if (ws.projects.some((p) => p.id === id)) {
|
|
6010
|
+
throw new Error(
|
|
6011
|
+
`Project id "${id}" is already registered in ${workspacePath}. Pass --name for a different id.`
|
|
6012
|
+
);
|
|
6013
|
+
}
|
|
6014
|
+
const lockOwner = ws.projects.find((p) => resolveProject(dir, p).lockPath === lockPath);
|
|
6015
|
+
if (lockOwner) {
|
|
6016
|
+
throw new Error(`Lockfile ${lockPath} is already registered under project id "${lockOwner.id}".`);
|
|
6017
|
+
}
|
|
6018
|
+
const project = {
|
|
6019
|
+
id,
|
|
6020
|
+
manifest: toPortablePath(dir, manifestPath),
|
|
6021
|
+
lock: toPortablePath(dir, lockPath),
|
|
6022
|
+
provider: args.provider ?? loadedTarget.manifest.provider,
|
|
6023
|
+
...args.account ? { account: args.account } : {}
|
|
6024
|
+
};
|
|
6025
|
+
await saveWorkspace(workspacePath, { version: 1, projects: [...ws.projects, project] });
|
|
6026
|
+
log(` registered "${id}" in ${path18.relative(process.cwd(), workspacePath)}`);
|
|
6027
|
+
log(` manifest: ${project.manifest}`);
|
|
6028
|
+
log(` lock: ${project.lock}`);
|
|
6029
|
+
if (!existsSync15(lockPath)) {
|
|
6030
|
+
log(
|
|
6031
|
+
` warning: no lockfile there yet \u2014 this project contributes no claims until one is generated`
|
|
6032
|
+
);
|
|
6033
|
+
}
|
|
6034
|
+
return;
|
|
6035
|
+
}
|
|
6036
|
+
if (args.subcommand === "remove") {
|
|
6037
|
+
const ws = await loadWorkspace(workspacePath);
|
|
6038
|
+
const resolvedTarget = path18.resolve(args.target);
|
|
6039
|
+
const match = ws.projects.find(
|
|
6040
|
+
(p) => p.id === args.target || resolveProject(dir, p).manifestPath === resolvedTarget
|
|
6041
|
+
);
|
|
6042
|
+
if (!match) {
|
|
6043
|
+
throw new Error(`No registered project matches "${args.target}" (checked id and manifest path).`);
|
|
6044
|
+
}
|
|
6045
|
+
await saveWorkspace(workspacePath, {
|
|
6046
|
+
version: 1,
|
|
6047
|
+
projects: ws.projects.filter((p) => p !== match)
|
|
6048
|
+
});
|
|
6049
|
+
log(` removed "${match.id}" from ${path18.relative(process.cwd(), workspacePath)}`);
|
|
6050
|
+
return;
|
|
6051
|
+
}
|
|
6052
|
+
if (args.subcommand === "list") {
|
|
6053
|
+
if (!existsSync15(workspacePath)) {
|
|
6054
|
+
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
6055
|
+
}
|
|
6056
|
+
const ws = await loadWorkspace(workspacePath);
|
|
6057
|
+
const diagnostics = validateWorkspace(ws, dir);
|
|
6058
|
+
if (args.json) {
|
|
6059
|
+
log(JSON.stringify({ version: 1, workspace: workspacePath, projects: ws.projects, diagnostics }, null, 2));
|
|
6060
|
+
} else if (!ws.projects.length) {
|
|
6061
|
+
log(` no projects registered in ${path18.relative(process.cwd(), workspacePath)}`);
|
|
6062
|
+
} else {
|
|
6063
|
+
log(` ${ws.projects.length} project(s) in ${path18.relative(process.cwd(), workspacePath)}:`);
|
|
6064
|
+
for (const p of ws.projects) {
|
|
6065
|
+
log(` ${p.id.padEnd(24)} ${p.manifest.padEnd(40)} (${p.provider}${p.account ? `, ${p.account}` : ""})`);
|
|
6066
|
+
}
|
|
6067
|
+
for (const d of diagnostics) log(` ${d.level === "error" ? "ERROR" : "WARN "} ${d.id.padEnd(18)} ${d.message}`);
|
|
6068
|
+
}
|
|
6069
|
+
if (args.check && diagnostics.some((d) => d.level === "error")) process.exitCode = 1;
|
|
6070
|
+
return;
|
|
6071
|
+
}
|
|
6072
|
+
if (args.subcommand === "status") {
|
|
6073
|
+
if (!existsSync15(workspacePath)) {
|
|
6074
|
+
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
6075
|
+
}
|
|
6076
|
+
const ws = await loadWorkspace(workspacePath);
|
|
6077
|
+
const report = await workspaceStatus(ws, dir);
|
|
6078
|
+
if (args.json) {
|
|
6079
|
+
log(JSON.stringify({ ...report, workspace: workspacePath }, null, 2));
|
|
6080
|
+
} else {
|
|
6081
|
+
log(` workspace: ${path18.relative(process.cwd(), workspacePath)}`);
|
|
6082
|
+
for (const p of report.projects) {
|
|
6083
|
+
if (p.error) {
|
|
6084
|
+
log(`
|
|
6085
|
+
${p.id} \u2014 ERROR: ${p.error}`);
|
|
6086
|
+
continue;
|
|
6087
|
+
}
|
|
6088
|
+
log(`
|
|
6089
|
+
${p.id} (${p.provider}${p.account ? `, ${p.account}` : ""})`);
|
|
6090
|
+
log(` ${p.entries} lock entries`);
|
|
6091
|
+
for (const [state, n] of Object.entries(p.byState)) if (n) log(` ${state.padEnd(12)} ${n}`);
|
|
6092
|
+
for (const [unit, amount] of Object.entries(p.spendByUnit).sort()) {
|
|
6093
|
+
if (amount) log(` spend: ${formatCost(unit, amount)}`);
|
|
6094
|
+
}
|
|
6095
|
+
}
|
|
6096
|
+
log(`
|
|
6097
|
+
totals:`);
|
|
6098
|
+
for (const [state, n] of Object.entries(report.totals.byState)) if (n) log(` ${state.padEnd(12)} ${n}`);
|
|
6099
|
+
for (const [unit, amount] of Object.entries(report.totals.spendByUnit).sort()) {
|
|
6100
|
+
if (amount) log(` spend: ${formatCost(unit, amount)}`);
|
|
6101
|
+
}
|
|
6102
|
+
log(` claims: ${report.totals.claims}`);
|
|
6103
|
+
for (const d of report.diagnostics) log(` ${d.level === "error" ? "ERROR" : "WARN "} ${d.id.padEnd(18)} ${d.message}`);
|
|
6104
|
+
}
|
|
6105
|
+
if (args.check && !report.safe) process.exitCode = 1;
|
|
6106
|
+
return;
|
|
6107
|
+
}
|
|
6108
|
+
if (args.subcommand === "claims") {
|
|
6109
|
+
const { claims, diagnostics } = await requireCompleteWorkspaceClaims(workspacePath);
|
|
6110
|
+
if (args.json) {
|
|
6111
|
+
log(JSON.stringify({
|
|
6112
|
+
version: 1,
|
|
6113
|
+
claimed: [...claims.claimed].sort(),
|
|
6114
|
+
byProject: claims.byProject,
|
|
6115
|
+
lockPaths: claims.lockPaths
|
|
6116
|
+
}, null, 2));
|
|
6117
|
+
} else {
|
|
6118
|
+
log(` ${claims.claimed.size} claimed id(s) across ${claims.lockPaths.length} lockfile(s):`);
|
|
6119
|
+
for (const [id, n] of Object.entries(claims.byProject).sort()) log(` ${id.padEnd(24)} ${n}`);
|
|
6120
|
+
for (const d of diagnostics) log(` WARN ${d.id.padEnd(18)} ${d.message}`);
|
|
6121
|
+
}
|
|
6122
|
+
return;
|
|
6123
|
+
}
|
|
6124
|
+
}
|
|
6125
|
+
if (!existsSync15(path18.resolve(args.manifest))) {
|
|
5182
6126
|
throw new Error(
|
|
5183
|
-
`No manifest at ${
|
|
6127
|
+
`No manifest at ${path18.resolve(args.manifest)}. Pass --manifest, or run \`pixelkiln init --from <dir>\`.`
|
|
5184
6128
|
);
|
|
5185
6129
|
}
|
|
5186
|
-
const manifestDir =
|
|
6130
|
+
const manifestDir = path18.dirname(path18.resolve(args.manifest));
|
|
5187
6131
|
const envFiles = [...loadEnvFiles(manifestDir)];
|
|
5188
|
-
if (
|
|
6132
|
+
if (path18.resolve(process.cwd()) !== manifestDir) envFiles.push(...loadEnvFiles(process.cwd()));
|
|
5189
6133
|
const loaded = await loadManifest(args.manifest);
|
|
5190
|
-
const estimator =
|
|
6134
|
+
const estimator = createProvider(loaded.manifest.provider, "offline");
|
|
5191
6135
|
const specs = await resolveSpecs(loaded, {
|
|
5192
6136
|
styles: args.styles,
|
|
5193
6137
|
assets: args.assets,
|
|
@@ -5206,9 +6150,9 @@ async function main() {
|
|
|
5206
6150
|
for (const [s, n] of Object.entries(byStatus).sort()) log(` ${s.padEnd(12)} ${n}`);
|
|
5207
6151
|
const spend = spendByUnit(lock);
|
|
5208
6152
|
let reported = false;
|
|
5209
|
-
for (const unit of
|
|
5210
|
-
if (
|
|
5211
|
-
log(` recorded successful submissions: ${formatCost(unit,
|
|
6153
|
+
for (const [unit, amount] of Object.entries(spend).sort()) {
|
|
6154
|
+
if (amount) {
|
|
6155
|
+
log(` recorded successful submissions: ${formatCost(unit, amount)}`);
|
|
5212
6156
|
reported = true;
|
|
5213
6157
|
}
|
|
5214
6158
|
}
|
|
@@ -5217,12 +6161,14 @@ async function main() {
|
|
|
5217
6161
|
}
|
|
5218
6162
|
const plan = await buildPlan(specs, lock, { force: args.force });
|
|
5219
6163
|
if (args.command === "doctor") {
|
|
5220
|
-
const
|
|
5221
|
-
const
|
|
6164
|
+
const factory = providerFactory(loaded.manifest.provider);
|
|
6165
|
+
const apiKeyPresent = !factory.credentialEnv || Boolean(process.env[factory.credentialEnv]);
|
|
6166
|
+
const provider2 = !args.dryRun && apiKeyPresent ? createProvider(loaded.manifest.provider, "online") : void 0;
|
|
5222
6167
|
const report = await doctor(loaded, specs, lock, args.lock, {
|
|
5223
6168
|
provider: provider2,
|
|
5224
6169
|
offline: args.dryRun,
|
|
5225
|
-
apiKeyPresent
|
|
6170
|
+
apiKeyPresent,
|
|
6171
|
+
credentialEnv: factory.credentialEnv
|
|
5226
6172
|
});
|
|
5227
6173
|
if (args.json) {
|
|
5228
6174
|
log(JSON.stringify(report, null, 2));
|
|
@@ -5260,7 +6206,7 @@ async function main() {
|
|
|
5260
6206
|
let intact = true;
|
|
5261
6207
|
for (const output of entry.outputs) {
|
|
5262
6208
|
const file = resolveOutputPath(output.path, item.spec.root);
|
|
5263
|
-
if (!
|
|
6209
|
+
if (!existsSync15(file) || await sha256File(file) !== output.sha256) {
|
|
5264
6210
|
intact = false;
|
|
5265
6211
|
break;
|
|
5266
6212
|
}
|
|
@@ -5318,7 +6264,7 @@ async function main() {
|
|
|
5318
6264
|
if (args.primaryOnly && args.outputRoles.length) {
|
|
5319
6265
|
throw new Error("pack accepts either --primary-only or --output-role, not both");
|
|
5320
6266
|
}
|
|
5321
|
-
const manifestDir2 =
|
|
6267
|
+
const manifestDir2 = path18.dirname(path18.resolve(args.manifest));
|
|
5322
6268
|
const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
|
|
5323
6269
|
for (const styleId of styleIds) {
|
|
5324
6270
|
const { png, atlas, skipped, sources } = packStyle(lock, styleId, manifestDir2, {
|
|
@@ -5327,7 +6273,7 @@ async function main() {
|
|
|
5327
6273
|
primaryOnly: args.primaryOnly
|
|
5328
6274
|
});
|
|
5329
6275
|
const style = loaded.manifest.styles[styleId];
|
|
5330
|
-
const base = args.out ?
|
|
6276
|
+
const base = args.out ? path18.resolve(args.out.replace(/\.png$/, "")) : path18.resolve(manifestDir2, style.outDir, `${styleId}-sheet`);
|
|
5331
6277
|
const outputs = [
|
|
5332
6278
|
{ path: `${base}.png`, data: png },
|
|
5333
6279
|
{ path: `${base}.json`, data: JSON.stringify(atlas, null, 2) + "\n" }
|
|
@@ -5350,13 +6296,13 @@ async function main() {
|
|
|
5350
6296
|
log(
|
|
5351
6297
|
` ${styleId} \u2014 ${atlas.frames.length} sprite(s), ${atlas.sheet.width}x${atlas.sheet.height} in ${atlas.columns} column(s)`
|
|
5352
6298
|
);
|
|
5353
|
-
log(` ${
|
|
6299
|
+
log(` ${path18.relative(process.cwd(), base)}.png + .json + .pixelkiln.json`);
|
|
5354
6300
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
5355
6301
|
}
|
|
5356
6302
|
return;
|
|
5357
6303
|
}
|
|
5358
6304
|
if (args.command === "mount") {
|
|
5359
|
-
const manifestDir2 =
|
|
6305
|
+
const manifestDir2 = path18.dirname(path18.resolve(args.manifest));
|
|
5360
6306
|
const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
|
|
5361
6307
|
for (const styleId of styleIds) {
|
|
5362
6308
|
const style = loaded.manifest.styles[styleId];
|
|
@@ -5386,7 +6332,7 @@ async function main() {
|
|
|
5386
6332
|
sources,
|
|
5387
6333
|
outputRoles
|
|
5388
6334
|
);
|
|
5389
|
-
const out =
|
|
6335
|
+
const out = path18.resolve(manifestDir2, style.mount.out);
|
|
5390
6336
|
const metadata = out.replace(/\.png$/, "") + ".json";
|
|
5391
6337
|
const companion = out.replace(/\.png$/, "") + ".pixelkiln.json";
|
|
5392
6338
|
const outputs = [
|
|
@@ -5399,7 +6345,7 @@ async function main() {
|
|
|
5399
6345
|
await provenanceFile("$manifest", args.manifest),
|
|
5400
6346
|
await provenanceFile("$lock", args.lock),
|
|
5401
6347
|
...artifactSources.filter(
|
|
5402
|
-
(source) => source.id !== "$base" ||
|
|
6348
|
+
(source) => source.id !== "$base" || path18.resolve(source.path) !== out
|
|
5403
6349
|
)
|
|
5404
6350
|
],
|
|
5405
6351
|
options: {
|
|
@@ -5412,7 +6358,7 @@ async function main() {
|
|
|
5412
6358
|
log(
|
|
5413
6359
|
` ${styleId} \u2014 ${atlas.frames.length} cell(s) into ${atlas.sheet.width}x${atlas.sheet.height}` + (overBase ? ` over ${style.mount.base}` : " (new sheet)")
|
|
5414
6360
|
);
|
|
5415
|
-
log(` ${
|
|
6361
|
+
log(` ${path18.relative(process.cwd(), out)} + atlas/provenance JSON`);
|
|
5416
6362
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
5417
6363
|
}
|
|
5418
6364
|
return;
|
|
@@ -5468,7 +6414,7 @@ async function main() {
|
|
|
5468
6414
|
}
|
|
5469
6415
|
if (args.command === "export") {
|
|
5470
6416
|
const format = args.format ?? "generic";
|
|
5471
|
-
const manifestDir2 =
|
|
6417
|
+
const manifestDir2 = path18.dirname(path18.resolve(args.manifest));
|
|
5472
6418
|
const selected = specs.filter((spec) => {
|
|
5473
6419
|
if (spec.generator !== "tiles") return false;
|
|
5474
6420
|
if (args.styles.length && !args.styles.includes(spec.styleId)) return false;
|
|
@@ -5484,12 +6430,12 @@ async function main() {
|
|
|
5484
6430
|
for (const spec of selected) {
|
|
5485
6431
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
5486
6432
|
const style = loaded.manifest.styles[spec.styleId];
|
|
5487
|
-
const defaultBase =
|
|
5488
|
-
const base = args.out ?
|
|
6433
|
+
const defaultBase = path18.resolve(manifestDir2, style.outDir, `${spec.assetId}-tileset`);
|
|
6434
|
+
const base = args.out ? path18.resolve(args.out.replace(/\.(?:png|json|tsj|tres)$/i, "")) : defaultBase;
|
|
5489
6435
|
const result = exportTileset(entry, spec, {
|
|
5490
6436
|
format,
|
|
5491
6437
|
manifestDir: manifestDir2,
|
|
5492
|
-
imageName:
|
|
6438
|
+
imageName: path18.basename(`${base}.png`),
|
|
5493
6439
|
columns: args.columns
|
|
5494
6440
|
});
|
|
5495
6441
|
const outputs = [
|
|
@@ -5507,7 +6453,7 @@ async function main() {
|
|
|
5507
6453
|
asset: spec.assetId,
|
|
5508
6454
|
columns: args.columns ?? null,
|
|
5509
6455
|
format,
|
|
5510
|
-
image:
|
|
6456
|
+
image: path18.basename(`${base}.png`),
|
|
5511
6457
|
providerRules: result.generic.providerRules,
|
|
5512
6458
|
style: spec.styleId,
|
|
5513
6459
|
tileType: spec.tileType ?? null
|
|
@@ -5517,12 +6463,12 @@ async function main() {
|
|
|
5517
6463
|
` ${spec.styleId}/${spec.assetId} \u2014 ${result.generic.tiles.length} tile(s), ${result.generic.sheet.width}x${result.generic.sheet.height} (${format})`
|
|
5518
6464
|
);
|
|
5519
6465
|
log(
|
|
5520
|
-
` ${
|
|
6466
|
+
` ${path18.relative(process.cwd(), base)}.png + ${path18.basename(base)}${result.extension} + .pixelkiln.json`
|
|
5521
6467
|
);
|
|
5522
6468
|
}
|
|
5523
6469
|
return;
|
|
5524
6470
|
}
|
|
5525
|
-
const provider = args.command === "restore" || args.command === "fetch" && !args.tag ?
|
|
6471
|
+
const provider = args.command === "restore" || args.command === "fetch" && !args.tag ? createProvider(loaded.manifest.provider, "downloads") : createProvider(loaded.manifest.provider, "online");
|
|
5526
6472
|
if (args.command === "adopt") {
|
|
5527
6473
|
log(`
|
|
5528
6474
|
Reconciling account objects against files already on disk\u2026`);
|
|
@@ -5557,10 +6503,10 @@ async function main() {
|
|
|
5557
6503
|
tagged ${n} object(s) upstream`);
|
|
5558
6504
|
}
|
|
5559
6505
|
if (args.writePrompts) {
|
|
5560
|
-
const { filled, stillEmpty } = await writePromptsBack(
|
|
6506
|
+
const { filled, stillEmpty } = await writePromptsBack(path18.resolve(args.manifest), lock, {
|
|
5561
6507
|
onProgress: log
|
|
5562
6508
|
});
|
|
5563
|
-
log(` recovered ${filled} prompt(s) into ${
|
|
6509
|
+
log(` recovered ${filled} prompt(s) into ${path18.relative(process.cwd(), args.manifest)}`);
|
|
5564
6510
|
const reloaded = await loadManifest(args.manifest);
|
|
5565
6511
|
const rebased = await resolveSpecs(reloaded, {
|
|
5566
6512
|
styles: args.styles,
|
|
@@ -5589,19 +6535,39 @@ async function main() {
|
|
|
5589
6535
|
if (args.command === "salvage") {
|
|
5590
6536
|
const jsonMode = args.dryRun && args.json;
|
|
5591
6537
|
const diag = jsonMode ? (msg = "") => console.error(msg) : log;
|
|
5592
|
-
const ownLock =
|
|
6538
|
+
const ownLock = path18.resolve(args.lock);
|
|
6539
|
+
let workspaceProjects = [];
|
|
6540
|
+
let workspaceDir = "";
|
|
6541
|
+
if (args.workspace) {
|
|
6542
|
+
const workspacePath = path18.resolve(args.workspace);
|
|
6543
|
+
workspaceDir = path18.dirname(workspacePath);
|
|
6544
|
+
const complete = await requireCompleteWorkspaceClaims(workspacePath);
|
|
6545
|
+
workspaceProjects = complete.ws.projects;
|
|
6546
|
+
for (const d of complete.diagnostics) diag(` WARN ${d.id}: ${d.message}`);
|
|
6547
|
+
}
|
|
6548
|
+
const workspaceLockPaths = workspaceProjects.map((p) => resolveProject(workspaceDir, p).lockPath);
|
|
5593
6549
|
const lockPaths = [
|
|
5594
|
-
|
|
5595
|
-
|
|
6550
|
+
.../* @__PURE__ */ new Set([
|
|
6551
|
+
...workspaceLockPaths,
|
|
6552
|
+
...existsSync15(ownLock) ? [ownLock] : [],
|
|
6553
|
+
...args.claims.map((c) => path18.resolve(c))
|
|
6554
|
+
])
|
|
5596
6555
|
];
|
|
5597
6556
|
diag(` claim set (${lockPaths.length} lockfile(s)):`);
|
|
5598
|
-
for (const p of lockPaths) diag(` ${
|
|
5599
|
-
if (!args.claims.length) {
|
|
6557
|
+
for (const p of lockPaths) diag(` ${path18.relative(process.cwd(), p)}`);
|
|
6558
|
+
if (!args.claims.length && !args.workspace) {
|
|
5600
6559
|
diag(
|
|
5601
6560
|
`
|
|
5602
6561
|
Only this project's lockfile was consulted. If the account is shared,
|
|
5603
|
-
pass every other project's lockfile via --claims a.json,b.json or
|
|
5604
|
-
|
|
6562
|
+
pass every other project's lockfile via --claims a.json,b.json, or
|
|
6563
|
+
register every project in a workspace catalog and pass --workspace.`
|
|
6564
|
+
);
|
|
6565
|
+
} else if (args.workspace && !workspaceProjects.some((p) => resolveProject(workspaceDir, p).manifestPath === path18.resolve(args.manifest))) {
|
|
6566
|
+
diag(
|
|
6567
|
+
`
|
|
6568
|
+
This project's manifest is not registered in the workspace catalog. Its own
|
|
6569
|
+
lockfile is still included above, so this run's claim set is complete \u2014 but
|
|
6570
|
+
\`pixelkiln workspace add ${args.manifest}\` would keep it aggregated too.`
|
|
5605
6571
|
);
|
|
5606
6572
|
}
|
|
5607
6573
|
const claimed = await loadClaims(lockPaths);
|
|
@@ -5613,16 +6579,11 @@ async function main() {
|
|
|
5613
6579
|
nothing to triage`);
|
|
5614
6580
|
return;
|
|
5615
6581
|
}
|
|
5616
|
-
const siblings =
|
|
5617
|
-
|
|
5618
|
-
|
|
5619
|
-
|
|
5620
|
-
|
|
5621
|
-
const siblingLoaded = await loadManifest(siblingManifestPath);
|
|
5622
|
-
siblings.push({ label: path16.basename(path16.dirname(siblingManifestPath)), manifest: siblingLoaded.manifest });
|
|
5623
|
-
} catch {
|
|
5624
|
-
}
|
|
5625
|
-
}
|
|
6582
|
+
const siblings = await loadSiblingManifests(
|
|
6583
|
+
args.manifest,
|
|
6584
|
+
workspaceProjects.map((p) => resolveProject(workspaceDir, p).manifestPath),
|
|
6585
|
+
args.claims
|
|
6586
|
+
);
|
|
5626
6587
|
const { matched, unmatched, elsewhere } = groupOrphansByStyle(orphans, loaded.manifest, siblings);
|
|
5627
6588
|
const multiStyle = Object.keys(loaded.manifest.styles).length > 1;
|
|
5628
6589
|
if (multiStyle) {
|
|
@@ -5685,7 +6646,7 @@ async function main() {
|
|
|
5685
6646
|
manifestPath: loaded.path,
|
|
5686
6647
|
manifest: loaded.manifest,
|
|
5687
6648
|
styleId,
|
|
5688
|
-
importDir:
|
|
6649
|
+
importDir: path18.resolve(loaded.root, style.outDir),
|
|
5689
6650
|
lock,
|
|
5690
6651
|
lockPath: args.lock
|
|
5691
6652
|
},
|
|
@@ -5750,7 +6711,7 @@ async function main() {
|
|
|
5750
6711
|
return;
|
|
5751
6712
|
}
|
|
5752
6713
|
log(`
|
|
5753
|
-
This permanently deletes them from your
|
|
6714
|
+
This permanently deletes them from your ${provider.id} account.`);
|
|
5754
6715
|
log(` Any local files already downloaded are untouched, but the objects`);
|
|
5755
6716
|
log(` and their URLs are gone and cannot be re-downloaded.`);
|
|
5756
6717
|
if (!await confirm(` Delete ${doomed.length} object(s)?`, args.yes)) {
|
|
@@ -5785,21 +6746,26 @@ async function main() {
|
|
|
5785
6746
|
return;
|
|
5786
6747
|
}
|
|
5787
6748
|
if (plan.actionable.length) {
|
|
5788
|
-
const balance = await provider.balance();
|
|
5789
|
-
|
|
6749
|
+
const balance = provider.balance ? await provider.balance() : null;
|
|
6750
|
+
if (balance) {
|
|
6751
|
+
log(`
|
|
5790
6752
|
balance: ${formatCost(balance.unit, balance.remaining)} remaining (${provider.id})`);
|
|
5791
|
-
|
|
5792
|
-
|
|
5793
|
-
|
|
5794
|
-
|
|
5795
|
-
|
|
5796
|
-
|
|
5797
|
-
|
|
5798
|
-
|
|
5799
|
-
|
|
6753
|
+
if (balance.unit !== plan.costUnit) {
|
|
6754
|
+
throw new Error(
|
|
6755
|
+
`Provider estimate unit ${plan.costUnit} does not match balance unit ${balance.unit}.`
|
|
6756
|
+
);
|
|
6757
|
+
}
|
|
6758
|
+
if (balance.unit !== "free" && plan.cost > balance.remaining) {
|
|
6759
|
+
throw new Error(
|
|
6760
|
+
`This run needs ${formatCost(balance.unit, plan.cost)} but only ${formatCost(balance.unit, balance.remaining)} remain.`
|
|
6761
|
+
);
|
|
6762
|
+
}
|
|
6763
|
+
} else {
|
|
6764
|
+
log(`
|
|
6765
|
+
${provider.id} does not expose an account balance; enforcing the explicit run budget`);
|
|
5800
6766
|
}
|
|
5801
6767
|
const ok = await confirm(
|
|
5802
|
-
` Spend ${formatCost(
|
|
6768
|
+
` Spend ${formatCost(plan.costUnit, plan.cost)} on ${plan.actionable.length} asset(s)?`,
|
|
5803
6769
|
args.yes
|
|
5804
6770
|
);
|
|
5805
6771
|
if (!ok) {
|
|
@@ -5817,7 +6783,8 @@ async function main() {
|
|
|
5817
6783
|
submitted ${res.submitted}, failed ${res.failed}, estimated ${formatCost(res.unit, res.spent)}`
|
|
5818
6784
|
);
|
|
5819
6785
|
try {
|
|
5820
|
-
const after = await provider.balance();
|
|
6786
|
+
const after = balance && provider.balance ? await provider.balance() : null;
|
|
6787
|
+
if (!balance || !after) throw new Error("balance reporting is unsupported");
|
|
5821
6788
|
const measured = measureBalanceChange(balance, after);
|
|
5822
6789
|
if (measured) {
|
|
5823
6790
|
const movement = measured.credited ? `${formatCost(measured.unit, measured.credited)} credited` : `${formatCost(measured.unit, measured.spent)} consumed`;
|