pixelkiln 0.11.0 → 0.12.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 +1 -1
- package/PROVIDERS.md +22 -24
- package/README.md +37 -36
- package/SECURITY.md +2 -3
- package/dist/cli.d.ts +5 -3
- package/dist/cli.js +1061 -149
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +997 -228
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1282 -1
- package/dist/index.d.ts +1282 -1
- package/dist/index.js +932 -181
- package/dist/index.js.map +1 -1
- package/docs/AGENTS.md +7 -5
- package/docs/CLI.md +69 -5
- package/docs/COMFYUI.md +38 -18
- package/docs/GETTING_STARTED.md +13 -6
- package/docs/LIBRARY.md +46 -0
- package/docs/PROVIDER_BENCHMARK.md +22 -18
- package/docs/QUALITY.md +59 -11
- package/docs/README.md +5 -5
- package/docs/RECIPES.md +113 -0
- package/package.json +3 -1
- package/recipes/comfyui/pixel-art-xl-environment/1.0.0/pixelkiln.recipe.json +82 -0
- package/recipes/comfyui/pixel-art-xl-environment/1.0.0/workflow-api.json +79 -0
- package/schema/quality-baseline.schema.json +189 -0
- package/schema/recipe.schema.json +528 -0
- package/skills/pixelkiln/SKILL.md +8 -2
- package/skills/pixelkiln/references/comfyui.md +18 -7
- package/skills/pixelkiln/references/mixed-providers.md +1 -1
- package/skills/pixelkiln/references/recipes.md +26 -0
- package/skills/pixelkiln/references/retro-diffusion.md +2 -2
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 path22 from "path";
|
|
5
|
+
import { existsSync as existsSync17 } from "fs";
|
|
6
|
+
import { readFile as readFile17 } from "fs/promises";
|
|
7
7
|
|
|
8
8
|
// src/env.ts
|
|
9
9
|
import { readFileSync, existsSync } from "fs";
|
|
@@ -105,17 +105,23 @@ function formatCost(unit, amount) {
|
|
|
105
105
|
|
|
106
106
|
// src/providers/comfyui.ts
|
|
107
107
|
import { randomUUID } from "crypto";
|
|
108
|
-
import { readFile
|
|
108
|
+
import { readFile } from "fs/promises";
|
|
109
109
|
import path2 from "path";
|
|
110
110
|
|
|
111
111
|
// src/hash.ts
|
|
112
112
|
import { createHash } from "crypto";
|
|
113
|
-
import {
|
|
113
|
+
import { createReadStream } from "fs";
|
|
114
114
|
function sha256(data) {
|
|
115
115
|
return createHash("sha256").update(data).digest("hex");
|
|
116
116
|
}
|
|
117
|
-
async function sha256File(
|
|
118
|
-
return
|
|
117
|
+
async function sha256File(path23) {
|
|
118
|
+
return new Promise((resolve, reject) => {
|
|
119
|
+
const hash = createHash("sha256");
|
|
120
|
+
const stream = createReadStream(path23);
|
|
121
|
+
stream.on("data", (chunk2) => hash.update(chunk2));
|
|
122
|
+
stream.on("error", reject);
|
|
123
|
+
stream.on("end", () => resolve(hash.digest("hex")));
|
|
124
|
+
});
|
|
119
125
|
}
|
|
120
126
|
function specHash(spec, styleImageHashes, providerOptionIdentity = spec.providerOptions) {
|
|
121
127
|
return sha256(
|
|
@@ -683,7 +689,7 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
683
689
|
const workflowPath = path2.resolve(context.root, options.workflowFile);
|
|
684
690
|
let raw;
|
|
685
691
|
try {
|
|
686
|
-
raw = await
|
|
692
|
+
raw = await readFile(workflowPath, "utf8");
|
|
687
693
|
} catch (err) {
|
|
688
694
|
throw new Error(
|
|
689
695
|
`ComfyUI workflow for style "${context.styleId}" could not be read at ${workflowPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -974,9 +980,9 @@ function normalizeBaseUrl(value) {
|
|
|
974
980
|
}
|
|
975
981
|
function historyError(status) {
|
|
976
982
|
const messages = Array.isArray(status.messages) ? status.messages : [];
|
|
977
|
-
for (const
|
|
978
|
-
if (!Array.isArray(
|
|
979
|
-
const detail =
|
|
983
|
+
for (const message5 of messages) {
|
|
984
|
+
if (!Array.isArray(message5) || message5[0] !== "execution_error" || !isObject(message5[1])) continue;
|
|
985
|
+
const detail = message5[1].exception_message;
|
|
980
986
|
if (typeof detail === "string" && detail.trim()) {
|
|
981
987
|
return `ComfyUI execution failed: ${detail.trim().slice(0, 300)}`;
|
|
982
988
|
}
|
|
@@ -1097,8 +1103,8 @@ function validateResponse(schema, raw, operation) {
|
|
|
1097
1103
|
throw new Error(`Invalid PixelLab response for ${operation}: ${issues}`);
|
|
1098
1104
|
}
|
|
1099
1105
|
var PixelLabError = class extends Error {
|
|
1100
|
-
constructor(
|
|
1101
|
-
super(
|
|
1106
|
+
constructor(message5, status, body) {
|
|
1107
|
+
super(message5);
|
|
1102
1108
|
this.status = status;
|
|
1103
1109
|
this.body = body;
|
|
1104
1110
|
this.name = "PixelLabError";
|
|
@@ -1123,11 +1129,11 @@ var PixelLabClient = class {
|
|
|
1123
1129
|
* common than the failure mode of retrying (a duplicate object), and a
|
|
1124
1130
|
* duplicate is visible and free to delete whereas a silent gap is neither.
|
|
1125
1131
|
*/
|
|
1126
|
-
async request(
|
|
1132
|
+
async request(path23, init, attempt = 0) {
|
|
1127
1133
|
const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
|
|
1128
1134
|
let res;
|
|
1129
1135
|
try {
|
|
1130
|
-
res = await fetch(`${BASE}${
|
|
1136
|
+
res = await fetch(`${BASE}${path23}`, {
|
|
1131
1137
|
...init,
|
|
1132
1138
|
signal: init?.signal ?? AbortSignal.timeout(this.timeoutMs),
|
|
1133
1139
|
headers: {
|
|
@@ -1139,24 +1145,24 @@ var PixelLabClient = class {
|
|
|
1139
1145
|
} catch (err) {
|
|
1140
1146
|
if (attempt < MAX_RETRIES) {
|
|
1141
1147
|
await sleep(backoffMs(attempt));
|
|
1142
|
-
return this.request(
|
|
1148
|
+
return this.request(path23, init, attempt + 1);
|
|
1143
1149
|
}
|
|
1144
1150
|
throw err;
|
|
1145
1151
|
}
|
|
1146
1152
|
if (!res.ok && shouldRetry(res.status) && attempt < MAX_RETRIES) {
|
|
1147
1153
|
const waitMs = retryAfterMs(res.headers.get("retry-after")) ?? backoffMs(attempt);
|
|
1148
1154
|
await sleep(waitMs);
|
|
1149
|
-
return this.request(
|
|
1155
|
+
return this.request(path23, init, attempt + 1);
|
|
1150
1156
|
}
|
|
1151
1157
|
const text = await res.text();
|
|
1152
1158
|
if (!res.ok) {
|
|
1153
|
-
throw new PixelLabError(`${init?.method ?? "GET"} ${
|
|
1159
|
+
throw new PixelLabError(`${init?.method ?? "GET"} ${path23} \u2192 ${res.status}`, res.status, text);
|
|
1154
1160
|
}
|
|
1155
1161
|
if (!text) return {};
|
|
1156
1162
|
try {
|
|
1157
1163
|
return JSON.parse(text);
|
|
1158
1164
|
} catch {
|
|
1159
|
-
throw new Error(`${init?.method ?? "GET"} ${
|
|
1165
|
+
throw new Error(`${init?.method ?? "GET"} ${path23} returned invalid JSON`);
|
|
1160
1166
|
}
|
|
1161
1167
|
}
|
|
1162
1168
|
async balance() {
|
|
@@ -1996,9 +2002,9 @@ var RetroDiffusionClient = class {
|
|
|
1996
2002
|
}
|
|
1997
2003
|
return balance;
|
|
1998
2004
|
}
|
|
1999
|
-
async call(
|
|
2005
|
+
async call(path23, init = {}) {
|
|
2000
2006
|
if (!this.token) throw new Error("RD_API_KEY is not set");
|
|
2001
|
-
const response = await this.request(`${this.baseUrl}${
|
|
2007
|
+
const response = await this.request(`${this.baseUrl}${path23}`, {
|
|
2002
2008
|
...init,
|
|
2003
2009
|
headers: {
|
|
2004
2010
|
"Content-Type": "application/json",
|
|
@@ -2262,8 +2268,8 @@ function retroError(value) {
|
|
|
2262
2268
|
if (details.length) return details.join("; ");
|
|
2263
2269
|
}
|
|
2264
2270
|
if (record.detail && typeof record.detail === "object") {
|
|
2265
|
-
const
|
|
2266
|
-
if (typeof
|
|
2271
|
+
const message5 = record.detail.message;
|
|
2272
|
+
if (typeof message5 === "string") return message5;
|
|
2267
2273
|
}
|
|
2268
2274
|
return JSON.stringify(value);
|
|
2269
2275
|
}
|
|
@@ -2315,13 +2321,13 @@ registerProvider({
|
|
|
2315
2321
|
});
|
|
2316
2322
|
|
|
2317
2323
|
// src/manifest.ts
|
|
2318
|
-
import { readFile as
|
|
2324
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
2319
2325
|
import { existsSync as existsSync3 } from "fs";
|
|
2320
2326
|
import path4 from "path";
|
|
2321
2327
|
async function loadManifest(manifestPath) {
|
|
2322
2328
|
const abs = path4.resolve(manifestPath);
|
|
2323
2329
|
if (!existsSync3(abs)) throw new Error(`No manifest at ${abs}`);
|
|
2324
|
-
const parsed = ManifestSchema.safeParse(JSON.parse(await
|
|
2330
|
+
const parsed = ManifestSchema.safeParse(JSON.parse(await readFile2(abs, "utf8")));
|
|
2325
2331
|
if (!parsed.success) {
|
|
2326
2332
|
const issues = parsed.error.issues.map((i) => ` ${i.path.join(".")}: ${i.message}`).join("\n");
|
|
2327
2333
|
throw new Error(`Manifest at ${abs} is invalid:
|
|
@@ -2370,7 +2376,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
2370
2376
|
let hit = styleImageCache.get(abs);
|
|
2371
2377
|
if (!hit) {
|
|
2372
2378
|
if (!existsSync3(abs)) throw new Error(`Style image not found: ${abs}`);
|
|
2373
|
-
const buf = await
|
|
2379
|
+
const buf = await readFile2(abs);
|
|
2374
2380
|
const metadata = imageMetadata(buf);
|
|
2375
2381
|
if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${abs}`);
|
|
2376
2382
|
if (metadata.width < 1 || metadata.height < 1) {
|
|
@@ -2496,7 +2502,7 @@ async function resolveStyleImages(loaded, styleId) {
|
|
|
2496
2502
|
const out = [];
|
|
2497
2503
|
for (const img of style.styleImages) {
|
|
2498
2504
|
const file = path4.resolve(loaded.root, img.path);
|
|
2499
|
-
const buf = await
|
|
2505
|
+
const buf = await readFile2(file);
|
|
2500
2506
|
const metadata = imageMetadata(buf);
|
|
2501
2507
|
if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${file}`);
|
|
2502
2508
|
if (metadata.width < 1 || metadata.height < 1) {
|
|
@@ -2543,13 +2549,13 @@ import path7 from "path";
|
|
|
2543
2549
|
import path6 from "path";
|
|
2544
2550
|
|
|
2545
2551
|
// src/lock.ts
|
|
2546
|
-
import { readFile as
|
|
2552
|
+
import { readFile as readFile3, writeFile, rename, mkdir, rmdir, rm, stat } from "fs/promises";
|
|
2547
2553
|
import { existsSync as existsSync4 } from "fs";
|
|
2548
2554
|
import path5 from "path";
|
|
2549
2555
|
async function loadLock(lockPath) {
|
|
2550
2556
|
if (!existsSync4(lockPath)) return { version: 2, entries: {} };
|
|
2551
2557
|
try {
|
|
2552
|
-
return parseLock(JSON.parse(await
|
|
2558
|
+
return parseLock(JSON.parse(await readFile3(lockPath, "utf8")));
|
|
2553
2559
|
} catch (err) {
|
|
2554
2560
|
throw new Error(
|
|
2555
2561
|
`Lockfile at ${lockPath} is malformed:
|
|
@@ -2585,7 +2591,7 @@ async function writeLockWhileHeld(file, lock) {
|
|
|
2585
2591
|
let disk = { version: 2, entries: {} };
|
|
2586
2592
|
if (existsSync4(file)) {
|
|
2587
2593
|
try {
|
|
2588
|
-
disk = parseLock(JSON.parse(await
|
|
2594
|
+
disk = parseLock(JSON.parse(await readFile3(file, "utf8")));
|
|
2589
2595
|
} catch (err) {
|
|
2590
2596
|
throw new Error(
|
|
2591
2597
|
`Refusing to overwrite malformed lockfile at ${file}: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -3262,9 +3268,9 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
3262
3268
|
);
|
|
3263
3269
|
} catch (err) {
|
|
3264
3270
|
failed++;
|
|
3265
|
-
const
|
|
3266
|
-
upsert(lock, key, { status: "failed", error:
|
|
3267
|
-
log2(` FAILED ${key}: ${
|
|
3271
|
+
const message5 = err instanceof Error ? err.message : String(err);
|
|
3272
|
+
upsert(lock, key, { status: "failed", error: message5, cost: 0 });
|
|
3273
|
+
log2(` FAILED ${key}: ${message5}`);
|
|
3268
3274
|
}
|
|
3269
3275
|
await saveLock(lockPath, lock);
|
|
3270
3276
|
}
|
|
@@ -3335,7 +3341,7 @@ async function poll(provider, lock, lockPath, opts = {}) {
|
|
|
3335
3341
|
|
|
3336
3342
|
// src/pipeline/fetch.ts
|
|
3337
3343
|
import { existsSync as existsSync6 } from "fs";
|
|
3338
|
-
import { mkdir as mkdir2, readFile as
|
|
3344
|
+
import { mkdir as mkdir2, readFile as readFile4, rename as rename2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
3339
3345
|
import path9 from "path";
|
|
3340
3346
|
async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
3341
3347
|
const log2 = opts.onProgress ?? (() => {
|
|
@@ -3395,7 +3401,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
3395
3401
|
if (cacheDir) {
|
|
3396
3402
|
await cacheMedia(
|
|
3397
3403
|
cacheDir,
|
|
3398
|
-
await
|
|
3404
|
+
await readFile4(target),
|
|
3399
3405
|
recorded.mediaType ?? MediaType.PNG,
|
|
3400
3406
|
recorded.sha256
|
|
3401
3407
|
);
|
|
@@ -3452,14 +3458,14 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
3452
3458
|
});
|
|
3453
3459
|
result.downloaded++;
|
|
3454
3460
|
} catch (err) {
|
|
3455
|
-
const
|
|
3461
|
+
const message5 = err instanceof Error ? err.message : String(err);
|
|
3456
3462
|
upsert(lock, key, {
|
|
3457
3463
|
status: "download-failed",
|
|
3458
3464
|
outputs: mergeOutputs(entry.outputs, outputs),
|
|
3459
|
-
error: `download failed: ${
|
|
3465
|
+
error: `download failed: ${message5}`
|
|
3460
3466
|
});
|
|
3461
3467
|
result.failed++;
|
|
3462
|
-
log2(` FAILED ${key}: ${
|
|
3468
|
+
log2(` FAILED ${key}: ${message5}`);
|
|
3463
3469
|
}
|
|
3464
3470
|
await saveLock(lockPath, lock);
|
|
3465
3471
|
}
|
|
@@ -3472,7 +3478,7 @@ async function readCachedMedia(cacheDir, hash, mediaType) {
|
|
|
3472
3478
|
const file = path9.join(cacheDir, cacheFileName(hash, mediaType));
|
|
3473
3479
|
if (!existsSync6(file)) return null;
|
|
3474
3480
|
try {
|
|
3475
|
-
const buf = await
|
|
3481
|
+
const buf = await readFile4(file);
|
|
3476
3482
|
if (sha256(buf) !== hash) return null;
|
|
3477
3483
|
validateMedia(buf, mediaType);
|
|
3478
3484
|
return buf;
|
|
@@ -3529,7 +3535,7 @@ import { access, stat as stat2 } from "fs/promises";
|
|
|
3529
3535
|
import path10 from "path";
|
|
3530
3536
|
async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
3531
3537
|
const checks = [];
|
|
3532
|
-
const add = (id, level,
|
|
3538
|
+
const add = (id, level, message5) => checks.push({ id, level, message: message5 });
|
|
3533
3539
|
add(
|
|
3534
3540
|
"manifest",
|
|
3535
3541
|
"ok",
|
|
@@ -3659,11 +3665,11 @@ async function nearestExistingDirectory(start) {
|
|
|
3659
3665
|
|
|
3660
3666
|
// src/pipeline/adopt.ts
|
|
3661
3667
|
import { existsSync as existsSync9 } from "fs";
|
|
3662
|
-
import { readFile as
|
|
3668
|
+
import { readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
|
|
3663
3669
|
import path11 from "path";
|
|
3664
3670
|
|
|
3665
3671
|
// src/cache.ts
|
|
3666
|
-
import { mkdir as mkdir3, readFile as
|
|
3672
|
+
import { mkdir as mkdir3, readFile as readFile5, rename as rename3, rm as rm3, writeFile as writeFile3 } from "fs/promises";
|
|
3667
3673
|
import { existsSync as existsSync8 } from "fs";
|
|
3668
3674
|
import pathModule from "path";
|
|
3669
3675
|
import { z as z3 } from "zod";
|
|
@@ -3679,10 +3685,10 @@ function isSha256Hash(value) {
|
|
|
3679
3685
|
function parseCache(value) {
|
|
3680
3686
|
return HashCacheSchema.parse(value);
|
|
3681
3687
|
}
|
|
3682
|
-
async function loadCache(
|
|
3683
|
-
if (!existsSync8(
|
|
3688
|
+
async function loadCache(path23) {
|
|
3689
|
+
if (!existsSync8(path23)) return { version: 1, hashes: {} };
|
|
3684
3690
|
try {
|
|
3685
|
-
const parsed = HashCacheSchema.safeParse(JSON.parse(await
|
|
3691
|
+
const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile5(path23, "utf8")));
|
|
3686
3692
|
if (!parsed.success) return { version: 1, hashes: {} };
|
|
3687
3693
|
return {
|
|
3688
3694
|
version: 1,
|
|
@@ -3694,18 +3700,18 @@ async function loadCache(path21) {
|
|
|
3694
3700
|
return { version: 1, hashes: {} };
|
|
3695
3701
|
}
|
|
3696
3702
|
}
|
|
3697
|
-
async function saveCache(
|
|
3703
|
+
async function saveCache(path23, cache) {
|
|
3698
3704
|
const sorted = {};
|
|
3699
3705
|
for (const key of Object.keys(cache.hashes).sort()) {
|
|
3700
3706
|
const hash = cache.hashes[key];
|
|
3701
3707
|
if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
|
|
3702
3708
|
sorted[key] = hash;
|
|
3703
3709
|
}
|
|
3704
|
-
await mkdir3(pathModule.dirname(pathModule.resolve(
|
|
3705
|
-
const tmp = `${
|
|
3710
|
+
await mkdir3(pathModule.dirname(pathModule.resolve(path23)), { recursive: true });
|
|
3711
|
+
const tmp = `${path23}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
3706
3712
|
try {
|
|
3707
3713
|
await writeFile3(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
|
|
3708
|
-
await rename3(tmp,
|
|
3714
|
+
await rename3(tmp, path23);
|
|
3709
3715
|
} finally {
|
|
3710
3716
|
await rm3(tmp, { force: true });
|
|
3711
3717
|
}
|
|
@@ -3740,7 +3746,7 @@ async function adopt(provider, specs, lock, lockPath, opts = {}) {
|
|
|
3740
3746
|
unmatchedLocal.push(`${spec.styleId}/${spec.assetId} (no file at ${spec.outFile})`);
|
|
3741
3747
|
continue;
|
|
3742
3748
|
}
|
|
3743
|
-
const bytes = await
|
|
3749
|
+
const bytes = await readFile6(spec.outFile);
|
|
3744
3750
|
try {
|
|
3745
3751
|
decodePng(bytes);
|
|
3746
3752
|
} catch (err) {
|
|
@@ -3857,7 +3863,7 @@ async function tagAdopted(provider, specs, lock, opts = {}) {
|
|
|
3857
3863
|
async function writePromptsBack(manifestPath, lock, opts = {}) {
|
|
3858
3864
|
const log2 = opts.onProgress ?? (() => {
|
|
3859
3865
|
});
|
|
3860
|
-
const raw = JSON.parse(await
|
|
3866
|
+
const raw = JSON.parse(await readFile6(manifestPath, "utf8"));
|
|
3861
3867
|
const best = /* @__PURE__ */ new Map();
|
|
3862
3868
|
for (const entry of Object.values(lock.entries)) {
|
|
3863
3869
|
if (!entry.objectId || !entry.prompt) continue;
|
|
@@ -4156,10 +4162,10 @@ function serveReviewPage(opts) {
|
|
|
4156
4162
|
res.end(JSON.stringify(result));
|
|
4157
4163
|
server.close(() => resolve(result));
|
|
4158
4164
|
} catch (err) {
|
|
4159
|
-
const
|
|
4160
|
-
log2(` apply failed: ${
|
|
4165
|
+
const message5 = err instanceof Error ? err.message : String(err);
|
|
4166
|
+
log2(` apply failed: ${message5}`);
|
|
4161
4167
|
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
4162
|
-
res.end(
|
|
4168
|
+
res.end(message5);
|
|
4163
4169
|
}
|
|
4164
4170
|
return;
|
|
4165
4171
|
}
|
|
@@ -4247,7 +4253,7 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
|
|
|
4247
4253
|
}
|
|
4248
4254
|
|
|
4249
4255
|
// src/pipeline/init.ts
|
|
4250
|
-
import { readdir, readFile as
|
|
4256
|
+
import { readdir, readFile as readFile7, writeFile as writeFile5 } from "fs/promises";
|
|
4251
4257
|
import { existsSync as existsSync10 } from "fs";
|
|
4252
4258
|
import path12 from "path";
|
|
4253
4259
|
function pngSize(buf) {
|
|
@@ -4277,7 +4283,7 @@ async function scanAssets(root, opts = {}) {
|
|
|
4277
4283
|
const skipped = [];
|
|
4278
4284
|
const seen = /* @__PURE__ */ new Map();
|
|
4279
4285
|
for (const file of files.sort()) {
|
|
4280
|
-
const size = pngSize(await
|
|
4286
|
+
const size = pngSize(await readFile7(file));
|
|
4281
4287
|
if (!size) {
|
|
4282
4288
|
skipped.push(`${file} (not a readable PNG)`);
|
|
4283
4289
|
continue;
|
|
@@ -4346,7 +4352,7 @@ async function writeManifestFile(target, manifest) {
|
|
|
4346
4352
|
}
|
|
4347
4353
|
|
|
4348
4354
|
// src/pipeline/salvage.ts
|
|
4349
|
-
import { readFile as
|
|
4355
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
4350
4356
|
import { existsSync as existsSync11 } from "fs";
|
|
4351
4357
|
import path13 from "path";
|
|
4352
4358
|
async function loadClaims(lockPaths) {
|
|
@@ -4355,7 +4361,7 @@ async function loadClaims(lockPaths) {
|
|
|
4355
4361
|
if (!existsSync11(p)) throw new Error(`Claim lockfile not found: ${p}`);
|
|
4356
4362
|
let parsed;
|
|
4357
4363
|
try {
|
|
4358
|
-
parsed = parseLock(JSON.parse(await
|
|
4364
|
+
parsed = parseLock(JSON.parse(await readFile8(p, "utf8")));
|
|
4359
4365
|
} catch {
|
|
4360
4366
|
throw new Error(`Claim lockfile is malformed: ${p}`);
|
|
4361
4367
|
}
|
|
@@ -4526,7 +4532,7 @@ async function applyTags(provider, decisions, existing, opts = {}) {
|
|
|
4526
4532
|
}
|
|
4527
4533
|
|
|
4528
4534
|
// src/workspace.ts
|
|
4529
|
-
import { mkdir as mkdir4, readFile as
|
|
4535
|
+
import { mkdir as mkdir4, readFile as readFile9, rename as rename4, rm as rm4, writeFile as writeFile6 } from "fs/promises";
|
|
4530
4536
|
import { existsSync as existsSync12 } from "fs";
|
|
4531
4537
|
import path14 from "path";
|
|
4532
4538
|
import { z as z4 } from "zod";
|
|
@@ -4556,7 +4562,7 @@ async function loadWorkspace(workspacePath) {
|
|
|
4556
4562
|
if (!existsSync12(workspacePath)) return { version: 1, projects: [] };
|
|
4557
4563
|
let raw;
|
|
4558
4564
|
try {
|
|
4559
|
-
raw = JSON.parse(await
|
|
4565
|
+
raw = JSON.parse(await readFile9(workspacePath, "utf8"));
|
|
4560
4566
|
} catch (err) {
|
|
4561
4567
|
throw new Error(
|
|
4562
4568
|
`Workspace catalog at ${workspacePath} is malformed:
|
|
@@ -4753,7 +4759,7 @@ async function workspaceStatus(ws, dir) {
|
|
|
4753
4759
|
}
|
|
4754
4760
|
|
|
4755
4761
|
// src/pipeline/audit.ts
|
|
4756
|
-
import { readFile as
|
|
4762
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
4757
4763
|
import { existsSync as existsSync13 } from "fs";
|
|
4758
4764
|
import path15 from "path";
|
|
4759
4765
|
function colorDistance(a, b) {
|
|
@@ -4805,7 +4811,7 @@ async function auditStyle(loaded, specs, styleId, lock) {
|
|
|
4805
4811
|
continue;
|
|
4806
4812
|
}
|
|
4807
4813
|
try {
|
|
4808
|
-
const png = decodePng(await
|
|
4814
|
+
const png = decodePng(await readFile10(output.absolutePath));
|
|
4809
4815
|
const palette = extractPalette(png, 12);
|
|
4810
4816
|
assets.push({
|
|
4811
4817
|
assetId: spec.assetId,
|
|
@@ -4832,7 +4838,7 @@ async function auditStyle(loaded, specs, styleId, lock) {
|
|
|
4832
4838
|
const abs = path15.resolve(loaded.root, img.path);
|
|
4833
4839
|
if (!existsSync13(abs)) continue;
|
|
4834
4840
|
try {
|
|
4835
|
-
refPalettes.push(extractPalette(decodePng(await
|
|
4841
|
+
refPalettes.push(extractPalette(decodePng(await readFile10(abs)), 12));
|
|
4836
4842
|
} catch {
|
|
4837
4843
|
}
|
|
4838
4844
|
}
|
|
@@ -4909,7 +4915,7 @@ function hex(c) {
|
|
|
4909
4915
|
|
|
4910
4916
|
// src/pipeline/cache-health.ts
|
|
4911
4917
|
import { existsSync as existsSync14 } from "fs";
|
|
4912
|
-
import { readFile as
|
|
4918
|
+
import { readFile as readFile11, readdir as readdir2, rm as rm5 } from "fs/promises";
|
|
4913
4919
|
import path16 from "path";
|
|
4914
4920
|
async function inspectCaches(lock, lockPath, options = {}) {
|
|
4915
4921
|
if (options.prune && !existsSync14(lockPath)) {
|
|
@@ -4939,7 +4945,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
4939
4945
|
await saveCache(remotePath, { version: 1, hashes: {} });
|
|
4940
4946
|
removed.resetRemoteHashCache = true;
|
|
4941
4947
|
} else if (remoteHashes.invalidIds.length) {
|
|
4942
|
-
const cache = parseCache(JSON.parse(await
|
|
4948
|
+
const cache = parseCache(JSON.parse(await readFile11(remotePath, "utf8")));
|
|
4943
4949
|
for (const id of remoteHashes.invalidIds) delete cache.hashes[id];
|
|
4944
4950
|
removed.remoteHashEntries = remoteHashes.invalidIds.length;
|
|
4945
4951
|
await saveCache(remotePath, cache);
|
|
@@ -4983,7 +4989,7 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
4983
4989
|
const expected = mediaType ? entry.name.slice(0, -4) : "";
|
|
4984
4990
|
let bytes;
|
|
4985
4991
|
try {
|
|
4986
|
-
bytes = await
|
|
4992
|
+
bytes = await readFile11(file);
|
|
4987
4993
|
report.bytes += bytes.length;
|
|
4988
4994
|
} catch (err) {
|
|
4989
4995
|
report.invalid.push({
|
|
@@ -5030,7 +5036,7 @@ async function inspectRemoteHashCache(remotePath) {
|
|
|
5030
5036
|
if (!report.exists) return report;
|
|
5031
5037
|
let cache;
|
|
5032
5038
|
try {
|
|
5033
|
-
cache = parseCache(JSON.parse(await
|
|
5039
|
+
cache = parseCache(JSON.parse(await readFile11(remotePath, "utf8")));
|
|
5034
5040
|
} catch (err) {
|
|
5035
5041
|
report.error = err instanceof Error ? err.message : String(err);
|
|
5036
5042
|
return report;
|
|
@@ -5047,14 +5053,14 @@ async function inspectRemoteHashCache(remotePath) {
|
|
|
5047
5053
|
// src/pipeline/refine.ts
|
|
5048
5054
|
import path18 from "path";
|
|
5049
5055
|
import { execFile } from "child_process";
|
|
5050
|
-
import { mkdtemp, readFile as
|
|
5056
|
+
import { mkdtemp, readFile as readFile13, rm as rm7 } from "fs/promises";
|
|
5051
5057
|
import { tmpdir } from "os";
|
|
5052
5058
|
import { promisify } from "util";
|
|
5053
5059
|
|
|
5054
5060
|
// src/artifacts.ts
|
|
5055
5061
|
import path17 from "path";
|
|
5056
5062
|
import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
|
|
5057
|
-
import { mkdir as mkdir5, readFile as
|
|
5063
|
+
import { mkdir as mkdir5, readFile as readFile12, rename as rename5, rm as rm6, writeFile as writeFile7 } from "fs/promises";
|
|
5058
5064
|
var activeTransactions = /* @__PURE__ */ new Set();
|
|
5059
5065
|
function message(error) {
|
|
5060
5066
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -5114,11 +5120,11 @@ function parseArtifactManifest(absolute, data) {
|
|
|
5114
5120
|
}
|
|
5115
5121
|
async function readArtifactBundleManifest(manifestPath) {
|
|
5116
5122
|
const absolute = path17.resolve(manifestPath);
|
|
5117
|
-
return parseArtifactManifest(absolute, await
|
|
5123
|
+
return parseArtifactManifest(absolute, await readFile12(absolute, "utf8"));
|
|
5118
5124
|
}
|
|
5119
5125
|
async function readOptional(file) {
|
|
5120
5126
|
try {
|
|
5121
|
-
return await
|
|
5127
|
+
return await readFile12(file);
|
|
5122
5128
|
} catch (error) {
|
|
5123
5129
|
if (isCode(error, "ENOENT")) return null;
|
|
5124
5130
|
throw error;
|
|
@@ -5307,7 +5313,7 @@ async function verifyArtifactBundle(manifestPath) {
|
|
|
5307
5313
|
for (const source of raw.sources) {
|
|
5308
5314
|
let actual = null;
|
|
5309
5315
|
try {
|
|
5310
|
-
actual = digest(await
|
|
5316
|
+
actual = digest(await readFile12(path17.resolve(root, source.path)));
|
|
5311
5317
|
} catch (error) {
|
|
5312
5318
|
if (!isCode(error, "ENOENT")) throw error;
|
|
5313
5319
|
}
|
|
@@ -5315,7 +5321,7 @@ async function verifyArtifactBundle(manifestPath) {
|
|
|
5315
5321
|
}
|
|
5316
5322
|
for (const output of raw.outputs) {
|
|
5317
5323
|
try {
|
|
5318
|
-
if (digest(await
|
|
5324
|
+
if (digest(await readFile12(path17.resolve(root, output.path))) !== output.sha256) {
|
|
5319
5325
|
changedOutputs.push(output.path);
|
|
5320
5326
|
}
|
|
5321
5327
|
} catch (error) {
|
|
@@ -5387,7 +5393,7 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
5387
5393
|
const unchanged = [];
|
|
5388
5394
|
for (const artifact of normalized) {
|
|
5389
5395
|
try {
|
|
5390
|
-
const current = await
|
|
5396
|
+
const current = await readFile12(artifact.destination);
|
|
5391
5397
|
if (current.equals(artifact.data)) {
|
|
5392
5398
|
unchanged.push(artifact.destination);
|
|
5393
5399
|
} else {
|
|
@@ -5706,7 +5712,7 @@ async function refineAsset(options) {
|
|
|
5706
5712
|
if (options.minTransparency !== void 0 && (!Number.isFinite(options.minTransparency) || options.minTransparency < 0 || options.minTransparency > 1)) throw new Error("minTransparency must be between 0 and 1.");
|
|
5707
5713
|
let sourcePng;
|
|
5708
5714
|
try {
|
|
5709
|
-
sourcePng = decodePng(await
|
|
5715
|
+
sourcePng = decodePng(await readFile13(source));
|
|
5710
5716
|
} catch (error) {
|
|
5711
5717
|
throw new Error(`Cannot read refinement source ${source}: ${message2(error)}`, { cause: error });
|
|
5712
5718
|
}
|
|
@@ -5733,7 +5739,7 @@ async function refineAsset(options) {
|
|
|
5733
5739
|
);
|
|
5734
5740
|
}
|
|
5735
5741
|
const detection = parseDetection(firstJsonObject(stdout));
|
|
5736
|
-
const native = decodePng(await
|
|
5742
|
+
const native = decodePng(await readFile13(recovered));
|
|
5737
5743
|
const quantized = quantizeToPalette(native, palette);
|
|
5738
5744
|
const png = encodeRgbaPng(quantized.width, quantized.height, quantized.pixels);
|
|
5739
5745
|
const audit = auditRefinement(
|
|
@@ -5832,7 +5838,7 @@ async function approveQualityRecord(recordPath, approval) {
|
|
|
5832
5838
|
}
|
|
5833
5839
|
const root = path18.dirname(record);
|
|
5834
5840
|
const outputPath = path18.resolve(root, manifest.outputs[0].path);
|
|
5835
|
-
const output = await
|
|
5841
|
+
const output = await readFile13(outputPath);
|
|
5836
5842
|
const sources = manifest.sources.map((source) => ({
|
|
5837
5843
|
...source,
|
|
5838
5844
|
path: path18.resolve(root, source.path)
|
|
@@ -6118,7 +6124,7 @@ function isRecord(value) {
|
|
|
6118
6124
|
}
|
|
6119
6125
|
|
|
6120
6126
|
// src/pick/salvage-server.ts
|
|
6121
|
-
import { mkdir as mkdir6, writeFile as writeFile8, readFile as
|
|
6127
|
+
import { mkdir as mkdir6, writeFile as writeFile8, readFile as readFile14 } from "fs/promises";
|
|
6122
6128
|
import path19 from "path";
|
|
6123
6129
|
|
|
6124
6130
|
// src/pick/salvage-sheet.ts
|
|
@@ -6366,7 +6372,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
6366
6372
|
}
|
|
6367
6373
|
}
|
|
6368
6374
|
await applyTags(provider, decisions, existingTags, { onProgress: log2 });
|
|
6369
|
-
const raw = JSON.parse(await
|
|
6375
|
+
const raw = JSON.parse(await readFile14(ctx.manifestPath, "utf8"));
|
|
6370
6376
|
for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
|
|
6371
6377
|
await writeFile8(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
|
|
6372
6378
|
await saveLock(ctx.lockPath, ctx.lock);
|
|
@@ -6375,14 +6381,742 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
6375
6381
|
});
|
|
6376
6382
|
}
|
|
6377
6383
|
|
|
6384
|
+
// src/recipes.ts
|
|
6385
|
+
import path20 from "path";
|
|
6386
|
+
import { existsSync as existsSync15 } from "fs";
|
|
6387
|
+
import { readdir as readdir3, readFile as readFile15 } from "fs/promises";
|
|
6388
|
+
import { fileURLToPath } from "url";
|
|
6389
|
+
import { z as z5 } from "zod";
|
|
6390
|
+
var SHA256_RE = /^[0-9a-f]{64}$/;
|
|
6391
|
+
var RECIPE_ID_RE = /^[a-z0-9][a-z0-9-]*\/[a-z0-9][a-z0-9-]*$/;
|
|
6392
|
+
var VERSION_RE = /^\d+\.\d+\.\d+$/;
|
|
6393
|
+
var RECIPE_FILE = "pixelkiln.recipe.json";
|
|
6394
|
+
var RECIPE_DIR_TOKEN = "{{recipeDir}}";
|
|
6395
|
+
var PortablePathSchema = z5.string().min(1).refine((value) => {
|
|
6396
|
+
if (value.includes("\\") || path20.posix.isAbsolute(value)) return false;
|
|
6397
|
+
const parts = value.split("/");
|
|
6398
|
+
return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
|
|
6399
|
+
}, "expected a portable relative path without '.', '..', or backslashes");
|
|
6400
|
+
var RecipeBindingSchema = z5.object({
|
|
6401
|
+
nodeId: z5.string().min(1),
|
|
6402
|
+
input: z5.string().min(1)
|
|
6403
|
+
}).strict();
|
|
6404
|
+
var RecipeSchema = z5.object({
|
|
6405
|
+
$schema: z5.string().url().optional(),
|
|
6406
|
+
format: z5.literal("pixelkiln-recipe"),
|
|
6407
|
+
schemaVersion: z5.literal(1),
|
|
6408
|
+
id: z5.string().regex(RECIPE_ID_RE, "expected provider/name"),
|
|
6409
|
+
version: z5.string().regex(VERSION_RE, "expected x.y.z"),
|
|
6410
|
+
provider: z5.string().regex(/^[a-z0-9][a-z0-9-]*$/),
|
|
6411
|
+
summary: z5.string().min(1).max(240),
|
|
6412
|
+
files: z5.array(z5.object({
|
|
6413
|
+
path: PortablePathSchema,
|
|
6414
|
+
role: z5.enum(["workflow", "reference"]),
|
|
6415
|
+
sha256: z5.string().regex(SHA256_RE)
|
|
6416
|
+
}).strict()).default([]),
|
|
6417
|
+
models: z5.array(z5.object({
|
|
6418
|
+
path: PortablePathSchema,
|
|
6419
|
+
sha256: z5.string().regex(SHA256_RE),
|
|
6420
|
+
source: z5.string().url(),
|
|
6421
|
+
license: z5.string().min(1)
|
|
6422
|
+
}).strict()).default([]),
|
|
6423
|
+
styleId: z5.string().regex(/^[a-z0-9][a-z0-9-]*$/),
|
|
6424
|
+
style: StyleSchema,
|
|
6425
|
+
workflow: z5.object({
|
|
6426
|
+
path: PortablePathSchema,
|
|
6427
|
+
outputNodeId: z5.string().min(1),
|
|
6428
|
+
numImages: z5.number().int().min(1).max(16),
|
|
6429
|
+
bindings: z5.object({
|
|
6430
|
+
prompt: RecipeBindingSchema,
|
|
6431
|
+
width: RecipeBindingSchema,
|
|
6432
|
+
height: RecipeBindingSchema,
|
|
6433
|
+
batchSize: RecipeBindingSchema,
|
|
6434
|
+
seed: RecipeBindingSchema.optional()
|
|
6435
|
+
}).strict()
|
|
6436
|
+
}).strict().optional(),
|
|
6437
|
+
quality: z5.object({
|
|
6438
|
+
stage: z5.enum(["composition-source", "production-candidate"]),
|
|
6439
|
+
workingCanvas: z5.object({
|
|
6440
|
+
width: z5.number().int().min(16),
|
|
6441
|
+
height: z5.number().int().min(16)
|
|
6442
|
+
}).strict(),
|
|
6443
|
+
recommendedNativeSize: z5.object({
|
|
6444
|
+
min: z5.number().int().min(1),
|
|
6445
|
+
max: z5.number().int().min(1)
|
|
6446
|
+
}).strict(),
|
|
6447
|
+
paletteColors: z5.object({
|
|
6448
|
+
min: z5.number().int().min(1),
|
|
6449
|
+
max: z5.number().int().min(1)
|
|
6450
|
+
}).strict(),
|
|
6451
|
+
background: z5.enum(["opaque", "transparent", "full-bleed"]),
|
|
6452
|
+
checks: z5.array(z5.enum([
|
|
6453
|
+
"prompt-coverage",
|
|
6454
|
+
"native-grid",
|
|
6455
|
+
"final-palette",
|
|
6456
|
+
"alpha-edge",
|
|
6457
|
+
"human-1x"
|
|
6458
|
+
])).min(1),
|
|
6459
|
+
notes: z5.array(z5.string().min(1)).default([])
|
|
6460
|
+
}).strict(),
|
|
6461
|
+
integrity: z5.object({
|
|
6462
|
+
algorithm: z5.literal("sha256"),
|
|
6463
|
+
digest: z5.string().regex(SHA256_RE)
|
|
6464
|
+
}).strict()
|
|
6465
|
+
}).strict().superRefine((recipe, context) => {
|
|
6466
|
+
const filePaths = recipe.files.map((file) => file.path);
|
|
6467
|
+
if (new Set(filePaths).size !== filePaths.length) {
|
|
6468
|
+
context.addIssue({ code: z5.ZodIssueCode.custom, path: ["files"], message: "duplicate file path" });
|
|
6469
|
+
}
|
|
6470
|
+
const modelPaths = recipe.models.map((model) => model.path);
|
|
6471
|
+
if (new Set(modelPaths).size !== modelPaths.length) {
|
|
6472
|
+
context.addIssue({ code: z5.ZodIssueCode.custom, path: ["models"], message: "duplicate model path" });
|
|
6473
|
+
}
|
|
6474
|
+
if (filePaths.includes(RECIPE_FILE)) {
|
|
6475
|
+
context.addIssue({
|
|
6476
|
+
code: z5.ZodIssueCode.custom,
|
|
6477
|
+
path: ["files"],
|
|
6478
|
+
message: `${RECIPE_FILE} cannot hash itself`
|
|
6479
|
+
});
|
|
6480
|
+
}
|
|
6481
|
+
if (recipe.quality.recommendedNativeSize.min > recipe.quality.recommendedNativeSize.max) {
|
|
6482
|
+
context.addIssue({
|
|
6483
|
+
code: z5.ZodIssueCode.custom,
|
|
6484
|
+
path: ["quality", "recommendedNativeSize"],
|
|
6485
|
+
message: "min cannot exceed max"
|
|
6486
|
+
});
|
|
6487
|
+
}
|
|
6488
|
+
if (recipe.quality.paletteColors.min > recipe.quality.paletteColors.max) {
|
|
6489
|
+
context.addIssue({
|
|
6490
|
+
code: z5.ZodIssueCode.custom,
|
|
6491
|
+
path: ["quality", "paletteColors"],
|
|
6492
|
+
message: "min cannot exceed max"
|
|
6493
|
+
});
|
|
6494
|
+
}
|
|
6495
|
+
if (recipe.workflow && !recipe.files.some(
|
|
6496
|
+
(file) => file.path === recipe.workflow.path && file.role === "workflow"
|
|
6497
|
+
)) {
|
|
6498
|
+
context.addIssue({
|
|
6499
|
+
code: z5.ZodIssueCode.custom,
|
|
6500
|
+
path: ["workflow", "path"],
|
|
6501
|
+
message: "must name a file whose role is workflow"
|
|
6502
|
+
});
|
|
6503
|
+
}
|
|
6504
|
+
if (recipe.provider === "comfyui") {
|
|
6505
|
+
if (!recipe.workflow) {
|
|
6506
|
+
context.addIssue({
|
|
6507
|
+
code: z5.ZodIssueCode.custom,
|
|
6508
|
+
path: ["workflow"],
|
|
6509
|
+
message: "ComfyUI recipes require workflow metadata"
|
|
6510
|
+
});
|
|
6511
|
+
return;
|
|
6512
|
+
}
|
|
6513
|
+
const options = recipe.style.providerOptions.comfyui;
|
|
6514
|
+
if (!options || typeof options !== "object" || Array.isArray(options)) {
|
|
6515
|
+
context.addIssue({
|
|
6516
|
+
code: z5.ZodIssueCode.custom,
|
|
6517
|
+
path: ["style", "providerOptions", "comfyui"],
|
|
6518
|
+
message: "ComfyUI recipes require ComfyUI provider options"
|
|
6519
|
+
});
|
|
6520
|
+
return;
|
|
6521
|
+
}
|
|
6522
|
+
const expected = {
|
|
6523
|
+
workflowFile: `${RECIPE_DIR_TOKEN}/${recipe.workflow.path}`,
|
|
6524
|
+
outputNodeId: recipe.workflow.outputNodeId,
|
|
6525
|
+
numImages: recipe.workflow.numImages,
|
|
6526
|
+
bindings: recipe.workflow.bindings
|
|
6527
|
+
};
|
|
6528
|
+
if (JSON.stringify(canonical3(options)) !== JSON.stringify(canonical3(expected))) {
|
|
6529
|
+
context.addIssue({
|
|
6530
|
+
code: z5.ZodIssueCode.custom,
|
|
6531
|
+
path: ["style", "providerOptions", "comfyui"],
|
|
6532
|
+
message: "must match workflow metadata and use {{recipeDir}} for workflowFile"
|
|
6533
|
+
});
|
|
6534
|
+
}
|
|
6535
|
+
}
|
|
6536
|
+
});
|
|
6537
|
+
function canonical3(value) {
|
|
6538
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
6539
|
+
if (typeof value === "number") {
|
|
6540
|
+
if (!Number.isFinite(value)) throw new Error("Recipe numbers must be finite.");
|
|
6541
|
+
return value;
|
|
6542
|
+
}
|
|
6543
|
+
if (Array.isArray(value)) return value.map(canonical3);
|
|
6544
|
+
if (typeof value === "object") {
|
|
6545
|
+
const sorted = {};
|
|
6546
|
+
for (const key of Object.keys(value).sort()) {
|
|
6547
|
+
const item = value[key];
|
|
6548
|
+
if (item !== void 0) sorted[key] = canonical3(item);
|
|
6549
|
+
}
|
|
6550
|
+
return sorted;
|
|
6551
|
+
}
|
|
6552
|
+
throw new Error(`Recipe metadata cannot contain ${typeof value} values.`);
|
|
6553
|
+
}
|
|
6554
|
+
function recipeDigest(recipe) {
|
|
6555
|
+
const { integrity: _integrity, ...payload } = recipe;
|
|
6556
|
+
return sha256(JSON.stringify(canonical3(payload)));
|
|
6557
|
+
}
|
|
6558
|
+
function message3(error) {
|
|
6559
|
+
return error instanceof Error ? error.message : String(error);
|
|
6560
|
+
}
|
|
6561
|
+
async function readRecipeFile(recipePath, bundled) {
|
|
6562
|
+
const absolute = path20.resolve(recipePath);
|
|
6563
|
+
let raw;
|
|
6564
|
+
try {
|
|
6565
|
+
raw = JSON.parse(await readFile15(absolute, "utf8"));
|
|
6566
|
+
} catch (error) {
|
|
6567
|
+
throw new Error(`Could not read recipe ${absolute}: ${message3(error)}`, { cause: error });
|
|
6568
|
+
}
|
|
6569
|
+
const parsed = RecipeSchema.safeParse(raw);
|
|
6570
|
+
if (!parsed.success) {
|
|
6571
|
+
throw new Error(
|
|
6572
|
+
`Invalid recipe ${absolute}:
|
|
6573
|
+
` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
|
|
6574
|
+
);
|
|
6575
|
+
}
|
|
6576
|
+
return { recipe: parsed.data, path: absolute, dir: path20.dirname(absolute), bundled };
|
|
6577
|
+
}
|
|
6578
|
+
async function walkRecipeFiles(root) {
|
|
6579
|
+
if (!existsSync15(root)) return [];
|
|
6580
|
+
const out = [];
|
|
6581
|
+
for (const entry of await readdir3(root, { withFileTypes: true })) {
|
|
6582
|
+
const resolved = path20.join(root, entry.name);
|
|
6583
|
+
if (entry.isDirectory()) out.push(...await walkRecipeFiles(resolved));
|
|
6584
|
+
else if (entry.isFile() && entry.name === RECIPE_FILE) out.push(resolved);
|
|
6585
|
+
}
|
|
6586
|
+
return out;
|
|
6587
|
+
}
|
|
6588
|
+
function bundledRecipeRoot() {
|
|
6589
|
+
return fileURLToPath(new URL("../recipes/", import.meta.url));
|
|
6590
|
+
}
|
|
6591
|
+
async function listBundledRecipes(root = bundledRecipeRoot()) {
|
|
6592
|
+
const recipes = await Promise.all(
|
|
6593
|
+
(await walkRecipeFiles(path20.resolve(root))).map((file) => readRecipeFile(file, true))
|
|
6594
|
+
);
|
|
6595
|
+
return recipes.sort(
|
|
6596
|
+
(a, b) => a.recipe.id.localeCompare(b.recipe.id) || compareVersions(b.recipe.version, a.recipe.version)
|
|
6597
|
+
);
|
|
6598
|
+
}
|
|
6599
|
+
function compareVersions(a, b) {
|
|
6600
|
+
const av = a.split(".").map(Number);
|
|
6601
|
+
const bv = b.split(".").map(Number);
|
|
6602
|
+
for (let i = 0; i < 3; i++) {
|
|
6603
|
+
const difference = av[i] - bv[i];
|
|
6604
|
+
if (difference) return difference;
|
|
6605
|
+
}
|
|
6606
|
+
return 0;
|
|
6607
|
+
}
|
|
6608
|
+
function parseSelector(selector) {
|
|
6609
|
+
const match = selector.match(/^([^@]+?)(?:@(\d+\.\d+\.\d+))?$/);
|
|
6610
|
+
if (!match || !RECIPE_ID_RE.test(match[1])) {
|
|
6611
|
+
throw new Error(`Invalid recipe selector "${selector}". Expected provider/name or provider/name@x.y.z.`);
|
|
6612
|
+
}
|
|
6613
|
+
return { id: match[1], version: match[2] };
|
|
6614
|
+
}
|
|
6615
|
+
async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
6616
|
+
const local = path20.resolve(target);
|
|
6617
|
+
if (existsSync15(local)) {
|
|
6618
|
+
const recipePath = path20.basename(local) === RECIPE_FILE ? local : path20.join(local, RECIPE_FILE);
|
|
6619
|
+
return readRecipeFile(recipePath, false);
|
|
6620
|
+
}
|
|
6621
|
+
if (target.startsWith(".") || target.startsWith("/") || target.includes("\\")) {
|
|
6622
|
+
throw new Error(`Recipe path not found: ${local}`);
|
|
6623
|
+
}
|
|
6624
|
+
const selector = parseSelector(target);
|
|
6625
|
+
const matches = (await listBundledRecipes(bundledRoot)).filter(
|
|
6626
|
+
(entry) => entry.recipe.id === selector.id && (selector.version === void 0 || entry.recipe.version === selector.version)
|
|
6627
|
+
);
|
|
6628
|
+
if (!matches.length) throw new Error(`Bundled recipe not found: ${target}`);
|
|
6629
|
+
return matches[0];
|
|
6630
|
+
}
|
|
6631
|
+
async function verifyFile(root, file) {
|
|
6632
|
+
const absolute = path20.resolve(root, ...file.path.split("/"));
|
|
6633
|
+
if (!existsSync15(absolute)) {
|
|
6634
|
+
return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
|
|
6635
|
+
}
|
|
6636
|
+
const actualSha256 = await sha256File(absolute);
|
|
6637
|
+
return {
|
|
6638
|
+
path: file.path,
|
|
6639
|
+
expectedSha256: file.sha256,
|
|
6640
|
+
actualSha256,
|
|
6641
|
+
status: actualSha256 === file.sha256 ? "ok" : "mismatch"
|
|
6642
|
+
};
|
|
6643
|
+
}
|
|
6644
|
+
async function verifyRecipe(target, options = {}) {
|
|
6645
|
+
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
6646
|
+
const actualIntegrity = recipeDigest(loaded.recipe);
|
|
6647
|
+
const files = await Promise.all(loaded.recipe.files.map((file) => verifyFile(loaded.dir, file)));
|
|
6648
|
+
const modelRoot = options.modelRoot ? path20.resolve(options.modelRoot) : null;
|
|
6649
|
+
const models = modelRoot ? await Promise.all(loaded.recipe.models.map(async (model) => ({
|
|
6650
|
+
...await verifyFile(modelRoot, model),
|
|
6651
|
+
source: model.source,
|
|
6652
|
+
license: model.license
|
|
6653
|
+
}))) : loaded.recipe.models.map((model) => ({
|
|
6654
|
+
path: model.path,
|
|
6655
|
+
expectedSha256: model.sha256,
|
|
6656
|
+
actualSha256: null,
|
|
6657
|
+
source: model.source,
|
|
6658
|
+
license: model.license,
|
|
6659
|
+
status: "unchecked"
|
|
6660
|
+
}));
|
|
6661
|
+
const integrityStatus = actualIntegrity === loaded.recipe.integrity.digest ? "ok" : "mismatch";
|
|
6662
|
+
return {
|
|
6663
|
+
version: 1,
|
|
6664
|
+
ok: integrityStatus === "ok" && files.every((file) => file.status === "ok") && (!modelRoot || models.every((model) => model.status === "ok")),
|
|
6665
|
+
recipe: {
|
|
6666
|
+
id: loaded.recipe.id,
|
|
6667
|
+
version: loaded.recipe.version,
|
|
6668
|
+
path: loaded.path,
|
|
6669
|
+
bundled: loaded.bundled
|
|
6670
|
+
},
|
|
6671
|
+
integrity: {
|
|
6672
|
+
expectedSha256: loaded.recipe.integrity.digest,
|
|
6673
|
+
actualSha256: actualIntegrity,
|
|
6674
|
+
status: integrityStatus
|
|
6675
|
+
},
|
|
6676
|
+
files,
|
|
6677
|
+
models,
|
|
6678
|
+
modelRoot
|
|
6679
|
+
};
|
|
6680
|
+
}
|
|
6681
|
+
function renderStyle(style, destination, cwd) {
|
|
6682
|
+
const portableDir = path20.relative(cwd, destination).split(path20.sep).join("/") || ".";
|
|
6683
|
+
const replace = (value) => {
|
|
6684
|
+
if (typeof value === "string") return value.replaceAll(RECIPE_DIR_TOKEN, portableDir);
|
|
6685
|
+
if (Array.isArray(value)) return value.map(replace);
|
|
6686
|
+
if (value && typeof value === "object") {
|
|
6687
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, replace(item)]));
|
|
6688
|
+
}
|
|
6689
|
+
return value;
|
|
6690
|
+
};
|
|
6691
|
+
return replace(style);
|
|
6692
|
+
}
|
|
6693
|
+
async function installRecipe(target, options = {}) {
|
|
6694
|
+
const cwd = path20.resolve(options.cwd ?? process.cwd());
|
|
6695
|
+
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
6696
|
+
const verification = await verifyRecipe(loaded.path);
|
|
6697
|
+
if (!verification.ok) {
|
|
6698
|
+
throw new Error(`Recipe ${loaded.recipe.id}@${loaded.recipe.version} failed integrity verification.`);
|
|
6699
|
+
}
|
|
6700
|
+
const destination = options.out ? path20.resolve(cwd, options.out) : path20.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
|
|
6701
|
+
const sources = [
|
|
6702
|
+
{ path: RECIPE_FILE, source: loaded.path },
|
|
6703
|
+
...loaded.recipe.files.map((file) => ({ path: file.path, source: path20.join(loaded.dir, ...file.path.split("/")) }))
|
|
6704
|
+
];
|
|
6705
|
+
const files = await Promise.all(sources.map(async (file) => ({
|
|
6706
|
+
path: path20.join(destination, ...file.path.split("/")),
|
|
6707
|
+
data: await readFile15(file.source)
|
|
6708
|
+
})));
|
|
6709
|
+
if (!options.force) {
|
|
6710
|
+
for (const file of files) {
|
|
6711
|
+
if (!existsSync15(file.path)) continue;
|
|
6712
|
+
const current = await readFile15(file.path);
|
|
6713
|
+
if (!current.equals(file.data)) {
|
|
6714
|
+
throw new Error(
|
|
6715
|
+
`Recipe destination has local changes: ${file.path}. Choose another --out or pass --force to replace declared recipe files.`
|
|
6716
|
+
);
|
|
6717
|
+
}
|
|
6718
|
+
}
|
|
6719
|
+
}
|
|
6720
|
+
const written = await writeArtifactBundle(files);
|
|
6721
|
+
return {
|
|
6722
|
+
recipe: loaded.recipe,
|
|
6723
|
+
destination,
|
|
6724
|
+
changed: written.changed,
|
|
6725
|
+
unchanged: written.unchanged,
|
|
6726
|
+
styleId: loaded.recipe.styleId,
|
|
6727
|
+
style: renderStyle(loaded.recipe.style, destination, cwd)
|
|
6728
|
+
};
|
|
6729
|
+
}
|
|
6730
|
+
|
|
6731
|
+
// src/pipeline/quality-regression.ts
|
|
6732
|
+
import path21 from "path";
|
|
6733
|
+
import { existsSync as existsSync16 } from "fs";
|
|
6734
|
+
import { readFile as readFile16 } from "fs/promises";
|
|
6735
|
+
import { z as z6 } from "zod";
|
|
6736
|
+
var SHA256_RE2 = /^[0-9a-f]{64}$/;
|
|
6737
|
+
var HEX_RE = /^#[0-9a-f]{6}$/;
|
|
6738
|
+
var MAX_REDMEAN_DISTANCE = 765;
|
|
6739
|
+
var QualityPathSchema = z6.string().min(1).refine(
|
|
6740
|
+
(value) => !path21.posix.isAbsolute(value) && !value.includes("\\"),
|
|
6741
|
+
"expected a portable relative path"
|
|
6742
|
+
);
|
|
6743
|
+
var DEFAULT_QUALITY_TOLERANCES = {
|
|
6744
|
+
requireExactHash: false,
|
|
6745
|
+
maxColorCountIncrease: 0,
|
|
6746
|
+
maxNewColors: 0,
|
|
6747
|
+
maxTransparencyDelta: 0.01,
|
|
6748
|
+
maxPartialAlphaIncrease: 0,
|
|
6749
|
+
maxEdgeDensityDelta: 0.03,
|
|
6750
|
+
maxEdgeContrastDrop: 0.03,
|
|
6751
|
+
maxIsolatedPixelIncrease: 5e-3
|
|
6752
|
+
};
|
|
6753
|
+
var QualityToleranceSchema = z6.object({
|
|
6754
|
+
requireExactHash: z6.boolean(),
|
|
6755
|
+
maxColorCountIncrease: z6.number().int().min(0),
|
|
6756
|
+
maxNewColors: z6.number().int().min(0),
|
|
6757
|
+
maxTransparencyDelta: z6.number().min(0).max(1),
|
|
6758
|
+
maxPartialAlphaIncrease: z6.number().min(0).max(1),
|
|
6759
|
+
maxEdgeDensityDelta: z6.number().min(0).max(1),
|
|
6760
|
+
maxEdgeContrastDrop: z6.number().min(0).max(1),
|
|
6761
|
+
maxIsolatedPixelIncrease: z6.number().min(0).max(1)
|
|
6762
|
+
}).strict();
|
|
6763
|
+
var ImageQualityMetricsSchema = z6.object({
|
|
6764
|
+
sha256: z6.string().regex(SHA256_RE2),
|
|
6765
|
+
width: z6.number().int().min(1),
|
|
6766
|
+
height: z6.number().int().min(1),
|
|
6767
|
+
colorCount: z6.number().int().min(0),
|
|
6768
|
+
colors: z6.array(z6.string().regex(HEX_RE)),
|
|
6769
|
+
transparency: z6.number().min(0).max(1),
|
|
6770
|
+
partialAlpha: z6.number().min(0).max(1),
|
|
6771
|
+
edgeDensity: z6.number().min(0).max(1),
|
|
6772
|
+
meanEdgeContrast: z6.number().min(0).max(1),
|
|
6773
|
+
isolatedPixelRatio: z6.number().min(0).max(1)
|
|
6774
|
+
}).strict();
|
|
6775
|
+
var QualityBaselineCaseSchema = z6.object({
|
|
6776
|
+
id: z6.string().regex(/^[a-z0-9][a-z0-9/_-]*$/),
|
|
6777
|
+
file: QualityPathSchema,
|
|
6778
|
+
record: z6.object({
|
|
6779
|
+
path: QualityPathSchema,
|
|
6780
|
+
sha256: z6.string().regex(SHA256_RE2)
|
|
6781
|
+
}).strict().optional(),
|
|
6782
|
+
expected: ImageQualityMetricsSchema,
|
|
6783
|
+
tolerances: QualityToleranceSchema
|
|
6784
|
+
}).strict();
|
|
6785
|
+
var QualityBaselineSchema = z6.object({
|
|
6786
|
+
$schema: z6.string().url().optional(),
|
|
6787
|
+
format: z6.literal("pixelkiln-quality-baseline"),
|
|
6788
|
+
schemaVersion: z6.literal(1),
|
|
6789
|
+
cases: z6.array(QualityBaselineCaseSchema).min(1)
|
|
6790
|
+
}).strict().superRefine((baseline, context) => {
|
|
6791
|
+
const ids = baseline.cases.map((entry) => entry.id);
|
|
6792
|
+
if (new Set(ids).size !== ids.length) {
|
|
6793
|
+
context.addIssue({ code: z6.ZodIssueCode.custom, path: ["cases"], message: "duplicate case id" });
|
|
6794
|
+
}
|
|
6795
|
+
});
|
|
6796
|
+
var QualityInputSchema = z6.object({
|
|
6797
|
+
id: z6.string().regex(/^[a-z0-9][a-z0-9/_-]*$/),
|
|
6798
|
+
path: z6.string().min(1),
|
|
6799
|
+
record: z6.string().min(1).optional(),
|
|
6800
|
+
tolerances: QualityToleranceSchema.partial().optional()
|
|
6801
|
+
}).strict();
|
|
6802
|
+
function message4(error) {
|
|
6803
|
+
return error instanceof Error ? error.message : String(error);
|
|
6804
|
+
}
|
|
6805
|
+
function round(value) {
|
|
6806
|
+
return Number(value.toFixed(6));
|
|
6807
|
+
}
|
|
6808
|
+
function pixelKey(pixels, offset) {
|
|
6809
|
+
const alpha = pixels[offset + 3];
|
|
6810
|
+
if (alpha === 0) return 0;
|
|
6811
|
+
return (pixels[offset] << 24 >>> 0 | pixels[offset + 1] << 16 | pixels[offset + 2] << 8 | alpha) >>> 0;
|
|
6812
|
+
}
|
|
6813
|
+
function edgeContrast(pixels, a, b) {
|
|
6814
|
+
const alphaA = pixels[a + 3];
|
|
6815
|
+
const alphaB = pixels[b + 3];
|
|
6816
|
+
const alphaDistance = Math.abs(alphaA - alphaB) / 255;
|
|
6817
|
+
if (alphaA === 0 || alphaB === 0) return alphaDistance;
|
|
6818
|
+
const rgbDistance = colorDistance(
|
|
6819
|
+
{ r: pixels[a], g: pixels[a + 1], b: pixels[a + 2] },
|
|
6820
|
+
{ r: pixels[b], g: pixels[b + 1], b: pixels[b + 2] }
|
|
6821
|
+
) / MAX_REDMEAN_DISTANCE;
|
|
6822
|
+
return Math.min(1, Math.max(alphaDistance, rgbDistance));
|
|
6823
|
+
}
|
|
6824
|
+
function measureDecodedImageQuality(png, digest2) {
|
|
6825
|
+
const colors = /* @__PURE__ */ new Set();
|
|
6826
|
+
let transparent = 0;
|
|
6827
|
+
let partialAlpha = 0;
|
|
6828
|
+
let visible = 0;
|
|
6829
|
+
let isolated = 0;
|
|
6830
|
+
for (let offset = 0; offset < png.pixels.length; offset += 4) {
|
|
6831
|
+
const alpha = png.pixels[offset + 3];
|
|
6832
|
+
if (alpha === 0) {
|
|
6833
|
+
transparent++;
|
|
6834
|
+
continue;
|
|
6835
|
+
}
|
|
6836
|
+
visible++;
|
|
6837
|
+
if (alpha < 255) partialAlpha++;
|
|
6838
|
+
colors.add(png.pixels[offset] << 16 | png.pixels[offset + 1] << 8 | png.pixels[offset + 2]);
|
|
6839
|
+
const index = offset / 4;
|
|
6840
|
+
const x = index % png.width;
|
|
6841
|
+
const y = Math.floor(index / png.width);
|
|
6842
|
+
const key = pixelKey(png.pixels, offset);
|
|
6843
|
+
const hasSameNeighbor = x > 0 && pixelKey(png.pixels, offset - 4) === key || x + 1 < png.width && pixelKey(png.pixels, offset + 4) === key || y > 0 && pixelKey(png.pixels, offset - png.width * 4) === key || y + 1 < png.height && pixelKey(png.pixels, offset + png.width * 4) === key;
|
|
6844
|
+
if (!hasSameNeighbor) {
|
|
6845
|
+
isolated++;
|
|
6846
|
+
}
|
|
6847
|
+
}
|
|
6848
|
+
let neighborPairs = 0;
|
|
6849
|
+
let changedPairs = 0;
|
|
6850
|
+
let contrastTotal = 0;
|
|
6851
|
+
for (let y = 0; y < png.height; y++) {
|
|
6852
|
+
for (let x = 0; x < png.width; x++) {
|
|
6853
|
+
const offset = (y * png.width + x) * 4;
|
|
6854
|
+
if (x + 1 < png.width) {
|
|
6855
|
+
const neighbor = offset + 4;
|
|
6856
|
+
neighborPairs++;
|
|
6857
|
+
if (pixelKey(png.pixels, offset) !== pixelKey(png.pixels, neighbor)) {
|
|
6858
|
+
changedPairs++;
|
|
6859
|
+
contrastTotal += edgeContrast(png.pixels, offset, neighbor);
|
|
6860
|
+
}
|
|
6861
|
+
}
|
|
6862
|
+
if (y + 1 < png.height) {
|
|
6863
|
+
const neighbor = offset + png.width * 4;
|
|
6864
|
+
neighborPairs++;
|
|
6865
|
+
if (pixelKey(png.pixels, offset) !== pixelKey(png.pixels, neighbor)) {
|
|
6866
|
+
changedPairs++;
|
|
6867
|
+
contrastTotal += edgeContrast(png.pixels, offset, neighbor);
|
|
6868
|
+
}
|
|
6869
|
+
}
|
|
6870
|
+
}
|
|
6871
|
+
}
|
|
6872
|
+
const total = png.width * png.height;
|
|
6873
|
+
return {
|
|
6874
|
+
sha256: digest2,
|
|
6875
|
+
width: png.width,
|
|
6876
|
+
height: png.height,
|
|
6877
|
+
colorCount: colors.size,
|
|
6878
|
+
colors: [...colors].sort((a, b) => a - b).map((color) => `#${color.toString(16).padStart(6, "0")}`),
|
|
6879
|
+
transparency: round(total ? transparent / total : 0),
|
|
6880
|
+
partialAlpha: round(total ? partialAlpha / total : 0),
|
|
6881
|
+
edgeDensity: round(neighborPairs ? changedPairs / neighborPairs : 0),
|
|
6882
|
+
meanEdgeContrast: round(changedPairs ? contrastTotal / changedPairs : 0),
|
|
6883
|
+
isolatedPixelRatio: round(visible ? isolated / visible : 0)
|
|
6884
|
+
};
|
|
6885
|
+
}
|
|
6886
|
+
async function measureImageQuality(file) {
|
|
6887
|
+
const absolute = path21.resolve(file);
|
|
6888
|
+
let bytes;
|
|
6889
|
+
try {
|
|
6890
|
+
bytes = await readFile16(absolute);
|
|
6891
|
+
} catch (error) {
|
|
6892
|
+
throw new Error(`Cannot read quality image ${absolute}: ${message4(error)}`, { cause: error });
|
|
6893
|
+
}
|
|
6894
|
+
try {
|
|
6895
|
+
return measureDecodedImageQuality(decodePng(bytes), sha256(bytes));
|
|
6896
|
+
} catch (error) {
|
|
6897
|
+
throw new Error(`Cannot measure quality image ${absolute}: ${message4(error)}`, { cause: error });
|
|
6898
|
+
}
|
|
6899
|
+
}
|
|
6900
|
+
function resolveQualityInputs(raw, inputsFilePath) {
|
|
6901
|
+
const parsed = z6.array(QualityInputSchema).min(1).safeParse(raw);
|
|
6902
|
+
if (!parsed.success) {
|
|
6903
|
+
throw new Error(
|
|
6904
|
+
"--inputs must be a non-empty JSON array of quality cases:\n" + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
|
|
6905
|
+
);
|
|
6906
|
+
}
|
|
6907
|
+
const ids = parsed.data.map((entry) => entry.id);
|
|
6908
|
+
if (new Set(ids).size !== ids.length) throw new Error("--inputs contains a duplicate quality case id.");
|
|
6909
|
+
const root = path21.dirname(path21.resolve(inputsFilePath));
|
|
6910
|
+
return parsed.data.map((entry) => ({
|
|
6911
|
+
id: entry.id,
|
|
6912
|
+
file: path21.resolve(root, entry.path),
|
|
6913
|
+
...entry.record ? { record: path21.resolve(root, entry.record) } : {},
|
|
6914
|
+
tolerances: QualityToleranceSchema.parse({
|
|
6915
|
+
...DEFAULT_QUALITY_TOLERANCES,
|
|
6916
|
+
...entry.tolerances
|
|
6917
|
+
})
|
|
6918
|
+
}));
|
|
6919
|
+
}
|
|
6920
|
+
function portableRelative2(from, to) {
|
|
6921
|
+
return path21.relative(from, path21.resolve(to)).split(path21.sep).join("/") || ".";
|
|
6922
|
+
}
|
|
6923
|
+
async function verifyRecordForImage(recordPath, imagePath) {
|
|
6924
|
+
const verification = await verifyArtifactBundle(recordPath);
|
|
6925
|
+
if (!verification.current) {
|
|
6926
|
+
const changed = [...verification.changedSources, ...verification.changedOutputs];
|
|
6927
|
+
throw new Error(
|
|
6928
|
+
`Quality record is not current: ${recordPath}` + (changed.length ? ` (${changed.join(", ")})` : "")
|
|
6929
|
+
);
|
|
6930
|
+
}
|
|
6931
|
+
const record = await readArtifactBundleManifest(recordPath);
|
|
6932
|
+
const options = record.options;
|
|
6933
|
+
if (record.kind !== "refine" || options === null || typeof options !== "object" || options.schema !== "pixelkiln-quality") {
|
|
6934
|
+
throw new Error(`Quality record ${recordPath} is not a PixelKiln refinement record.`);
|
|
6935
|
+
}
|
|
6936
|
+
const outputs = record.outputs.map((output) => path21.resolve(path21.dirname(recordPath), output.path));
|
|
6937
|
+
if (!outputs.includes(path21.resolve(imagePath))) {
|
|
6938
|
+
throw new Error(`Quality record ${recordPath} does not own ${imagePath}.`);
|
|
6939
|
+
}
|
|
6940
|
+
}
|
|
6941
|
+
async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
6942
|
+
if (!inputs.length) throw new Error("Cannot snapshot an empty quality baseline.");
|
|
6943
|
+
const absolute = path21.resolve(baselinePath);
|
|
6944
|
+
for (const input of inputs) {
|
|
6945
|
+
if (path21.resolve(input.file) === absolute || input.record && path21.resolve(input.record) === absolute) {
|
|
6946
|
+
throw new Error(`Quality baseline output would overwrite an input: ${absolute}.`);
|
|
6947
|
+
}
|
|
6948
|
+
}
|
|
6949
|
+
const root = path21.dirname(absolute);
|
|
6950
|
+
const cases = [];
|
|
6951
|
+
for (const input of [...inputs].sort((a, b) => a.id.localeCompare(b.id))) {
|
|
6952
|
+
const expected = await measureImageQuality(input.file);
|
|
6953
|
+
let record;
|
|
6954
|
+
if (input.record) {
|
|
6955
|
+
await verifyRecordForImage(input.record, input.file);
|
|
6956
|
+
record = {
|
|
6957
|
+
path: portableRelative2(root, input.record),
|
|
6958
|
+
sha256: await sha256File(input.record)
|
|
6959
|
+
};
|
|
6960
|
+
}
|
|
6961
|
+
cases.push({
|
|
6962
|
+
id: input.id,
|
|
6963
|
+
file: portableRelative2(root, input.file),
|
|
6964
|
+
...record ? { record } : {},
|
|
6965
|
+
expected,
|
|
6966
|
+
tolerances: input.tolerances
|
|
6967
|
+
});
|
|
6968
|
+
}
|
|
6969
|
+
const baseline = QualityBaselineSchema.parse({
|
|
6970
|
+
$schema: "https://unpkg.com/pixelkiln/schema/quality-baseline.schema.json",
|
|
6971
|
+
format: "pixelkiln-quality-baseline",
|
|
6972
|
+
schemaVersion: 1,
|
|
6973
|
+
cases
|
|
6974
|
+
});
|
|
6975
|
+
const data = Buffer.from(JSON.stringify(baseline, null, 2) + "\n");
|
|
6976
|
+
if (existsSync16(absolute) && !options.force) {
|
|
6977
|
+
const current = await readFile16(absolute);
|
|
6978
|
+
if (!current.equals(data)) {
|
|
6979
|
+
throw new Error(`Quality baseline already exists with different content: ${absolute}. Pass --force to replace it.`);
|
|
6980
|
+
}
|
|
6981
|
+
}
|
|
6982
|
+
const result = await writeArtifactBundle([{ path: absolute, data }]);
|
|
6983
|
+
return { baseline, path: absolute, changed: result.changed.length > 0 };
|
|
6984
|
+
}
|
|
6985
|
+
async function readQualityBaseline(baselinePath) {
|
|
6986
|
+
const absolute = path21.resolve(baselinePath);
|
|
6987
|
+
let raw;
|
|
6988
|
+
try {
|
|
6989
|
+
raw = JSON.parse(await readFile16(absolute, "utf8"));
|
|
6990
|
+
} catch (error) {
|
|
6991
|
+
throw new Error(`Could not read quality baseline ${absolute}: ${message4(error)}`, { cause: error });
|
|
6992
|
+
}
|
|
6993
|
+
const parsed = QualityBaselineSchema.safeParse(raw);
|
|
6994
|
+
if (!parsed.success) {
|
|
6995
|
+
throw new Error(
|
|
6996
|
+
`Invalid quality baseline ${absolute}:
|
|
6997
|
+
` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
|
|
6998
|
+
);
|
|
6999
|
+
}
|
|
7000
|
+
return parsed.data;
|
|
7001
|
+
}
|
|
7002
|
+
function regressionViolations(expected, actual, tolerances) {
|
|
7003
|
+
const violations = [];
|
|
7004
|
+
const warnings = [];
|
|
7005
|
+
const changed = actual.sha256 !== expected.sha256;
|
|
7006
|
+
if (expected.width !== actual.width || expected.height !== actual.height) {
|
|
7007
|
+
violations.push(
|
|
7008
|
+
`dimensions changed from ${expected.width}x${expected.height} to ${actual.width}x${actual.height}`
|
|
7009
|
+
);
|
|
7010
|
+
}
|
|
7011
|
+
if (changed && tolerances.requireExactHash) violations.push("image hash changed");
|
|
7012
|
+
else if (changed) warnings.push("image hash changed; metric tolerances decide this case");
|
|
7013
|
+
const colorIncrease = actual.colorCount - expected.colorCount;
|
|
7014
|
+
if (colorIncrease > tolerances.maxColorCountIncrease) {
|
|
7015
|
+
violations.push(
|
|
7016
|
+
`color count increased by ${colorIncrease} (allowed ${tolerances.maxColorCountIncrease})`
|
|
7017
|
+
);
|
|
7018
|
+
}
|
|
7019
|
+
const expectedColors = new Set(expected.colors);
|
|
7020
|
+
const newColors = actual.colors.filter((color) => !expectedColors.has(color));
|
|
7021
|
+
if (newColors.length > tolerances.maxNewColors) {
|
|
7022
|
+
violations.push(
|
|
7023
|
+
`${newColors.length} new color(s) exceed ${tolerances.maxNewColors}: ${newColors.slice(0, 8).join(", ")}`
|
|
7024
|
+
);
|
|
7025
|
+
}
|
|
7026
|
+
const transparencyDelta = Math.abs(actual.transparency - expected.transparency);
|
|
7027
|
+
if (transparencyDelta > tolerances.maxTransparencyDelta) {
|
|
7028
|
+
violations.push(
|
|
7029
|
+
`transparency changed by ${(transparencyDelta * 100).toFixed(2)} points (allowed ${(tolerances.maxTransparencyDelta * 100).toFixed(2)})`
|
|
7030
|
+
);
|
|
7031
|
+
}
|
|
7032
|
+
const partialAlphaIncrease = actual.partialAlpha - expected.partialAlpha;
|
|
7033
|
+
if (partialAlphaIncrease > tolerances.maxPartialAlphaIncrease) {
|
|
7034
|
+
violations.push(
|
|
7035
|
+
`partial-alpha pixels increased by ${(partialAlphaIncrease * 100).toFixed(2)} points (allowed ${(tolerances.maxPartialAlphaIncrease * 100).toFixed(2)})`
|
|
7036
|
+
);
|
|
7037
|
+
}
|
|
7038
|
+
const edgeDensityDelta = Math.abs(actual.edgeDensity - expected.edgeDensity);
|
|
7039
|
+
if (edgeDensityDelta > tolerances.maxEdgeDensityDelta) {
|
|
7040
|
+
violations.push(
|
|
7041
|
+
`edge density changed by ${(edgeDensityDelta * 100).toFixed(2)} points (allowed ${(tolerances.maxEdgeDensityDelta * 100).toFixed(2)})`
|
|
7042
|
+
);
|
|
7043
|
+
}
|
|
7044
|
+
const edgeContrastDrop = expected.meanEdgeContrast - actual.meanEdgeContrast;
|
|
7045
|
+
if (edgeContrastDrop > tolerances.maxEdgeContrastDrop) {
|
|
7046
|
+
violations.push(
|
|
7047
|
+
`mean edge contrast dropped by ${(edgeContrastDrop * 100).toFixed(2)} points (allowed ${(tolerances.maxEdgeContrastDrop * 100).toFixed(2)})`
|
|
7048
|
+
);
|
|
7049
|
+
}
|
|
7050
|
+
const isolatedIncrease = actual.isolatedPixelRatio - expected.isolatedPixelRatio;
|
|
7051
|
+
if (isolatedIncrease > tolerances.maxIsolatedPixelIncrease) {
|
|
7052
|
+
violations.push(
|
|
7053
|
+
`isolated-pixel ratio increased by ${(isolatedIncrease * 100).toFixed(2)} points (allowed ${(tolerances.maxIsolatedPixelIncrease * 100).toFixed(2)})`
|
|
7054
|
+
);
|
|
7055
|
+
}
|
|
7056
|
+
return { violations, warnings };
|
|
7057
|
+
}
|
|
7058
|
+
async function checkQualityBaseline(baselinePath) {
|
|
7059
|
+
const absolute = path21.resolve(baselinePath);
|
|
7060
|
+
const baseline = await readQualityBaseline(absolute);
|
|
7061
|
+
const root = path21.dirname(absolute);
|
|
7062
|
+
const cases = [];
|
|
7063
|
+
for (const entry of baseline.cases) {
|
|
7064
|
+
const file = path21.resolve(root, entry.file);
|
|
7065
|
+
let actual = null;
|
|
7066
|
+
const violations = [];
|
|
7067
|
+
const warnings = [];
|
|
7068
|
+
try {
|
|
7069
|
+
actual = await measureImageQuality(file);
|
|
7070
|
+
const regression = regressionViolations(entry.expected, actual, entry.tolerances);
|
|
7071
|
+
violations.push(...regression.violations);
|
|
7072
|
+
warnings.push(...regression.warnings);
|
|
7073
|
+
} catch (error) {
|
|
7074
|
+
violations.push(message4(error));
|
|
7075
|
+
}
|
|
7076
|
+
if (entry.record) {
|
|
7077
|
+
const recordPath = path21.resolve(root, entry.record.path);
|
|
7078
|
+
try {
|
|
7079
|
+
const recordHash = await sha256File(recordPath);
|
|
7080
|
+
if (recordHash !== entry.record.sha256) violations.push("quality record hash changed");
|
|
7081
|
+
await verifyRecordForImage(recordPath, file);
|
|
7082
|
+
} catch (error) {
|
|
7083
|
+
violations.push(message4(error));
|
|
7084
|
+
}
|
|
7085
|
+
}
|
|
7086
|
+
cases.push({
|
|
7087
|
+
id: entry.id,
|
|
7088
|
+
file,
|
|
7089
|
+
status: violations.length ? "fail" : "pass",
|
|
7090
|
+
hashChanged: actual !== null && actual.sha256 !== entry.expected.sha256,
|
|
7091
|
+
expected: entry.expected,
|
|
7092
|
+
actual,
|
|
7093
|
+
violations,
|
|
7094
|
+
warnings
|
|
7095
|
+
});
|
|
7096
|
+
}
|
|
7097
|
+
const failed = cases.filter((entry) => entry.status === "fail").length;
|
|
7098
|
+
return {
|
|
7099
|
+
version: 1,
|
|
7100
|
+
safe: failed === 0,
|
|
7101
|
+
baseline: absolute,
|
|
7102
|
+
summary: {
|
|
7103
|
+
total: cases.length,
|
|
7104
|
+
passed: cases.length - failed,
|
|
7105
|
+
failed,
|
|
7106
|
+
changed: cases.filter((entry) => entry.hashChanged).length
|
|
7107
|
+
},
|
|
7108
|
+
cases
|
|
7109
|
+
};
|
|
7110
|
+
}
|
|
7111
|
+
|
|
6378
7112
|
// src/cli.ts
|
|
6379
7113
|
var log = (msg = "") => console.log(msg);
|
|
6380
7114
|
async function provenanceFile(id, file) {
|
|
6381
|
-
const absolute =
|
|
7115
|
+
const absolute = path22.resolve(file);
|
|
6382
7116
|
return {
|
|
6383
7117
|
id,
|
|
6384
7118
|
path: absolute,
|
|
6385
|
-
sha256:
|
|
7119
|
+
sha256: existsSync17(absolute) ? await sha256File(absolute) : null,
|
|
6386
7120
|
included: true
|
|
6387
7121
|
};
|
|
6388
7122
|
}
|
|
@@ -6415,7 +7149,8 @@ var VALUE_FLAGS = [
|
|
|
6415
7149
|
"--fixer-revision",
|
|
6416
7150
|
"--min-grid-confidence",
|
|
6417
7151
|
"--reviewer",
|
|
6418
|
-
"--note"
|
|
7152
|
+
"--note",
|
|
7153
|
+
"--model-root"
|
|
6419
7154
|
];
|
|
6420
7155
|
var BOOL_FLAGS = [
|
|
6421
7156
|
"--force",
|
|
@@ -6454,7 +7189,9 @@ var COMMANDS = [
|
|
|
6454
7189
|
"tag",
|
|
6455
7190
|
"balance",
|
|
6456
7191
|
"status",
|
|
7192
|
+
"quality",
|
|
6457
7193
|
"refine",
|
|
7194
|
+
"recipe",
|
|
6458
7195
|
"workspace",
|
|
6459
7196
|
"help",
|
|
6460
7197
|
"--help",
|
|
@@ -6464,6 +7201,8 @@ var COMMANDS = [
|
|
|
6464
7201
|
];
|
|
6465
7202
|
var WORKSPACE_SUBCOMMANDS = ["add", "remove", "list", "status", "claims"];
|
|
6466
7203
|
var REFINE_SUBCOMMANDS = ["run", "approve", "check"];
|
|
7204
|
+
var RECIPE_SUBCOMMANDS = ["list", "inspect", "install", "verify"];
|
|
7205
|
+
var QUALITY_SUBCOMMANDS = ["snapshot", "check"];
|
|
6467
7206
|
function parseArgs(argv) {
|
|
6468
7207
|
const [command = "help"] = argv;
|
|
6469
7208
|
if (!COMMANDS.includes(command)) {
|
|
@@ -6472,7 +7211,36 @@ function parseArgs(argv) {
|
|
|
6472
7211
|
let rest = argv.slice(1);
|
|
6473
7212
|
let subcommand;
|
|
6474
7213
|
let target;
|
|
6475
|
-
if (command === "
|
|
7214
|
+
if (command === "quality") {
|
|
7215
|
+
subcommand = rest[0];
|
|
7216
|
+
if (subcommand === void 0 || subcommand.startsWith("-")) {
|
|
7217
|
+
throw new Error(`quality needs a subcommand: ${QUALITY_SUBCOMMANDS.join(", ")}`);
|
|
7218
|
+
}
|
|
7219
|
+
if (!QUALITY_SUBCOMMANDS.includes(subcommand)) {
|
|
7220
|
+
throw new Error(
|
|
7221
|
+
`Unknown quality subcommand "${subcommand}". Known: ${QUALITY_SUBCOMMANDS.join(", ")}`
|
|
7222
|
+
);
|
|
7223
|
+
}
|
|
7224
|
+
rest = rest.slice(1);
|
|
7225
|
+
} else if (command === "recipe") {
|
|
7226
|
+
subcommand = rest[0];
|
|
7227
|
+
if (subcommand === void 0 || subcommand.startsWith("-")) {
|
|
7228
|
+
throw new Error(`recipe needs a subcommand: ${RECIPE_SUBCOMMANDS.join(", ")}`);
|
|
7229
|
+
}
|
|
7230
|
+
if (!RECIPE_SUBCOMMANDS.includes(subcommand)) {
|
|
7231
|
+
throw new Error(
|
|
7232
|
+
`Unknown recipe subcommand "${subcommand}". Known: ${RECIPE_SUBCOMMANDS.join(", ")}`
|
|
7233
|
+
);
|
|
7234
|
+
}
|
|
7235
|
+
rest = rest.slice(1);
|
|
7236
|
+
if (subcommand !== "list") {
|
|
7237
|
+
target = rest[0];
|
|
7238
|
+
if (target === void 0 || target.startsWith("-")) {
|
|
7239
|
+
throw new Error(`recipe ${subcommand} needs a recipe id, selector, or path.`);
|
|
7240
|
+
}
|
|
7241
|
+
rest = rest.slice(1);
|
|
7242
|
+
}
|
|
7243
|
+
} else if (command === "workspace") {
|
|
6476
7244
|
subcommand = rest[0];
|
|
6477
7245
|
if (subcommand === void 0 || subcommand.startsWith("-")) {
|
|
6478
7246
|
throw new Error(`workspace needs a subcommand: ${WORKSPACE_SUBCOMMANDS.join(", ")}`);
|
|
@@ -6583,7 +7351,7 @@ function parseArgs(argv) {
|
|
|
6583
7351
|
return {
|
|
6584
7352
|
command,
|
|
6585
7353
|
manifest,
|
|
6586
|
-
lock: get("--lock") ??
|
|
7354
|
+
lock: get("--lock") ?? path22.join(path22.dirname(path22.resolve(manifest)), "pixelkiln.lock.json"),
|
|
6587
7355
|
explicitLock: get("--lock"),
|
|
6588
7356
|
styles: list("--style"),
|
|
6589
7357
|
assets: list("--only"),
|
|
@@ -6620,6 +7388,7 @@ function parseArgs(argv) {
|
|
|
6620
7388
|
minGridConfidence: rawGridConfidence,
|
|
6621
7389
|
reviewer: get("--reviewer"),
|
|
6622
7390
|
note: get("--note"),
|
|
7391
|
+
modelRoot: get("--model-root"),
|
|
6623
7392
|
subcommand,
|
|
6624
7393
|
workspace: get("--workspace"),
|
|
6625
7394
|
target,
|
|
@@ -6648,6 +7417,8 @@ Commands
|
|
|
6648
7417
|
audit Measure how consistently a style's assets hold together. Offline.
|
|
6649
7418
|
refine Recover a native pixel grid, enforce a final palette, audit it,
|
|
6650
7419
|
and attach a verifiable human approval. run/approve/check. Offline.
|
|
7420
|
+
recipe List, inspect, install, or verify versioned workflow packs. Offline.
|
|
7421
|
+
quality Snapshot or check measurable image regressions. Offline.
|
|
6651
7422
|
cache Inspect local recovery/object-hash caches; optionally verify or prune.
|
|
6652
7423
|
pack Composite sprites into a PNG/atlas/provenance bundle. Offline.
|
|
6653
7424
|
mount Write a style's sprites into their declared cells of an existing
|
|
@@ -6665,7 +7436,7 @@ Commands
|
|
|
6665
7436
|
Options
|
|
6666
7437
|
--columns <n> pack/export: sprites or tiles per row (default: near-square)
|
|
6667
7438
|
--port <n> Local review-server port (default: choose a free port)
|
|
6668
|
-
--inputs <path> pack: JSON
|
|
7439
|
+
--inputs <path> pack/quality snapshot: JSON input list; needs --out
|
|
6669
7440
|
--format <format> export: generic (default), tiled, or godot
|
|
6670
7441
|
--output-role <r> pack: include only this output role (repeatable)
|
|
6671
7442
|
--primary-only pack: include only unambiguous primary/single outputs
|
|
@@ -6679,16 +7450,17 @@ Options
|
|
|
6679
7450
|
--min-grid-confidence <level> refine: high (default), medium, or low
|
|
6680
7451
|
--reviewer <name> refine approve: human reviewer recorded in provenance
|
|
6681
7452
|
--note <text> refine approve: optional review note
|
|
7453
|
+
--model-root <dir> recipe verify: also hash required local model files
|
|
6682
7454
|
--prune cache: remove invalid/unreferenced local cache data
|
|
6683
7455
|
--manifest <path> Default: pixelkiln.manifest.json
|
|
6684
7456
|
--lock <path> Default: pixelkiln.lock.json beside the manifest
|
|
6685
7457
|
--style a,b Restrict to these styles
|
|
6686
7458
|
--only id1,id2 Restrict to these asset ids
|
|
6687
7459
|
--budget <n> Refuse to spend more than n provider cost units
|
|
6688
|
-
--force Regenerate;
|
|
7460
|
+
--force Regenerate; also replace changed recipe files or quality baselines
|
|
6689
7461
|
--dry-run Never spend; doctor also skips provider connectivity
|
|
6690
7462
|
--all salvage --dry-run: list every unclaimed object, not just the first 30
|
|
6691
|
-
--json Machine-readable output where supported, including
|
|
7463
|
+
--json Machine-readable output where supported, including quality checks
|
|
6692
7464
|
--check plan/audit/cache: exit nonzero when the selected state is unsafe
|
|
6693
7465
|
--yes, -y Skip the confirmation prompt
|
|
6694
7466
|
--no-open Do not auto-open the browser during pick
|
|
@@ -6696,7 +7468,7 @@ Options
|
|
|
6696
7468
|
--claims a.json,b Other projects' lockfiles (salvage; required if account is shared)
|
|
6697
7469
|
--workspace <path> Workspace catalog (default: pixelkiln.workspace.json). Also
|
|
6698
7470
|
derives salvage's claim set instead of repeated --claims.
|
|
6699
|
-
--from <
|
|
7471
|
+
--from <path> Source for init/refine, or baseline for quality check
|
|
6700
7472
|
--write-prompts adopt: recover prompts into the manifest
|
|
6701
7473
|
|
|
6702
7474
|
Examples
|
|
@@ -6711,6 +7483,11 @@ Examples
|
|
|
6711
7483
|
pixelkiln refine --from source.png --out final.png --palette "#101820,#f2aa4c"
|
|
6712
7484
|
pixelkiln refine approve --from final.pixelkiln.json --reviewer "Your Name"
|
|
6713
7485
|
pixelkiln refine check --from final.pixelkiln.json
|
|
7486
|
+
pixelkiln recipe list
|
|
7487
|
+
pixelkiln recipe install comfyui/pixel-art-xl-environment
|
|
7488
|
+
pixelkiln recipe verify pixelkiln-recipes/comfyui/pixel-art-xl-environment/1.0.0 --model-root /path/to/ComfyUI/models
|
|
7489
|
+
pixelkiln quality snapshot --inputs quality-inputs.json --out pixelkiln.quality.json
|
|
7490
|
+
pixelkiln quality check --from pixelkiln.quality.json
|
|
6714
7491
|
pixelkiln workspace add ../other-game/pixelkiln.manifest.json
|
|
6715
7492
|
pixelkiln workspace status --json
|
|
6716
7493
|
pixelkiln salvage --workspace pixelkiln.workspace.json
|
|
@@ -6755,10 +7532,10 @@ async function confirm(question, auto) {
|
|
|
6755
7532
|
return answer === "y" || answer === "yes";
|
|
6756
7533
|
}
|
|
6757
7534
|
async function requireCompleteWorkspaceClaims(workspacePath) {
|
|
6758
|
-
if (!
|
|
7535
|
+
if (!existsSync17(workspacePath)) {
|
|
6759
7536
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
6760
7537
|
}
|
|
6761
|
-
const dir =
|
|
7538
|
+
const dir = path22.dirname(path22.resolve(workspacePath));
|
|
6762
7539
|
const ws = await loadWorkspace(workspacePath);
|
|
6763
7540
|
const diagnostics = validateWorkspace(ws, dir);
|
|
6764
7541
|
const errors = diagnostics.filter((d) => d.level === "error");
|
|
@@ -6779,11 +7556,146 @@ async function main() {
|
|
|
6779
7556
|
}
|
|
6780
7557
|
if (args.command === "--version" || args.command === "-v") {
|
|
6781
7558
|
const pkg = JSON.parse(
|
|
6782
|
-
await
|
|
7559
|
+
await readFile17(new URL("../package.json", import.meta.url), "utf8")
|
|
6783
7560
|
);
|
|
6784
7561
|
log(`${pkg.name} ${pkg.version}`);
|
|
6785
7562
|
return;
|
|
6786
7563
|
}
|
|
7564
|
+
if (args.command === "quality") {
|
|
7565
|
+
if (args.subcommand === "snapshot") {
|
|
7566
|
+
if (!args.inputs) throw new Error("quality snapshot needs --inputs <quality-inputs.json>.");
|
|
7567
|
+
if (!args.out) throw new Error("quality snapshot needs --out <pixelkiln.quality.json>.");
|
|
7568
|
+
let raw;
|
|
7569
|
+
try {
|
|
7570
|
+
raw = JSON.parse(await readFile17(path22.resolve(args.inputs), "utf8"));
|
|
7571
|
+
} catch (error) {
|
|
7572
|
+
throw new Error(
|
|
7573
|
+
`Could not read quality inputs ${path22.resolve(args.inputs)}: ${error instanceof Error ? error.message : String(error)}`,
|
|
7574
|
+
{ cause: error }
|
|
7575
|
+
);
|
|
7576
|
+
}
|
|
7577
|
+
const inputs = resolveQualityInputs(raw, args.inputs);
|
|
7578
|
+
if (path22.resolve(args.inputs) === path22.resolve(args.out)) {
|
|
7579
|
+
throw new Error("quality snapshot --out must not overwrite its --inputs file.");
|
|
7580
|
+
}
|
|
7581
|
+
const result = await snapshotQualityBaseline(inputs, args.out, { force: args.force });
|
|
7582
|
+
if (args.json) {
|
|
7583
|
+
log(JSON.stringify({
|
|
7584
|
+
version: 1,
|
|
7585
|
+
path: result.path,
|
|
7586
|
+
changed: result.changed,
|
|
7587
|
+
baseline: result.baseline
|
|
7588
|
+
}, null, 2));
|
|
7589
|
+
} else {
|
|
7590
|
+
log(
|
|
7591
|
+
` ${result.changed ? "wrote" : "unchanged"} ${path22.relative(process.cwd(), result.path)} (${result.baseline.cases.length} case(s))`
|
|
7592
|
+
);
|
|
7593
|
+
log(` Review the tolerances, commit the baseline, then run quality check in CI.`);
|
|
7594
|
+
}
|
|
7595
|
+
return;
|
|
7596
|
+
}
|
|
7597
|
+
if (!args.from) throw new Error("quality check needs --from <pixelkiln.quality.json>.");
|
|
7598
|
+
const report = await checkQualityBaseline(args.from);
|
|
7599
|
+
if (args.json) {
|
|
7600
|
+
log(JSON.stringify(report, null, 2));
|
|
7601
|
+
} else {
|
|
7602
|
+
for (const entry of report.cases) {
|
|
7603
|
+
log(` ${entry.status === "pass" ? "ok" : "ERROR"} ${entry.id}`);
|
|
7604
|
+
for (const violation of entry.violations) log(` ${violation}`);
|
|
7605
|
+
for (const warning of entry.warnings) log(` WARN ${warning}`);
|
|
7606
|
+
}
|
|
7607
|
+
log(
|
|
7608
|
+
`
|
|
7609
|
+
${report.summary.passed}/${report.summary.total} passed` + (report.summary.changed ? ` \xB7 ${report.summary.changed} image hash(es) changed` : "")
|
|
7610
|
+
);
|
|
7611
|
+
log(` Metrics catch structural regressions; they do not replace art review.`);
|
|
7612
|
+
}
|
|
7613
|
+
if (!report.safe) process.exitCode = 1;
|
|
7614
|
+
return;
|
|
7615
|
+
}
|
|
7616
|
+
if (args.command === "recipe") {
|
|
7617
|
+
if (args.subcommand === "list") {
|
|
7618
|
+
const recipes = await listBundledRecipes();
|
|
7619
|
+
if (args.json) {
|
|
7620
|
+
log(JSON.stringify({
|
|
7621
|
+
version: 1,
|
|
7622
|
+
recipes: recipes.map(({ recipe }) => ({
|
|
7623
|
+
id: recipe.id,
|
|
7624
|
+
version: recipe.version,
|
|
7625
|
+
provider: recipe.provider,
|
|
7626
|
+
summary: recipe.summary,
|
|
7627
|
+
selector: `${recipe.id}@${recipe.version}`
|
|
7628
|
+
}))
|
|
7629
|
+
}, null, 2));
|
|
7630
|
+
} else {
|
|
7631
|
+
log(` ${recipes.length} bundled recipe(s):`);
|
|
7632
|
+
for (const { recipe } of recipes) {
|
|
7633
|
+
log(` ${(recipe.id + "@" + recipe.version).padEnd(52)} ${recipe.summary}`);
|
|
7634
|
+
}
|
|
7635
|
+
}
|
|
7636
|
+
return;
|
|
7637
|
+
}
|
|
7638
|
+
const target = args.target;
|
|
7639
|
+
if (args.subcommand === "inspect") {
|
|
7640
|
+
const { recipe, path: recipePath, bundled } = await resolveRecipe(target);
|
|
7641
|
+
if (args.json) {
|
|
7642
|
+
log(JSON.stringify({ ...recipe, path: recipePath, bundled }, null, 2));
|
|
7643
|
+
} else {
|
|
7644
|
+
log(` ${recipe.id}@${recipe.version}`);
|
|
7645
|
+
log(` ${recipe.summary}`);
|
|
7646
|
+
log(` provider: ${recipe.provider} \xB7 style: ${recipe.styleId} \xB7 stage: ${recipe.quality.stage}`);
|
|
7647
|
+
log(
|
|
7648
|
+
` native target: ${recipe.quality.recommendedNativeSize.min}\u2013${recipe.quality.recommendedNativeSize.max}px \xB7 palette: ${recipe.quality.paletteColors.min}\u2013${recipe.quality.paletteColors.max} colors`
|
|
7649
|
+
);
|
|
7650
|
+
if (recipe.workflow) log(` workflow: ${recipe.workflow.path} \xB7 ${recipe.workflow.numImages} candidate(s)`);
|
|
7651
|
+
for (const model of recipe.models) log(` model: ${model.path} \xB7 ${model.license}`);
|
|
7652
|
+
log(` source: ${recipePath}${bundled ? " (bundled)" : ""}`);
|
|
7653
|
+
}
|
|
7654
|
+
return;
|
|
7655
|
+
}
|
|
7656
|
+
if (args.subcommand === "verify") {
|
|
7657
|
+
const report = await verifyRecipe(target, { modelRoot: args.modelRoot });
|
|
7658
|
+
if (args.json) {
|
|
7659
|
+
log(JSON.stringify(report, null, 2));
|
|
7660
|
+
} else {
|
|
7661
|
+
log(` ${report.ok ? "ok" : "ERROR"} ${report.recipe.id}@${report.recipe.version}`);
|
|
7662
|
+
log(` ${report.integrity.status.padEnd(9)} recipe metadata`);
|
|
7663
|
+
for (const file of report.files) log(` ${file.status.padEnd(9)} ${file.path}`);
|
|
7664
|
+
for (const model of report.models) log(` ${model.status.padEnd(9)} ${model.path}`);
|
|
7665
|
+
if (!report.modelRoot && report.models.length) {
|
|
7666
|
+
log(` models were not checked; pass --model-root <ComfyUI/models> to verify this workstation`);
|
|
7667
|
+
}
|
|
7668
|
+
}
|
|
7669
|
+
if (!report.ok) process.exitCode = 1;
|
|
7670
|
+
return;
|
|
7671
|
+
}
|
|
7672
|
+
if (args.subcommand === "install") {
|
|
7673
|
+
const result = await installRecipe(target, { out: args.out, force: args.force });
|
|
7674
|
+
if (args.json) {
|
|
7675
|
+
log(JSON.stringify({
|
|
7676
|
+
version: 1,
|
|
7677
|
+
recipe: `${result.recipe.id}@${result.recipe.version}`,
|
|
7678
|
+
destination: result.destination,
|
|
7679
|
+
changed: result.changed,
|
|
7680
|
+
unchanged: result.unchanged,
|
|
7681
|
+
styleId: result.styleId,
|
|
7682
|
+
style: result.style
|
|
7683
|
+
}, null, 2));
|
|
7684
|
+
} else {
|
|
7685
|
+
log(` installed ${result.recipe.id}@${result.recipe.version}`);
|
|
7686
|
+
log(` ${path22.relative(process.cwd(), result.destination) || "."}`);
|
|
7687
|
+
log(`
|
|
7688
|
+
Add this entry under your manifest's styles object:`);
|
|
7689
|
+
log(JSON.stringify({ [result.styleId]: result.style }, null, 2));
|
|
7690
|
+
if (result.recipe.models.length) {
|
|
7691
|
+
log(`
|
|
7692
|
+
Models are not downloaded automatically. Verify them with:`);
|
|
7693
|
+
log(` pixelkiln recipe verify ${path22.relative(process.cwd(), result.destination)} --model-root <ComfyUI/models>`);
|
|
7694
|
+
}
|
|
7695
|
+
}
|
|
7696
|
+
return;
|
|
7697
|
+
}
|
|
7698
|
+
}
|
|
6787
7699
|
if (args.command === "refine") {
|
|
6788
7700
|
if (!args.from) {
|
|
6789
7701
|
throw new Error(
|
|
@@ -6812,12 +7724,12 @@ async function main() {
|
|
|
6812
7724
|
` recovered ${result2.detection.columns}x${result2.detection.rows} native grid (${result2.detection.confidence}: ${result2.detection.consensus})`
|
|
6813
7725
|
);
|
|
6814
7726
|
log(` enforced ${result2.palette.length}-color palette without dithering`);
|
|
6815
|
-
log(` wrote ${
|
|
6816
|
-
log(` quality record: ${
|
|
7727
|
+
log(` wrote ${path22.relative(process.cwd(), result2.output)}`);
|
|
7728
|
+
log(` quality record: ${path22.relative(process.cwd(), result2.record)}`);
|
|
6817
7729
|
log(`
|
|
6818
7730
|
Automated checks passed. Human 1\xD7 review is still required:`);
|
|
6819
7731
|
log(
|
|
6820
|
-
` pixelkiln refine approve --from ${
|
|
7732
|
+
` pixelkiln refine approve --from ${path22.relative(process.cwd(), result2.record)} --reviewer "Your Name"`
|
|
6821
7733
|
);
|
|
6822
7734
|
}
|
|
6823
7735
|
return;
|
|
@@ -6837,21 +7749,21 @@ async function main() {
|
|
|
6837
7749
|
note: args.note
|
|
6838
7750
|
});
|
|
6839
7751
|
if (args.json) log(JSON.stringify(result2, null, 2));
|
|
6840
|
-
else log(` approved ${
|
|
7752
|
+
else log(` approved ${path22.relative(process.cwd(), result2.output)} by ${args.reviewer.trim()}`);
|
|
6841
7753
|
return;
|
|
6842
7754
|
}
|
|
6843
7755
|
const result = await checkQualityRecord(args.from);
|
|
6844
7756
|
if (args.json) {
|
|
6845
7757
|
log(JSON.stringify(result, null, 2));
|
|
6846
7758
|
} else {
|
|
6847
|
-
log(` ${result.safe ? "release-ready" : "not release-ready"}: ${
|
|
7759
|
+
log(` ${result.safe ? "release-ready" : "not release-ready"}: ${path22.relative(process.cwd(), result.output || result.record)}`);
|
|
6848
7760
|
for (const reason of result.reasons) log(` ${reason}`);
|
|
6849
7761
|
}
|
|
6850
7762
|
if (!result.safe) process.exitCode = 1;
|
|
6851
7763
|
return;
|
|
6852
7764
|
}
|
|
6853
7765
|
if (args.command === "balance") {
|
|
6854
|
-
loadEnvFiles(
|
|
7766
|
+
loadEnvFiles(path22.dirname(path22.resolve(args.manifest)));
|
|
6855
7767
|
loadEnvFiles(process.cwd());
|
|
6856
7768
|
const loaded2 = await loadManifest(args.manifest);
|
|
6857
7769
|
const p = createProvider(loaded2.manifest.provider, "online");
|
|
@@ -6863,31 +7775,31 @@ async function main() {
|
|
|
6863
7775
|
}
|
|
6864
7776
|
if (args.command === "init") {
|
|
6865
7777
|
if (!args.from) throw new Error("init needs --from <dir> pointing at your existing PNGs.");
|
|
6866
|
-
const root =
|
|
6867
|
-
if (!
|
|
7778
|
+
const root = path22.resolve(args.from);
|
|
7779
|
+
if (!existsSync17(root)) throw new Error(`No directory at ${root}`);
|
|
6868
7780
|
const generator = args.generator ?? "map";
|
|
6869
7781
|
if (generator !== "1dir" && generator !== "map") {
|
|
6870
7782
|
throw new Error(`--generator must be "1dir" or "map", got "${args.generator}".`);
|
|
6871
7783
|
}
|
|
6872
|
-
const target =
|
|
7784
|
+
const target = path22.resolve(args.out ?? "pixelkiln.manifest.json");
|
|
6873
7785
|
const { assets, skipped } = await scanAssets(root, { exclude: args.exclude });
|
|
6874
7786
|
if (!assets.length) throw new Error(`No PNGs found under ${root}`);
|
|
6875
7787
|
const manifest = buildManifest(
|
|
6876
|
-
args.name ??
|
|
7788
|
+
args.name ?? path22.basename(path22.dirname(target)),
|
|
6877
7789
|
args.styles[0] ?? "base",
|
|
6878
7790
|
generator,
|
|
6879
|
-
|
|
7791
|
+
path22.relative(path22.dirname(target), root) || ".",
|
|
6880
7792
|
assets
|
|
6881
7793
|
);
|
|
6882
7794
|
await writeManifestFile(target, manifest);
|
|
6883
|
-
log(` scanned ${assets.length} PNG(s) under ${
|
|
7795
|
+
log(` scanned ${assets.length} PNG(s) under ${path22.relative(process.cwd(), root)}`);
|
|
6884
7796
|
if (skipped.length) log(` skipped ${skipped.length} unreadable file(s)`);
|
|
6885
|
-
log(` wrote ${
|
|
7797
|
+
log(` wrote ${path22.relative(process.cwd(), target)}`);
|
|
6886
7798
|
log(`
|
|
6887
7799
|
Prompts are intentionally empty. To recover the real ones from your`);
|
|
6888
7800
|
log(` PixelLab account instead of inventing them:`);
|
|
6889
7801
|
log(`
|
|
6890
|
-
pixelkiln adopt --manifest ${
|
|
7802
|
+
pixelkiln adopt --manifest ${path22.relative(process.cwd(), target)} --write-prompts
|
|
6891
7803
|
`);
|
|
6892
7804
|
return;
|
|
6893
7805
|
}
|
|
@@ -6935,10 +7847,10 @@ async function main() {
|
|
|
6935
7847
|
if (args.primaryOnly || args.outputRoles.length) {
|
|
6936
7848
|
throw new Error("--primary-only and --output-role require manifest-driven pack");
|
|
6937
7849
|
}
|
|
6938
|
-
const raw = JSON.parse(await
|
|
7850
|
+
const raw = JSON.parse(await readFile17(path22.resolve(args.inputs), "utf8"));
|
|
6939
7851
|
const inputs = resolvePackInputs(raw, args.inputs);
|
|
6940
7852
|
const { png, atlas, skipped, sources } = packSprites(inputs, { columns: args.columns });
|
|
6941
|
-
const base =
|
|
7853
|
+
const base = path22.resolve(args.out.replace(/\.png$/, ""));
|
|
6942
7854
|
const outputs = [
|
|
6943
7855
|
{ path: `${base}.png`, data: png },
|
|
6944
7856
|
{ path: `${base}.json`, data: JSON.stringify(atlas, null, 2) + "\n" }
|
|
@@ -6951,17 +7863,17 @@ async function main() {
|
|
|
6951
7863
|
log(
|
|
6952
7864
|
` ${atlas.frames.length} sprite(s), ${atlas.sheet.width}x${atlas.sheet.height} in ${atlas.columns} column(s) \u2014 ${(png.length / 1024).toFixed(1)} KB`
|
|
6953
7865
|
);
|
|
6954
|
-
log(` ${
|
|
7866
|
+
log(` ${path22.relative(process.cwd(), base)}.png + .json + .pixelkiln.json`);
|
|
6955
7867
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
6956
7868
|
return;
|
|
6957
7869
|
}
|
|
6958
7870
|
if (args.command === "workspace") {
|
|
6959
|
-
const workspacePath =
|
|
6960
|
-
const dir =
|
|
7871
|
+
const workspacePath = path22.resolve(args.workspace ?? "pixelkiln.workspace.json");
|
|
7872
|
+
const dir = path22.dirname(workspacePath);
|
|
6961
7873
|
if (args.subcommand === "add") {
|
|
6962
|
-
const manifestPath =
|
|
7874
|
+
const manifestPath = path22.resolve(args.target);
|
|
6963
7875
|
const loadedTarget = await loadManifest(manifestPath);
|
|
6964
|
-
const lockPath = args.explicitLock ?
|
|
7876
|
+
const lockPath = args.explicitLock ? path22.resolve(args.explicitLock) : path22.join(path22.dirname(manifestPath), "pixelkiln.lock.json");
|
|
6965
7877
|
const ws = await loadWorkspace(workspacePath);
|
|
6966
7878
|
const id = args.name ?? loadedTarget.manifest.name;
|
|
6967
7879
|
if (ws.projects.some((p) => p.id === id)) {
|
|
@@ -6981,10 +7893,10 @@ async function main() {
|
|
|
6981
7893
|
...args.account ? { account: args.account } : {}
|
|
6982
7894
|
};
|
|
6983
7895
|
await saveWorkspace(workspacePath, { version: 1, projects: [...ws.projects, project] });
|
|
6984
|
-
log(` registered "${id}" in ${
|
|
7896
|
+
log(` registered "${id}" in ${path22.relative(process.cwd(), workspacePath)}`);
|
|
6985
7897
|
log(` manifest: ${project.manifest}`);
|
|
6986
7898
|
log(` lock: ${project.lock}`);
|
|
6987
|
-
if (!
|
|
7899
|
+
if (!existsSync17(lockPath)) {
|
|
6988
7900
|
log(
|
|
6989
7901
|
` warning: no lockfile there yet \u2014 this project contributes no claims until one is generated`
|
|
6990
7902
|
);
|
|
@@ -6993,7 +7905,7 @@ async function main() {
|
|
|
6993
7905
|
}
|
|
6994
7906
|
if (args.subcommand === "remove") {
|
|
6995
7907
|
const ws = await loadWorkspace(workspacePath);
|
|
6996
|
-
const resolvedTarget =
|
|
7908
|
+
const resolvedTarget = path22.resolve(args.target);
|
|
6997
7909
|
const match = ws.projects.find(
|
|
6998
7910
|
(p) => p.id === args.target || resolveProject(dir, p).manifestPath === resolvedTarget
|
|
6999
7911
|
);
|
|
@@ -7004,11 +7916,11 @@ async function main() {
|
|
|
7004
7916
|
version: 1,
|
|
7005
7917
|
projects: ws.projects.filter((p) => p !== match)
|
|
7006
7918
|
});
|
|
7007
|
-
log(` removed "${match.id}" from ${
|
|
7919
|
+
log(` removed "${match.id}" from ${path22.relative(process.cwd(), workspacePath)}`);
|
|
7008
7920
|
return;
|
|
7009
7921
|
}
|
|
7010
7922
|
if (args.subcommand === "list") {
|
|
7011
|
-
if (!
|
|
7923
|
+
if (!existsSync17(workspacePath)) {
|
|
7012
7924
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
7013
7925
|
}
|
|
7014
7926
|
const ws = await loadWorkspace(workspacePath);
|
|
@@ -7016,9 +7928,9 @@ async function main() {
|
|
|
7016
7928
|
if (args.json) {
|
|
7017
7929
|
log(JSON.stringify({ version: 1, workspace: workspacePath, projects: ws.projects, diagnostics }, null, 2));
|
|
7018
7930
|
} else if (!ws.projects.length) {
|
|
7019
|
-
log(` no projects registered in ${
|
|
7931
|
+
log(` no projects registered in ${path22.relative(process.cwd(), workspacePath)}`);
|
|
7020
7932
|
} else {
|
|
7021
|
-
log(` ${ws.projects.length} project(s) in ${
|
|
7933
|
+
log(` ${ws.projects.length} project(s) in ${path22.relative(process.cwd(), workspacePath)}:`);
|
|
7022
7934
|
for (const p of ws.projects) {
|
|
7023
7935
|
log(` ${p.id.padEnd(24)} ${p.manifest.padEnd(40)} (${p.provider}${p.account ? `, ${p.account}` : ""})`);
|
|
7024
7936
|
}
|
|
@@ -7028,7 +7940,7 @@ async function main() {
|
|
|
7028
7940
|
return;
|
|
7029
7941
|
}
|
|
7030
7942
|
if (args.subcommand === "status") {
|
|
7031
|
-
if (!
|
|
7943
|
+
if (!existsSync17(workspacePath)) {
|
|
7032
7944
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
7033
7945
|
}
|
|
7034
7946
|
const ws = await loadWorkspace(workspacePath);
|
|
@@ -7036,7 +7948,7 @@ async function main() {
|
|
|
7036
7948
|
if (args.json) {
|
|
7037
7949
|
log(JSON.stringify({ ...report, workspace: workspacePath }, null, 2));
|
|
7038
7950
|
} else {
|
|
7039
|
-
log(` workspace: ${
|
|
7951
|
+
log(` workspace: ${path22.relative(process.cwd(), workspacePath)}`);
|
|
7040
7952
|
for (const p of report.projects) {
|
|
7041
7953
|
if (p.error) {
|
|
7042
7954
|
log(`
|
|
@@ -7080,14 +7992,14 @@ async function main() {
|
|
|
7080
7992
|
return;
|
|
7081
7993
|
}
|
|
7082
7994
|
}
|
|
7083
|
-
if (!
|
|
7995
|
+
if (!existsSync17(path22.resolve(args.manifest))) {
|
|
7084
7996
|
throw new Error(
|
|
7085
|
-
`No manifest at ${
|
|
7997
|
+
`No manifest at ${path22.resolve(args.manifest)}. Pass --manifest, or run \`pixelkiln init --from <dir>\`.`
|
|
7086
7998
|
);
|
|
7087
7999
|
}
|
|
7088
|
-
const manifestDir =
|
|
8000
|
+
const manifestDir = path22.dirname(path22.resolve(args.manifest));
|
|
7089
8001
|
const envFiles = [...loadEnvFiles(manifestDir)];
|
|
7090
|
-
if (
|
|
8002
|
+
if (path22.resolve(process.cwd()) !== manifestDir) envFiles.push(...loadEnvFiles(process.cwd()));
|
|
7091
8003
|
const loaded = await loadManifest(args.manifest);
|
|
7092
8004
|
const estimator = createProvider(loaded.manifest.provider, "offline");
|
|
7093
8005
|
const specs = await resolveSpecs(loaded, {
|
|
@@ -7164,7 +8076,7 @@ async function main() {
|
|
|
7164
8076
|
let intact = true;
|
|
7165
8077
|
for (const output of entry.outputs) {
|
|
7166
8078
|
const file = resolveOutputPath(output.path, item.spec.root);
|
|
7167
|
-
if (!
|
|
8079
|
+
if (!existsSync17(file) || await sha256File(file) !== output.sha256) {
|
|
7168
8080
|
intact = false;
|
|
7169
8081
|
break;
|
|
7170
8082
|
}
|
|
@@ -7222,7 +8134,7 @@ async function main() {
|
|
|
7222
8134
|
if (args.primaryOnly && args.outputRoles.length) {
|
|
7223
8135
|
throw new Error("pack accepts either --primary-only or --output-role, not both");
|
|
7224
8136
|
}
|
|
7225
|
-
const manifestDir2 =
|
|
8137
|
+
const manifestDir2 = path22.dirname(path22.resolve(args.manifest));
|
|
7226
8138
|
const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
|
|
7227
8139
|
for (const styleId of styleIds) {
|
|
7228
8140
|
const { png, atlas, skipped, sources } = packStyle(lock, styleId, manifestDir2, {
|
|
@@ -7231,7 +8143,7 @@ async function main() {
|
|
|
7231
8143
|
primaryOnly: args.primaryOnly
|
|
7232
8144
|
});
|
|
7233
8145
|
const style = loaded.manifest.styles[styleId];
|
|
7234
|
-
const base = args.out ?
|
|
8146
|
+
const base = args.out ? path22.resolve(args.out.replace(/\.png$/, "")) : path22.resolve(manifestDir2, style.outDir, `${styleId}-sheet`);
|
|
7235
8147
|
const outputs = [
|
|
7236
8148
|
{ path: `${base}.png`, data: png },
|
|
7237
8149
|
{ path: `${base}.json`, data: JSON.stringify(atlas, null, 2) + "\n" }
|
|
@@ -7254,13 +8166,13 @@ async function main() {
|
|
|
7254
8166
|
log(
|
|
7255
8167
|
` ${styleId} \u2014 ${atlas.frames.length} sprite(s), ${atlas.sheet.width}x${atlas.sheet.height} in ${atlas.columns} column(s)`
|
|
7256
8168
|
);
|
|
7257
|
-
log(` ${
|
|
8169
|
+
log(` ${path22.relative(process.cwd(), base)}.png + .json + .pixelkiln.json`);
|
|
7258
8170
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
7259
8171
|
}
|
|
7260
8172
|
return;
|
|
7261
8173
|
}
|
|
7262
8174
|
if (args.command === "mount") {
|
|
7263
|
-
const manifestDir2 =
|
|
8175
|
+
const manifestDir2 = path22.dirname(path22.resolve(args.manifest));
|
|
7264
8176
|
const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
|
|
7265
8177
|
for (const styleId of styleIds) {
|
|
7266
8178
|
const style = loaded.manifest.styles[styleId];
|
|
@@ -7290,7 +8202,7 @@ async function main() {
|
|
|
7290
8202
|
sources,
|
|
7291
8203
|
outputRoles
|
|
7292
8204
|
);
|
|
7293
|
-
const out =
|
|
8205
|
+
const out = path22.resolve(manifestDir2, style.mount.out);
|
|
7294
8206
|
const metadata = out.replace(/\.png$/, "") + ".json";
|
|
7295
8207
|
const companion = out.replace(/\.png$/, "") + ".pixelkiln.json";
|
|
7296
8208
|
const outputs = [
|
|
@@ -7303,7 +8215,7 @@ async function main() {
|
|
|
7303
8215
|
await provenanceFile("$manifest", args.manifest),
|
|
7304
8216
|
await provenanceFile("$lock", args.lock),
|
|
7305
8217
|
...artifactSources.filter(
|
|
7306
|
-
(source) => source.id !== "$base" ||
|
|
8218
|
+
(source) => source.id !== "$base" || path22.resolve(source.path) !== out
|
|
7307
8219
|
)
|
|
7308
8220
|
],
|
|
7309
8221
|
options: {
|
|
@@ -7316,7 +8228,7 @@ async function main() {
|
|
|
7316
8228
|
log(
|
|
7317
8229
|
` ${styleId} \u2014 ${atlas.frames.length} cell(s) into ${atlas.sheet.width}x${atlas.sheet.height}` + (overBase ? ` over ${style.mount.base}` : " (new sheet)")
|
|
7318
8230
|
);
|
|
7319
|
-
log(` ${
|
|
8231
|
+
log(` ${path22.relative(process.cwd(), out)} + atlas/provenance JSON`);
|
|
7320
8232
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
7321
8233
|
}
|
|
7322
8234
|
return;
|
|
@@ -7372,7 +8284,7 @@ async function main() {
|
|
|
7372
8284
|
}
|
|
7373
8285
|
if (args.command === "export") {
|
|
7374
8286
|
const format = args.format ?? "generic";
|
|
7375
|
-
const manifestDir2 =
|
|
8287
|
+
const manifestDir2 = path22.dirname(path22.resolve(args.manifest));
|
|
7376
8288
|
const selected = specs.filter((spec) => {
|
|
7377
8289
|
if (spec.generator !== "tiles") return false;
|
|
7378
8290
|
if (args.styles.length && !args.styles.includes(spec.styleId)) return false;
|
|
@@ -7388,12 +8300,12 @@ async function main() {
|
|
|
7388
8300
|
for (const spec of selected) {
|
|
7389
8301
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
7390
8302
|
const style = loaded.manifest.styles[spec.styleId];
|
|
7391
|
-
const defaultBase =
|
|
7392
|
-
const base = args.out ?
|
|
8303
|
+
const defaultBase = path22.resolve(manifestDir2, style.outDir, `${spec.assetId}-tileset`);
|
|
8304
|
+
const base = args.out ? path22.resolve(args.out.replace(/\.(?:png|json|tsj|tres)$/i, "")) : defaultBase;
|
|
7393
8305
|
const result = exportTileset(entry, spec, {
|
|
7394
8306
|
format,
|
|
7395
8307
|
manifestDir: manifestDir2,
|
|
7396
|
-
imageName:
|
|
8308
|
+
imageName: path22.basename(`${base}.png`),
|
|
7397
8309
|
columns: args.columns
|
|
7398
8310
|
});
|
|
7399
8311
|
const outputs = [
|
|
@@ -7411,7 +8323,7 @@ async function main() {
|
|
|
7411
8323
|
asset: spec.assetId,
|
|
7412
8324
|
columns: args.columns ?? null,
|
|
7413
8325
|
format,
|
|
7414
|
-
image:
|
|
8326
|
+
image: path22.basename(`${base}.png`),
|
|
7415
8327
|
providerRules: result.generic.providerRules,
|
|
7416
8328
|
style: spec.styleId,
|
|
7417
8329
|
tileType: spec.tileType ?? null
|
|
@@ -7421,7 +8333,7 @@ async function main() {
|
|
|
7421
8333
|
` ${spec.styleId}/${spec.assetId} \u2014 ${result.generic.tiles.length} tile(s), ${result.generic.sheet.width}x${result.generic.sheet.height} (${format})`
|
|
7422
8334
|
);
|
|
7423
8335
|
log(
|
|
7424
|
-
` ${
|
|
8336
|
+
` ${path22.relative(process.cwd(), base)}.png + ${path22.basename(base)}${result.extension} + .pixelkiln.json`
|
|
7425
8337
|
);
|
|
7426
8338
|
}
|
|
7427
8339
|
return;
|
|
@@ -7461,10 +8373,10 @@ async function main() {
|
|
|
7461
8373
|
tagged ${n} object(s) upstream`);
|
|
7462
8374
|
}
|
|
7463
8375
|
if (args.writePrompts) {
|
|
7464
|
-
const { filled, stillEmpty } = await writePromptsBack(
|
|
8376
|
+
const { filled, stillEmpty } = await writePromptsBack(path22.resolve(args.manifest), lock, {
|
|
7465
8377
|
onProgress: log
|
|
7466
8378
|
});
|
|
7467
|
-
log(` recovered ${filled} prompt(s) into ${
|
|
8379
|
+
log(` recovered ${filled} prompt(s) into ${path22.relative(process.cwd(), args.manifest)}`);
|
|
7468
8380
|
const reloaded = await loadManifest(args.manifest);
|
|
7469
8381
|
const rebased = await resolveSpecs(reloaded, {
|
|
7470
8382
|
styles: args.styles,
|
|
@@ -7493,12 +8405,12 @@ async function main() {
|
|
|
7493
8405
|
if (args.command === "salvage") {
|
|
7494
8406
|
const jsonMode = args.dryRun && args.json;
|
|
7495
8407
|
const diag = jsonMode ? (msg = "") => console.error(msg) : log;
|
|
7496
|
-
const ownLock =
|
|
8408
|
+
const ownLock = path22.resolve(args.lock);
|
|
7497
8409
|
let workspaceProjects = [];
|
|
7498
8410
|
let workspaceDir = "";
|
|
7499
8411
|
if (args.workspace) {
|
|
7500
|
-
const workspacePath =
|
|
7501
|
-
workspaceDir =
|
|
8412
|
+
const workspacePath = path22.resolve(args.workspace);
|
|
8413
|
+
workspaceDir = path22.dirname(workspacePath);
|
|
7502
8414
|
const complete = await requireCompleteWorkspaceClaims(workspacePath);
|
|
7503
8415
|
workspaceProjects = complete.ws.projects;
|
|
7504
8416
|
for (const d of complete.diagnostics) diag(` WARN ${d.id}: ${d.message}`);
|
|
@@ -7507,12 +8419,12 @@ async function main() {
|
|
|
7507
8419
|
const lockPaths = [
|
|
7508
8420
|
.../* @__PURE__ */ new Set([
|
|
7509
8421
|
...workspaceLockPaths,
|
|
7510
|
-
...
|
|
7511
|
-
...args.claims.map((c) =>
|
|
8422
|
+
...existsSync17(ownLock) ? [ownLock] : [],
|
|
8423
|
+
...args.claims.map((c) => path22.resolve(c))
|
|
7512
8424
|
])
|
|
7513
8425
|
];
|
|
7514
8426
|
diag(` claim set (${lockPaths.length} lockfile(s)):`);
|
|
7515
|
-
for (const p of lockPaths) diag(` ${
|
|
8427
|
+
for (const p of lockPaths) diag(` ${path22.relative(process.cwd(), p)}`);
|
|
7516
8428
|
if (!args.claims.length && !args.workspace) {
|
|
7517
8429
|
diag(
|
|
7518
8430
|
`
|
|
@@ -7520,7 +8432,7 @@ async function main() {
|
|
|
7520
8432
|
pass every other project's lockfile via --claims a.json,b.json, or
|
|
7521
8433
|
register every project in a workspace catalog and pass --workspace.`
|
|
7522
8434
|
);
|
|
7523
|
-
} else if (args.workspace && !workspaceProjects.some((p) => resolveProject(workspaceDir, p).manifestPath ===
|
|
8435
|
+
} else if (args.workspace && !workspaceProjects.some((p) => resolveProject(workspaceDir, p).manifestPath === path22.resolve(args.manifest))) {
|
|
7524
8436
|
diag(
|
|
7525
8437
|
`
|
|
7526
8438
|
This project's manifest is not registered in the workspace catalog. Its own
|
|
@@ -7604,7 +8516,7 @@ async function main() {
|
|
|
7604
8516
|
manifestPath: loaded.path,
|
|
7605
8517
|
manifest: loaded.manifest,
|
|
7606
8518
|
styleId,
|
|
7607
|
-
importDir:
|
|
8519
|
+
importDir: path22.resolve(loaded.root, style.outDir),
|
|
7608
8520
|
lock,
|
|
7609
8521
|
lockPath: args.lock
|
|
7610
8522
|
},
|