pixelkiln 0.10.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 +30 -33
- package/README.md +52 -42
- package/SECURITY.md +2 -3
- package/dist/cli.d.ts +13 -3
- package/dist/cli.js +1994 -574
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1361 -215
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1405 -2
- package/dist/index.d.ts +1405 -2
- package/dist/index.js +1290 -169
- package/dist/index.js.map +1 -1
- package/docs/AGENTS.md +10 -7
- package/docs/ARTIFACTS.md +28 -1
- package/docs/CLI.md +131 -7
- package/docs/COMFYUI.md +95 -24
- package/docs/GETTING_STARTED.md +13 -6
- package/docs/LIBRARY.md +67 -0
- package/docs/MANIFEST.md +3 -2
- package/docs/PROVIDER_BENCHMARK.md +46 -26
- package/docs/QUALITY.md +85 -6
- 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 +13 -3
- package/skills/pixelkiln/references/comfyui.md +30 -15
- package/skills/pixelkiln/references/mixed-providers.md +4 -3
- 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(path20) {
|
|
|
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;
|
|
@@ -5044,110 +5050,927 @@ async function inspectRemoteHashCache(remotePath) {
|
|
|
5044
5050
|
return report;
|
|
5045
5051
|
}
|
|
5046
5052
|
|
|
5047
|
-
// src/pipeline/
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5053
|
+
// src/pipeline/refine.ts
|
|
5054
|
+
import path18 from "path";
|
|
5055
|
+
import { execFile } from "child_process";
|
|
5056
|
+
import { mkdtemp, readFile as readFile13, rm as rm7 } from "fs/promises";
|
|
5057
|
+
import { tmpdir } from "os";
|
|
5058
|
+
import { promisify } from "util";
|
|
5059
|
+
|
|
5060
|
+
// src/artifacts.ts
|
|
5061
|
+
import path17 from "path";
|
|
5062
|
+
import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
|
|
5063
|
+
import { mkdir as mkdir5, readFile as readFile12, rename as rename5, rm as rm6, writeFile as writeFile7 } from "fs/promises";
|
|
5064
|
+
var activeTransactions = /* @__PURE__ */ new Set();
|
|
5065
|
+
function message(error) {
|
|
5066
|
+
return error instanceof Error ? error.message : String(error);
|
|
5067
|
+
}
|
|
5068
|
+
function digest(data) {
|
|
5069
|
+
return createHash2("sha256").update(data).digest("hex");
|
|
5070
|
+
}
|
|
5071
|
+
function portableRelative(from, to) {
|
|
5072
|
+
return path17.relative(from, path17.resolve(to)).split(path17.sep).join("/") || ".";
|
|
5073
|
+
}
|
|
5074
|
+
function canonical2(value) {
|
|
5075
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
5076
|
+
if (typeof value === "number") {
|
|
5077
|
+
if (!Number.isFinite(value)) throw new Error("Artifact provenance numbers must be finite.");
|
|
5078
|
+
return value;
|
|
5079
|
+
}
|
|
5080
|
+
if (Array.isArray(value)) return value.map(canonical2);
|
|
5081
|
+
if (typeof value === "object") {
|
|
5082
|
+
const sorted = {};
|
|
5083
|
+
for (const key of Object.keys(value).sort()) {
|
|
5084
|
+
const item = value[key];
|
|
5085
|
+
if (item !== void 0) sorted[key] = canonical2(item);
|
|
5060
5086
|
}
|
|
5087
|
+
return sorted;
|
|
5061
5088
|
}
|
|
5062
|
-
|
|
5063
|
-
const terrains = Array.isArray(raw.terrains) ? raw.terrains.filter((item) => typeof item === "string") : [];
|
|
5064
|
-
return {
|
|
5065
|
-
ruleType,
|
|
5066
|
-
arity,
|
|
5067
|
-
...connectivity ? { connectivity } : {},
|
|
5068
|
-
terrains,
|
|
5069
|
-
masks,
|
|
5070
|
-
raw
|
|
5071
|
-
};
|
|
5089
|
+
throw new Error(`Artifact provenance cannot contain ${typeof value} values.`);
|
|
5072
5090
|
}
|
|
5073
|
-
function
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
);
|
|
5091
|
+
function fingerprint(provenance) {
|
|
5092
|
+
return digest(JSON.stringify(canonical2({
|
|
5093
|
+
kind: provenance.kind,
|
|
5094
|
+
sources: provenance.sources,
|
|
5095
|
+
options: provenance.options,
|
|
5096
|
+
outputs: provenance.outputs
|
|
5097
|
+
})));
|
|
5098
|
+
}
|
|
5099
|
+
function parseArtifactManifest(absolute, data) {
|
|
5100
|
+
let raw;
|
|
5101
|
+
try {
|
|
5102
|
+
raw = JSON.parse(data);
|
|
5103
|
+
} catch (error) {
|
|
5104
|
+
throw new Error(`${absolute} is not valid JSON: ${message(error)}`, { cause: error });
|
|
5087
5105
|
}
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
const
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5106
|
+
if (raw.format !== "pixelkiln-artifact-bundle" || raw.version !== 1 || raw.kind !== "pack" && raw.kind !== "mount" && raw.kind !== "tileset" && raw.kind !== "refine" || typeof raw.fingerprint !== "string" || raw.options === void 0 || !Array.isArray(raw.sources) || !Array.isArray(raw.outputs)) {
|
|
5107
|
+
throw new Error(`${absolute} is not a PixelKiln artifact bundle manifest.`);
|
|
5108
|
+
}
|
|
5109
|
+
for (const source of raw.sources) {
|
|
5110
|
+
if (!source || typeof source.id !== "string" || typeof source.path !== "string" || source.sha256 !== null && typeof source.sha256 !== "string" || typeof source.included !== "boolean") {
|
|
5111
|
+
throw new Error(`${absolute} contains an invalid artifact source.`);
|
|
5112
|
+
}
|
|
5113
|
+
}
|
|
5114
|
+
for (const output of raw.outputs) {
|
|
5115
|
+
if (!output || typeof output.path !== "string" || typeof output.sha256 !== "string") {
|
|
5116
|
+
throw new Error(`${absolute} contains an invalid artifact output.`);
|
|
5117
|
+
}
|
|
5118
|
+
}
|
|
5119
|
+
return raw;
|
|
5120
|
+
}
|
|
5121
|
+
async function readArtifactBundleManifest(manifestPath) {
|
|
5122
|
+
const absolute = path17.resolve(manifestPath);
|
|
5123
|
+
return parseArtifactManifest(absolute, await readFile12(absolute, "utf8"));
|
|
5124
|
+
}
|
|
5125
|
+
async function readOptional(file) {
|
|
5126
|
+
try {
|
|
5127
|
+
return await readFile12(file);
|
|
5128
|
+
} catch (error) {
|
|
5129
|
+
if (isCode(error, "ENOENT")) return null;
|
|
5130
|
+
throw error;
|
|
5131
|
+
}
|
|
5132
|
+
}
|
|
5133
|
+
function transactionMarker(journal) {
|
|
5134
|
+
return `${journal}.committed`;
|
|
5135
|
+
}
|
|
5136
|
+
function processIsAlive(pid) {
|
|
5137
|
+
if (!Number.isInteger(pid) || pid < 1) return false;
|
|
5138
|
+
try {
|
|
5139
|
+
process.kill(pid, 0);
|
|
5140
|
+
return true;
|
|
5141
|
+
} catch (error) {
|
|
5142
|
+
return !isCode(error, "ESRCH");
|
|
5143
|
+
}
|
|
5144
|
+
}
|
|
5145
|
+
function validTemporaryPath(candidate, destination, type) {
|
|
5146
|
+
return path17.dirname(candidate) === path17.dirname(destination) && path17.basename(candidate).startsWith(
|
|
5147
|
+
`.${path17.basename(destination)}.pixelkiln-${type}-`
|
|
5099
5148
|
);
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
};
|
|
5149
|
+
}
|
|
5150
|
+
function parseTransaction(journal, data) {
|
|
5151
|
+
let raw;
|
|
5152
|
+
try {
|
|
5153
|
+
raw = JSON.parse(data);
|
|
5154
|
+
} catch (error) {
|
|
5155
|
+
throw new Error(`${journal} is not valid transaction JSON: ${message(error)}`, { cause: error });
|
|
5108
5156
|
}
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
throw new Error(
|
|
5112
|
-
`${options.format} export needs recognized adjacency rules; use --format generic to retain this provider rule object without guessing`
|
|
5113
|
-
);
|
|
5157
|
+
if (raw.format !== "pixelkiln-artifact-transaction" || raw.version !== 1 || !Number.isInteger(raw.pid) || typeof raw.createdAt !== "string" || !Array.isArray(raw.entries) || !raw.entries.length) {
|
|
5158
|
+
throw new Error(`${journal} is not a PixelKiln artifact transaction.`);
|
|
5114
5159
|
}
|
|
5115
|
-
|
|
5160
|
+
return raw;
|
|
5161
|
+
}
|
|
5162
|
+
async function removeTransactionFiles(journal) {
|
|
5163
|
+
await rm6(journal, { force: true });
|
|
5164
|
+
await rm6(transactionMarker(journal), { force: true });
|
|
5165
|
+
}
|
|
5166
|
+
async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
|
|
5167
|
+
const journal = path17.resolve(recoveryFile);
|
|
5168
|
+
const bytes = await readOptional(journal);
|
|
5169
|
+
if (!bytes) {
|
|
5170
|
+
await rm6(transactionMarker(journal), { force: true });
|
|
5171
|
+
return;
|
|
5172
|
+
}
|
|
5173
|
+
if (activeTransactions.has(journal)) {
|
|
5174
|
+
throw new Error(`Another artifact write in this process is already using ${journal}.`);
|
|
5175
|
+
}
|
|
5176
|
+
const transaction = parseTransaction(journal, bytes.toString("utf8"));
|
|
5177
|
+
if (transaction.pid !== process.pid && processIsAlive(transaction.pid)) {
|
|
5116
5178
|
throw new Error(
|
|
5117
|
-
|
|
5179
|
+
`Artifact transaction ${journal} is still owned by live process ${transaction.pid}; refusing concurrent recovery.`
|
|
5118
5180
|
);
|
|
5119
5181
|
}
|
|
5120
|
-
|
|
5182
|
+
for (const entry of transaction.entries) {
|
|
5183
|
+
if (typeof entry.destination !== "string" || typeof entry.stage !== "string" || typeof entry.sha256 !== "string" || entry.backup !== void 0 && typeof entry.backup !== "string" || !allowedDestinations.has(path17.resolve(entry.destination)) || !validTemporaryPath(entry.stage, entry.destination, "stage") || entry.backup !== void 0 && !validTemporaryPath(entry.backup, entry.destination, "backup")) {
|
|
5184
|
+
throw new Error(
|
|
5185
|
+
`Refusing unsafe artifact recovery from ${journal}; its destinations or temporary paths do not match the current bundle.`
|
|
5186
|
+
);
|
|
5187
|
+
}
|
|
5188
|
+
}
|
|
5189
|
+
const committed = await readOptional(transactionMarker(journal)) !== null;
|
|
5190
|
+
const errors = [];
|
|
5191
|
+
if (committed) {
|
|
5192
|
+
for (const entry of transaction.entries) {
|
|
5193
|
+
if (entry.backup) await remove2(entry.backup, errors);
|
|
5194
|
+
await remove2(entry.stage, errors);
|
|
5195
|
+
}
|
|
5196
|
+
} else {
|
|
5197
|
+
for (const entry of [...transaction.entries].reverse()) {
|
|
5198
|
+
const backup = entry.backup ? await readOptional(entry.backup) : null;
|
|
5199
|
+
const destination = await readOptional(entry.destination);
|
|
5200
|
+
if (backup) {
|
|
5201
|
+
if (destination && digest(destination) !== entry.sha256) {
|
|
5202
|
+
errors.push(
|
|
5203
|
+
`destination changed during interrupted recovery: ${entry.destination}; previous file remains at ${entry.backup}`
|
|
5204
|
+
);
|
|
5205
|
+
continue;
|
|
5206
|
+
}
|
|
5207
|
+
await remove2(entry.destination, errors);
|
|
5208
|
+
try {
|
|
5209
|
+
await rename5(entry.backup, entry.destination);
|
|
5210
|
+
} catch (error) {
|
|
5211
|
+
errors.push(
|
|
5212
|
+
`could not restore ${entry.destination}; previous file remains at ${entry.backup}: ${message(error)}`
|
|
5213
|
+
);
|
|
5214
|
+
}
|
|
5215
|
+
} else if (destination && digest(destination) === entry.sha256) {
|
|
5216
|
+
await remove2(entry.destination, errors);
|
|
5217
|
+
}
|
|
5218
|
+
await remove2(entry.stage, errors);
|
|
5219
|
+
}
|
|
5220
|
+
}
|
|
5221
|
+
if (errors.length) {
|
|
5121
5222
|
throw new Error(
|
|
5122
|
-
|
|
5223
|
+
`Artifact transaction recovery needs attention; journal retained at ${journal}: ` + errors.join("; ")
|
|
5123
5224
|
);
|
|
5124
5225
|
}
|
|
5125
|
-
|
|
5126
|
-
return {
|
|
5127
|
-
png: packed.png,
|
|
5128
|
-
extension: ".tsj",
|
|
5129
|
-
document: JSON.stringify(buildTiled(generic), null, 2) + "\n",
|
|
5130
|
-
generic,
|
|
5131
|
-
sources: packed.sources
|
|
5132
|
-
};
|
|
5133
|
-
}
|
|
5134
|
-
return {
|
|
5135
|
-
png: packed.png,
|
|
5136
|
-
extension: ".tres",
|
|
5137
|
-
document: buildGodot(generic, spec),
|
|
5138
|
-
generic,
|
|
5139
|
-
sources: packed.sources
|
|
5140
|
-
};
|
|
5226
|
+
await removeTransactionFiles(journal);
|
|
5141
5227
|
}
|
|
5142
|
-
function
|
|
5228
|
+
function createArtifactBundleManifest(manifestPath, outputs, provenance) {
|
|
5229
|
+
const root = path17.dirname(path17.resolve(manifestPath));
|
|
5230
|
+
const sources = provenance.sources.map((source) => ({
|
|
5231
|
+
...source,
|
|
5232
|
+
path: portableRelative(root, source.path)
|
|
5233
|
+
}));
|
|
5234
|
+
const normalized = {
|
|
5235
|
+
kind: provenance.kind,
|
|
5236
|
+
sources,
|
|
5237
|
+
options: canonical2(provenance.options)
|
|
5238
|
+
};
|
|
5239
|
+
const manifestOutputs = outputs.map((output) => ({
|
|
5240
|
+
path: portableRelative(root, output.path),
|
|
5241
|
+
sha256: digest(output.data)
|
|
5242
|
+
}));
|
|
5143
5243
|
return {
|
|
5144
|
-
format: "pixelkiln-
|
|
5244
|
+
format: "pixelkiln-artifact-bundle",
|
|
5145
5245
|
version: 1,
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5246
|
+
...normalized,
|
|
5247
|
+
fingerprint: fingerprint({ ...normalized, outputs: manifestOutputs }),
|
|
5248
|
+
outputs: manifestOutputs
|
|
5249
|
+
};
|
|
5250
|
+
}
|
|
5251
|
+
function withArtifactManifest(manifestPath, outputs, provenance) {
|
|
5252
|
+
const manifest = createArtifactBundleManifest(manifestPath, outputs, provenance);
|
|
5253
|
+
return [
|
|
5254
|
+
...outputs,
|
|
5255
|
+
{ path: manifestPath, data: JSON.stringify(manifest, null, 2) + "\n" }
|
|
5256
|
+
];
|
|
5257
|
+
}
|
|
5258
|
+
async function writeManagedArtifactBundle(manifestPath, outputs, provenance, options = {}) {
|
|
5259
|
+
const absoluteManifest = path17.resolve(manifestPath);
|
|
5260
|
+
const recoveryFile = `${absoluteManifest}.transaction`;
|
|
5261
|
+
const allowedDestinations = /* @__PURE__ */ new Set([
|
|
5262
|
+
absoluteManifest,
|
|
5263
|
+
...outputs.map((output) => path17.resolve(output.path))
|
|
5264
|
+
]);
|
|
5265
|
+
await recoverArtifactTransaction(recoveryFile, allowedDestinations);
|
|
5266
|
+
const existingManifestBytes = await readOptional(absoluteManifest);
|
|
5267
|
+
let previous = null;
|
|
5268
|
+
if (existingManifestBytes && !options.force) {
|
|
5269
|
+
try {
|
|
5270
|
+
previous = parseArtifactManifest(absoluteManifest, existingManifestBytes.toString("utf8"));
|
|
5271
|
+
const valid = previous.fingerprint === fingerprint(previous);
|
|
5272
|
+
if (!valid) throw new Error("its integrity fingerprint does not match");
|
|
5273
|
+
} catch (error) {
|
|
5274
|
+
throw new Error(
|
|
5275
|
+
`Refusing to replace unrecognized or modified provenance at ${absoluteManifest}: ${message(error)}. Pass { force: true } (CLI: --force) to take ownership of this artifact bundle.`,
|
|
5276
|
+
{ cause: error }
|
|
5277
|
+
);
|
|
5278
|
+
}
|
|
5279
|
+
}
|
|
5280
|
+
if (!options.force) {
|
|
5281
|
+
const root = path17.dirname(absoluteManifest);
|
|
5282
|
+
const recorded = new Map(
|
|
5283
|
+
previous?.outputs.map((output) => [path17.resolve(root, output.path), output.sha256]) ?? []
|
|
5284
|
+
);
|
|
5285
|
+
const conflicts = [];
|
|
5286
|
+
for (const output of outputs) {
|
|
5287
|
+
const destination = path17.resolve(output.path);
|
|
5288
|
+
const current = await readOptional(destination);
|
|
5289
|
+
if (!current || digest(current) === digest(output.data)) continue;
|
|
5290
|
+
const expected = recorded.get(destination);
|
|
5291
|
+
if (!expected || digest(current) !== expected) conflicts.push(destination);
|
|
5292
|
+
}
|
|
5293
|
+
if (conflicts.length) {
|
|
5294
|
+
throw new Error(
|
|
5295
|
+
`Refusing to overwrite modified or unowned artifact file(s):
|
|
5296
|
+
` + conflicts.map((file) => ` ${file}`).join("\n") + `
|
|
5297
|
+
Pass { force: true } (CLI: --force) to take ownership and replace the complete bundle.`
|
|
5298
|
+
);
|
|
5299
|
+
}
|
|
5300
|
+
}
|
|
5301
|
+
const { force: _force, ...bundleOptions } = options;
|
|
5302
|
+
return writeArtifactBundle(
|
|
5303
|
+
withArtifactManifest(absoluteManifest, outputs, provenance),
|
|
5304
|
+
{ ...bundleOptions, recoveryFile }
|
|
5305
|
+
);
|
|
5306
|
+
}
|
|
5307
|
+
async function verifyArtifactBundle(manifestPath) {
|
|
5308
|
+
const absolute = path17.resolve(manifestPath);
|
|
5309
|
+
const raw = await readArtifactBundleManifest(absolute);
|
|
5310
|
+
const root = path17.dirname(absolute);
|
|
5311
|
+
const changedSources = [];
|
|
5312
|
+
const changedOutputs = [];
|
|
5313
|
+
for (const source of raw.sources) {
|
|
5314
|
+
let actual = null;
|
|
5315
|
+
try {
|
|
5316
|
+
actual = digest(await readFile12(path17.resolve(root, source.path)));
|
|
5317
|
+
} catch (error) {
|
|
5318
|
+
if (!isCode(error, "ENOENT")) throw error;
|
|
5319
|
+
}
|
|
5320
|
+
if (actual !== source.sha256) changedSources.push(source.id);
|
|
5321
|
+
}
|
|
5322
|
+
for (const output of raw.outputs) {
|
|
5323
|
+
try {
|
|
5324
|
+
if (digest(await readFile12(path17.resolve(root, output.path))) !== output.sha256) {
|
|
5325
|
+
changedOutputs.push(output.path);
|
|
5326
|
+
}
|
|
5327
|
+
} catch (error) {
|
|
5328
|
+
if (!isCode(error, "ENOENT")) throw error;
|
|
5329
|
+
changedOutputs.push(output.path);
|
|
5330
|
+
}
|
|
5331
|
+
}
|
|
5332
|
+
const fingerprintValid = raw.fingerprint === fingerprint({
|
|
5333
|
+
kind: raw.kind,
|
|
5334
|
+
sources: raw.sources,
|
|
5335
|
+
options: raw.options,
|
|
5336
|
+
outputs: raw.outputs
|
|
5337
|
+
});
|
|
5338
|
+
return {
|
|
5339
|
+
current: fingerprintValid && !changedSources.length && !changedOutputs.length,
|
|
5340
|
+
fingerprintValid,
|
|
5341
|
+
changedSources,
|
|
5342
|
+
changedOutputs
|
|
5343
|
+
};
|
|
5344
|
+
}
|
|
5345
|
+
function isCode(error, code) {
|
|
5346
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
5347
|
+
}
|
|
5348
|
+
async function remove2(file, errors) {
|
|
5349
|
+
try {
|
|
5350
|
+
await rm6(file, { force: true });
|
|
5351
|
+
} catch (error) {
|
|
5352
|
+
errors?.push(`could not remove ${file}: ${message(error)}`);
|
|
5353
|
+
}
|
|
5354
|
+
}
|
|
5355
|
+
async function rollback(prepared) {
|
|
5356
|
+
const errors = [];
|
|
5357
|
+
for (const artifact of [...prepared].reverse()) {
|
|
5358
|
+
if (artifact.promoted) await remove2(artifact.destination, errors);
|
|
5359
|
+
}
|
|
5360
|
+
for (const artifact of [...prepared].reverse()) {
|
|
5361
|
+
if (!artifact.backup || !artifact.backedUp) continue;
|
|
5362
|
+
try {
|
|
5363
|
+
await rename5(artifact.backup, artifact.destination);
|
|
5364
|
+
artifact.backedUp = false;
|
|
5365
|
+
} catch (error) {
|
|
5366
|
+
errors.push(
|
|
5367
|
+
`could not restore ${artifact.destination}; previous file remains at ${artifact.backup}: ${message(error)}`
|
|
5368
|
+
);
|
|
5369
|
+
}
|
|
5370
|
+
}
|
|
5371
|
+
for (const artifact of prepared) {
|
|
5372
|
+
if (artifact.stage) await remove2(artifact.stage, errors);
|
|
5373
|
+
}
|
|
5374
|
+
return errors;
|
|
5375
|
+
}
|
|
5376
|
+
async function writeArtifactBundle(files, options = {}) {
|
|
5377
|
+
if (!files.length) throw new Error("An artifact bundle must contain at least one file.");
|
|
5378
|
+
const normalized = files.map((file) => {
|
|
5379
|
+
if (!file.path.trim()) throw new Error("Artifact paths cannot be empty.");
|
|
5380
|
+
return { destination: path17.resolve(file.path), data: Buffer.from(file.data) };
|
|
5381
|
+
});
|
|
5382
|
+
const destinations = /* @__PURE__ */ new Set();
|
|
5383
|
+
for (const artifact of normalized) {
|
|
5384
|
+
if (destinations.has(artifact.destination)) {
|
|
5385
|
+
throw new Error(`Artifact bundle contains duplicate destination: ${artifact.destination}`);
|
|
5386
|
+
}
|
|
5387
|
+
destinations.add(artifact.destination);
|
|
5388
|
+
}
|
|
5389
|
+
if (options.recoveryFile) {
|
|
5390
|
+
await recoverArtifactTransaction(options.recoveryFile, destinations);
|
|
5391
|
+
}
|
|
5392
|
+
const changed = [];
|
|
5393
|
+
const unchanged = [];
|
|
5394
|
+
for (const artifact of normalized) {
|
|
5395
|
+
try {
|
|
5396
|
+
const current = await readFile12(artifact.destination);
|
|
5397
|
+
if (current.equals(artifact.data)) {
|
|
5398
|
+
unchanged.push(artifact.destination);
|
|
5399
|
+
} else {
|
|
5400
|
+
changed.push({ ...artifact, existed: true, backedUp: false, promoted: false });
|
|
5401
|
+
}
|
|
5402
|
+
} catch (error) {
|
|
5403
|
+
if (!isCode(error, "ENOENT")) throw error;
|
|
5404
|
+
changed.push({ ...artifact, existed: false, backedUp: false, promoted: false });
|
|
5405
|
+
}
|
|
5406
|
+
}
|
|
5407
|
+
if (!changed.length) return { changed: [], unchanged };
|
|
5408
|
+
const token = randomUUID3();
|
|
5409
|
+
for (const [index, artifact] of changed.entries()) {
|
|
5410
|
+
const basename = path17.basename(artifact.destination);
|
|
5411
|
+
artifact.stage = path17.join(
|
|
5412
|
+
path17.dirname(artifact.destination),
|
|
5413
|
+
`.${basename}.pixelkiln-stage-${token}-${index}`
|
|
5414
|
+
);
|
|
5415
|
+
if (artifact.existed) {
|
|
5416
|
+
artifact.backup = path17.join(
|
|
5417
|
+
path17.dirname(artifact.destination),
|
|
5418
|
+
`.${basename}.pixelkiln-backup-${token}-${index}`
|
|
5419
|
+
);
|
|
5420
|
+
}
|
|
5421
|
+
}
|
|
5422
|
+
const journal = options.recoveryFile ? path17.resolve(options.recoveryFile) : null;
|
|
5423
|
+
if (journal) {
|
|
5424
|
+
await mkdir5(path17.dirname(journal), { recursive: true });
|
|
5425
|
+
const transaction = {
|
|
5426
|
+
format: "pixelkiln-artifact-transaction",
|
|
5427
|
+
version: 1,
|
|
5428
|
+
pid: process.pid,
|
|
5429
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5430
|
+
entries: changed.map((artifact) => ({
|
|
5431
|
+
destination: artifact.destination,
|
|
5432
|
+
stage: artifact.stage,
|
|
5433
|
+
...artifact.backup ? { backup: artifact.backup } : {},
|
|
5434
|
+
sha256: digest(artifact.data)
|
|
5435
|
+
}))
|
|
5436
|
+
};
|
|
5437
|
+
try {
|
|
5438
|
+
await writeFile7(journal, JSON.stringify(transaction, null, 2) + "\n", { flag: "wx" });
|
|
5439
|
+
activeTransactions.add(journal);
|
|
5440
|
+
} catch (error) {
|
|
5441
|
+
throw new Error(
|
|
5442
|
+
`Could not start durable artifact transaction at ${journal}: ${message(error)}`,
|
|
5443
|
+
{ cause: error }
|
|
5444
|
+
);
|
|
5445
|
+
}
|
|
5446
|
+
}
|
|
5447
|
+
try {
|
|
5448
|
+
for (const [index, artifact] of changed.entries()) {
|
|
5449
|
+
await mkdir5(path17.dirname(artifact.destination), { recursive: true });
|
|
5450
|
+
await options.beforeStage?.(artifact.destination, index);
|
|
5451
|
+
await writeFile7(artifact.stage, artifact.data, { flag: "wx" });
|
|
5452
|
+
}
|
|
5453
|
+
} catch (error) {
|
|
5454
|
+
const cleanupErrors2 = await rollback(changed);
|
|
5455
|
+
if (journal && !cleanupErrors2.length) await removeTransactionFiles(journal);
|
|
5456
|
+
if (journal) activeTransactions.delete(journal);
|
|
5457
|
+
const suffix = cleanupErrors2.length ? ` Cleanup also failed: ${cleanupErrors2.join("; ")}` : "";
|
|
5458
|
+
throw new Error(`Could not stage artifact bundle: ${message(error)}.${suffix}`, { cause: error });
|
|
5459
|
+
}
|
|
5460
|
+
let durableCommit = false;
|
|
5461
|
+
try {
|
|
5462
|
+
for (const artifact of changed) {
|
|
5463
|
+
if (!artifact.existed) continue;
|
|
5464
|
+
await rename5(artifact.destination, artifact.backup);
|
|
5465
|
+
artifact.backedUp = true;
|
|
5466
|
+
}
|
|
5467
|
+
for (const [index, artifact] of changed.entries()) {
|
|
5468
|
+
await options.beforePromote?.(artifact.destination, index);
|
|
5469
|
+
await rename5(artifact.stage, artifact.destination);
|
|
5470
|
+
artifact.stage = void 0;
|
|
5471
|
+
artifact.promoted = true;
|
|
5472
|
+
}
|
|
5473
|
+
if (journal) {
|
|
5474
|
+
await writeFile7(transactionMarker(journal), `${token}
|
|
5475
|
+
`, { flag: "wx" });
|
|
5476
|
+
durableCommit = true;
|
|
5477
|
+
await options.afterCommit?.();
|
|
5478
|
+
}
|
|
5479
|
+
} catch (error) {
|
|
5480
|
+
if (durableCommit) {
|
|
5481
|
+
if (journal) activeTransactions.delete(journal);
|
|
5482
|
+
throw new Error(
|
|
5483
|
+
`Artifact bundle was committed, but final cleanup was interrupted: ${message(error)}. The next write will finish cleanup from ${journal}.`,
|
|
5484
|
+
{ cause: error }
|
|
5485
|
+
);
|
|
5486
|
+
}
|
|
5487
|
+
const rollbackErrors = await rollback(changed);
|
|
5488
|
+
if (journal && !rollbackErrors.length) await removeTransactionFiles(journal);
|
|
5489
|
+
if (journal) activeTransactions.delete(journal);
|
|
5490
|
+
const suffix = rollbackErrors.length ? ` Rollback needs attention: ${rollbackErrors.join("; ")}` : " Previous bundle restored.";
|
|
5491
|
+
throw new Error(`Could not promote artifact bundle: ${message(error)}.${suffix}`, { cause: error });
|
|
5492
|
+
}
|
|
5493
|
+
const cleanupErrors = [];
|
|
5494
|
+
for (const artifact of changed) {
|
|
5495
|
+
if (artifact.backup) await remove2(artifact.backup, cleanupErrors);
|
|
5496
|
+
}
|
|
5497
|
+
if (cleanupErrors.length) {
|
|
5498
|
+
if (journal) activeTransactions.delete(journal);
|
|
5499
|
+
throw new Error(
|
|
5500
|
+
`Artifact bundle was written, but old backup cleanup failed: ${cleanupErrors.join("; ")}`
|
|
5501
|
+
);
|
|
5502
|
+
}
|
|
5503
|
+
if (journal) {
|
|
5504
|
+
try {
|
|
5505
|
+
await removeTransactionFiles(journal);
|
|
5506
|
+
} finally {
|
|
5507
|
+
activeTransactions.delete(journal);
|
|
5508
|
+
}
|
|
5509
|
+
}
|
|
5510
|
+
return {
|
|
5511
|
+
changed: changed.map((artifact) => artifact.destination),
|
|
5512
|
+
unchanged
|
|
5513
|
+
};
|
|
5514
|
+
}
|
|
5515
|
+
|
|
5516
|
+
// src/pipeline/refine.ts
|
|
5517
|
+
var execFileAsync = promisify(execFile);
|
|
5518
|
+
var PIXEL_ART_FIXER_URL = "https://github.com/Retro-Diffusion/pixel-art-fixer";
|
|
5519
|
+
var PIXEL_ART_FIXER_REVISION = "ef376e57e1c272633ca2dbf5f29ec3fcf6596465";
|
|
5520
|
+
function message2(error) {
|
|
5521
|
+
return error instanceof Error ? error.message : String(error);
|
|
5522
|
+
}
|
|
5523
|
+
function qualityRecordPath(output) {
|
|
5524
|
+
if (!/\.png$/i.test(output)) throw new Error("Refined output must be a .png file.");
|
|
5525
|
+
return output.replace(/\.png$/i, ".pixelkiln.json");
|
|
5526
|
+
}
|
|
5527
|
+
function confidenceFor(consensus) {
|
|
5528
|
+
if (consensus.startsWith("fast:")) return "high";
|
|
5529
|
+
if (consensus === "arbitrated" || consensus === "forced" || consensus.startsWith("fastmode:") && consensus.includes("+")) return "medium";
|
|
5530
|
+
return "low";
|
|
5531
|
+
}
|
|
5532
|
+
var confidenceRank = { low: 0, medium: 1, high: 2 };
|
|
5533
|
+
function requireConfidence(value) {
|
|
5534
|
+
if (value === "low" || value === "medium" || value === "high") return value;
|
|
5535
|
+
throw new Error(`Unknown grid confidence "${value}"; expected low, medium, or high.`);
|
|
5536
|
+
}
|
|
5537
|
+
function firstJsonObject(text) {
|
|
5538
|
+
const start = text.indexOf("{");
|
|
5539
|
+
if (start < 0) throw new Error("Pixel Art Fixer did not print detection JSON.");
|
|
5540
|
+
let depth = 0;
|
|
5541
|
+
let string = false;
|
|
5542
|
+
let escaped = false;
|
|
5543
|
+
for (let i = start; i < text.length; i++) {
|
|
5544
|
+
const char = text[i];
|
|
5545
|
+
if (string) {
|
|
5546
|
+
if (escaped) escaped = false;
|
|
5547
|
+
else if (char === "\\") escaped = true;
|
|
5548
|
+
else if (char === '"') string = false;
|
|
5549
|
+
continue;
|
|
5550
|
+
}
|
|
5551
|
+
if (char === '"') string = true;
|
|
5552
|
+
else if (char === "{") depth++;
|
|
5553
|
+
else if (char === "}" && --depth === 0) {
|
|
5554
|
+
try {
|
|
5555
|
+
return JSON.parse(text.slice(start, i + 1));
|
|
5556
|
+
} catch (error) {
|
|
5557
|
+
throw new Error(`Pixel Art Fixer printed invalid detection JSON: ${message2(error)}`);
|
|
5558
|
+
}
|
|
5559
|
+
}
|
|
5560
|
+
}
|
|
5561
|
+
throw new Error("Pixel Art Fixer printed incomplete detection JSON.");
|
|
5562
|
+
}
|
|
5563
|
+
var PYTHON_FIXER_BRIDGE = [
|
|
5564
|
+
"import json, sys",
|
|
5565
|
+
"from pixelfixer.api import process",
|
|
5566
|
+
"result = process(open(sys.argv[1], 'rb').read(), mode='full')",
|
|
5567
|
+
"open(sys.argv[2], 'wb').write(result.pop('png'))",
|
|
5568
|
+
"print(json.dumps(result))"
|
|
5569
|
+
].join("; ");
|
|
5570
|
+
function parseDetection(raw) {
|
|
5571
|
+
if (!raw || typeof raw !== "object") throw new Error("Pixel Art Fixer returned no detection result.");
|
|
5572
|
+
const value = raw;
|
|
5573
|
+
const stepX = Number(value.step_x);
|
|
5574
|
+
const stepY = Number(value.step_y);
|
|
5575
|
+
const columns = Number(value.cols);
|
|
5576
|
+
const rows = Number(value.rows);
|
|
5577
|
+
const consensus = value.consensus;
|
|
5578
|
+
if (!Number.isFinite(stepX) || stepX <= 0 || !Number.isFinite(stepY) || stepY <= 0 || !Number.isInteger(columns) || columns < 1 || !Number.isInteger(rows) || rows < 1 || typeof consensus !== "string" || !consensus) {
|
|
5579
|
+
throw new Error("Pixel Art Fixer returned an invalid grid detection result.");
|
|
5580
|
+
}
|
|
5581
|
+
return {
|
|
5582
|
+
stepX,
|
|
5583
|
+
stepY,
|
|
5584
|
+
columns,
|
|
5585
|
+
rows,
|
|
5586
|
+
consensus,
|
|
5587
|
+
confidence: confidenceFor(consensus)
|
|
5588
|
+
};
|
|
5589
|
+
}
|
|
5590
|
+
function normalizePalette(values) {
|
|
5591
|
+
const colors = /* @__PURE__ */ new Map();
|
|
5592
|
+
for (const value of values) {
|
|
5593
|
+
const rgb = parseHex(value);
|
|
5594
|
+
const hex2 = `#${rgb.r.toString(16).padStart(2, "0")}${rgb.g.toString(16).padStart(2, "0")}${rgb.b.toString(16).padStart(2, "0")}`;
|
|
5595
|
+
colors.set(hex2, { hex: hex2, ...rgb });
|
|
5596
|
+
}
|
|
5597
|
+
if (colors.size < 2 || colors.size > 256) {
|
|
5598
|
+
throw new Error(`A refinement palette must contain 2\u2013256 unique colors; got ${colors.size}.`);
|
|
5599
|
+
}
|
|
5600
|
+
return [...colors.values()];
|
|
5601
|
+
}
|
|
5602
|
+
function quantizeToPalette(png, palette) {
|
|
5603
|
+
const colors = normalizePalette(palette);
|
|
5604
|
+
const pixels = Buffer.from(png.pixels);
|
|
5605
|
+
for (let i = 0; i < pixels.length; i += 4) {
|
|
5606
|
+
const alpha = pixels[i + 3];
|
|
5607
|
+
if (alpha === 0) {
|
|
5608
|
+
pixels[i] = 0;
|
|
5609
|
+
pixels[i + 1] = 0;
|
|
5610
|
+
pixels[i + 2] = 0;
|
|
5611
|
+
continue;
|
|
5612
|
+
}
|
|
5613
|
+
const pixel = { r: pixels[i], g: pixels[i + 1], b: pixels[i + 2] };
|
|
5614
|
+
let nearest = colors[0];
|
|
5615
|
+
let distance = colorDistance(pixel, nearest);
|
|
5616
|
+
for (let j = 1; j < colors.length; j++) {
|
|
5617
|
+
const candidate = colors[j];
|
|
5618
|
+
const candidateDistance = colorDistance(pixel, candidate);
|
|
5619
|
+
if (candidateDistance < distance) {
|
|
5620
|
+
nearest = candidate;
|
|
5621
|
+
distance = candidateDistance;
|
|
5622
|
+
}
|
|
5623
|
+
}
|
|
5624
|
+
pixels[i] = nearest.r;
|
|
5625
|
+
pixels[i + 1] = nearest.g;
|
|
5626
|
+
pixels[i + 2] = nearest.b;
|
|
5627
|
+
}
|
|
5628
|
+
return { width: png.width, height: png.height, pixels };
|
|
5629
|
+
}
|
|
5630
|
+
function colorCount(png) {
|
|
5631
|
+
const colors = /* @__PURE__ */ new Set();
|
|
5632
|
+
for (let i = 0; i < png.pixels.length; i += 4) {
|
|
5633
|
+
if (png.pixels[i + 3] < 128) continue;
|
|
5634
|
+
colors.add(png.pixels[i] << 16 | png.pixels[i + 1] << 8 | png.pixels[i + 2]);
|
|
5635
|
+
}
|
|
5636
|
+
return colors.size;
|
|
5637
|
+
}
|
|
5638
|
+
function unexpectedPaletteColors(png, palette) {
|
|
5639
|
+
const allowed = new Set(palette.map((hex2) => {
|
|
5640
|
+
const color = parseHex(hex2);
|
|
5641
|
+
return color.r << 16 | color.g << 8 | color.b;
|
|
5642
|
+
}));
|
|
5643
|
+
const unexpected = /* @__PURE__ */ new Set();
|
|
5644
|
+
for (let i = 0; i < png.pixels.length; i += 4) {
|
|
5645
|
+
if (png.pixels[i + 3] === 0) continue;
|
|
5646
|
+
const color = png.pixels[i] << 16 | png.pixels[i + 1] << 8 | png.pixels[i + 2];
|
|
5647
|
+
if (!allowed.has(color)) unexpected.add(color);
|
|
5648
|
+
}
|
|
5649
|
+
return unexpected.size;
|
|
5650
|
+
}
|
|
5651
|
+
function auditRefinement(png, detection, palette, minTransparency, minGridConfidence) {
|
|
5652
|
+
const colors = colorCount(png);
|
|
5653
|
+
const unexpectedColors = unexpectedPaletteColors(png, palette);
|
|
5654
|
+
const transparency = transparencyRatio(png);
|
|
5655
|
+
const reasons = [];
|
|
5656
|
+
if (png.width !== detection.columns || png.height !== detection.rows) {
|
|
5657
|
+
reasons.push(
|
|
5658
|
+
`native output is ${png.width}x${png.height}, expected detected grid ${detection.columns}x${detection.rows}`
|
|
5659
|
+
);
|
|
5660
|
+
}
|
|
5661
|
+
if (colors > palette.length) reasons.push(`opaque color count ${colors} exceeds ${palette.length}`);
|
|
5662
|
+
if (unexpectedColors) reasons.push(`${unexpectedColors} visible color(s) are outside the final palette`);
|
|
5663
|
+
if (minTransparency !== void 0 && transparency < minTransparency) {
|
|
5664
|
+
reasons.push(
|
|
5665
|
+
`transparency ${(transparency * 100).toFixed(1)}% is below ${(minTransparency * 100).toFixed(1)}%`
|
|
5666
|
+
);
|
|
5667
|
+
}
|
|
5668
|
+
if (confidenceRank[detection.confidence] < confidenceRank[minGridConfidence]) {
|
|
5669
|
+
reasons.push(
|
|
5670
|
+
`grid confidence ${detection.confidence} is below required ${minGridConfidence} (${detection.consensus})`
|
|
5671
|
+
);
|
|
5672
|
+
}
|
|
5673
|
+
return {
|
|
5674
|
+
width: png.width,
|
|
5675
|
+
height: png.height,
|
|
5676
|
+
colorCount: colors,
|
|
5677
|
+
paletteConformant: unexpectedColors === 0,
|
|
5678
|
+
transparency,
|
|
5679
|
+
thresholds: {
|
|
5680
|
+
maxColors: palette.length,
|
|
5681
|
+
minTransparency: minTransparency ?? null,
|
|
5682
|
+
minGridConfidence
|
|
5683
|
+
},
|
|
5684
|
+
safe: reasons.length === 0,
|
|
5685
|
+
reasons
|
|
5686
|
+
};
|
|
5687
|
+
}
|
|
5688
|
+
function isGridConfidence(value) {
|
|
5689
|
+
return value === "low" || value === "medium" || value === "high";
|
|
5690
|
+
}
|
|
5691
|
+
function parseRecordOptions(manifest, record) {
|
|
5692
|
+
if (manifest.kind !== "refine") throw new Error(`${record} is not a PixelKiln refinement record.`);
|
|
5693
|
+
const raw = manifest.options;
|
|
5694
|
+
if (!raw || typeof raw !== "object") throw new Error(`${record} has invalid quality metadata.`);
|
|
5695
|
+
const options = raw;
|
|
5696
|
+
const grid = options.nativeGrid;
|
|
5697
|
+
const palette = options.palette;
|
|
5698
|
+
const audit = options.audit;
|
|
5699
|
+
const review = options.review;
|
|
5700
|
+
if (options.schema !== "pixelkiln-quality" || options.version !== 1 || !grid || grid.tool !== "pixel-art-fixer" || grid.url !== PIXEL_ART_FIXER_URL || typeof grid.revision !== "string" || !grid.revision || grid.protocol !== "python-api-v1" && grid.protocol !== "python-cli-v1" || !isGridConfidence(grid.confidence) || typeof grid.consensus !== "string" || !Number.isFinite(grid.stepX) || !Number.isFinite(grid.stepY) || !Number.isInteger(grid.sourceWidth) || !Number.isInteger(grid.sourceHeight) || !Number.isInteger(grid.nativeWidth) || !Number.isInteger(grid.nativeHeight) || !palette || !Array.isArray(palette.colors) || palette.colors.length < 2 || !palette.colors.every((color) => typeof color === "string") || palette.dither !== "none" || palette.distance !== "redmean" || !audit || typeof audit.safe !== "boolean" || typeof audit.paletteConformant !== "boolean" || !Array.isArray(audit.reasons) || !review || review.status !== "pending" && review.status !== "approved") {
|
|
5701
|
+
throw new Error(`${record} has invalid quality metadata.`);
|
|
5702
|
+
}
|
|
5703
|
+
return options;
|
|
5704
|
+
}
|
|
5705
|
+
async function refineAsset(options) {
|
|
5706
|
+
const source = path18.resolve(options.source);
|
|
5707
|
+
const output = path18.resolve(options.output);
|
|
5708
|
+
if (source === output) throw new Error("Refinement source and output must be different files.");
|
|
5709
|
+
const record = path18.resolve(qualityRecordPath(output));
|
|
5710
|
+
const palette = normalizePalette(options.palette).map((color) => color.hex);
|
|
5711
|
+
const minGridConfidence = requireConfidence(options.minGridConfidence ?? "high");
|
|
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.");
|
|
5713
|
+
let sourcePng;
|
|
5714
|
+
try {
|
|
5715
|
+
sourcePng = decodePng(await readFile13(source));
|
|
5716
|
+
} catch (error) {
|
|
5717
|
+
throw new Error(`Cannot read refinement source ${source}: ${message2(error)}`, { cause: error });
|
|
5718
|
+
}
|
|
5719
|
+
const temp = await mkdtemp(path18.join(tmpdir(), "pixelkiln-refine-"));
|
|
5720
|
+
const recovered = path18.join(temp, "native.png");
|
|
5721
|
+
const compatibilityCommand = options.fixerCommand;
|
|
5722
|
+
const fixerCommand = compatibilityCommand ?? options.fixerPython ?? process.env.PIXELKILN_PIXEL_FIXER_PYTHON ?? "python3";
|
|
5723
|
+
const args = compatibilityCommand ? [...options.fixerArgsPrefix ?? [], source, "--extract", recovered] : ["-c", PYTHON_FIXER_BRIDGE, source, recovered];
|
|
5724
|
+
try {
|
|
5725
|
+
let stdout;
|
|
5726
|
+
try {
|
|
5727
|
+
const result = await execFileAsync(fixerCommand, args, {
|
|
5728
|
+
encoding: "utf8",
|
|
5729
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
5730
|
+
timeout: 10 * 60 * 1e3
|
|
5731
|
+
});
|
|
5732
|
+
stdout = result.stdout;
|
|
5733
|
+
} catch (error) {
|
|
5734
|
+
const code = typeof error === "object" && error !== null && "code" in error ? String(error.code) : "";
|
|
5735
|
+
const install = `Install the pinned Python package from ${PIXEL_ART_FIXER_URL} at ${options.fixerRevision ?? PIXEL_ART_FIXER_REVISION}, or pass --fixer-python.`;
|
|
5736
|
+
throw new Error(
|
|
5737
|
+
code === "ENOENT" ? `Pixel Art Fixer command not found: ${fixerCommand}. ${install}` : `Pixel Art Fixer failed: ${message2(error)}. ${install}`,
|
|
5738
|
+
{ cause: error }
|
|
5739
|
+
);
|
|
5740
|
+
}
|
|
5741
|
+
const detection = parseDetection(firstJsonObject(stdout));
|
|
5742
|
+
const native = decodePng(await readFile13(recovered));
|
|
5743
|
+
const quantized = quantizeToPalette(native, palette);
|
|
5744
|
+
const png = encodeRgbaPng(quantized.width, quantized.height, quantized.pixels);
|
|
5745
|
+
const audit = auditRefinement(
|
|
5746
|
+
quantized,
|
|
5747
|
+
detection,
|
|
5748
|
+
palette,
|
|
5749
|
+
options.minTransparency,
|
|
5750
|
+
minGridConfidence
|
|
5751
|
+
);
|
|
5752
|
+
if (!audit.safe) {
|
|
5753
|
+
throw new Error(`Refinement failed its quality audit: ${audit.reasons.join("; ")}`);
|
|
5754
|
+
}
|
|
5755
|
+
const provenanceSource = {
|
|
5756
|
+
id: "source",
|
|
5757
|
+
path: source,
|
|
5758
|
+
sha256: await sha256File(source),
|
|
5759
|
+
included: true
|
|
5760
|
+
};
|
|
5761
|
+
const recordOptions = {
|
|
5762
|
+
schema: "pixelkiln-quality",
|
|
5763
|
+
version: 1,
|
|
5764
|
+
nativeGrid: {
|
|
5765
|
+
tool: "pixel-art-fixer",
|
|
5766
|
+
url: PIXEL_ART_FIXER_URL,
|
|
5767
|
+
revision: options.fixerRevision ?? PIXEL_ART_FIXER_REVISION,
|
|
5768
|
+
protocol: compatibilityCommand ? "python-cli-v1" : "python-api-v1",
|
|
5769
|
+
confidence: detection.confidence,
|
|
5770
|
+
consensus: detection.consensus,
|
|
5771
|
+
stepX: detection.stepX,
|
|
5772
|
+
stepY: detection.stepY,
|
|
5773
|
+
sourceWidth: sourcePng.width,
|
|
5774
|
+
sourceHeight: sourcePng.height,
|
|
5775
|
+
nativeWidth: quantized.width,
|
|
5776
|
+
nativeHeight: quantized.height
|
|
5777
|
+
},
|
|
5778
|
+
palette: { colors: palette, dither: "none", distance: "redmean" },
|
|
5779
|
+
audit,
|
|
5780
|
+
review: { status: "pending" }
|
|
5781
|
+
};
|
|
5782
|
+
await writeManagedArtifactBundle(
|
|
5783
|
+
record,
|
|
5784
|
+
[{ path: output, data: png }],
|
|
5785
|
+
{ kind: "refine", sources: [provenanceSource], options: recordOptions },
|
|
5786
|
+
{ force: options.force }
|
|
5787
|
+
);
|
|
5788
|
+
return { source, output, record, detection, palette, audit };
|
|
5789
|
+
} finally {
|
|
5790
|
+
await rm7(temp, { recursive: true, force: true });
|
|
5791
|
+
}
|
|
5792
|
+
}
|
|
5793
|
+
async function checkQualityRecord(recordPath) {
|
|
5794
|
+
const record = path18.resolve(recordPath);
|
|
5795
|
+
const manifest = await readArtifactBundleManifest(record);
|
|
5796
|
+
const options = parseRecordOptions(manifest, record);
|
|
5797
|
+
const verification = await verifyArtifactBundle(record);
|
|
5798
|
+
const output = manifest.outputs.length === 1 ? path18.resolve(path18.dirname(record), manifest.outputs[0].path) : "";
|
|
5799
|
+
const reasons = [];
|
|
5800
|
+
if (!verification.fingerprintValid) reasons.push("quality record fingerprint changed");
|
|
5801
|
+
for (const source of verification.changedSources) reasons.push(`source changed: ${source}`);
|
|
5802
|
+
for (const changed of verification.changedOutputs) reasons.push(`output changed: ${changed}`);
|
|
5803
|
+
if (manifest.sources.length !== 1) reasons.push("quality record must contain exactly one source");
|
|
5804
|
+
if (manifest.outputs.length !== 1) reasons.push("quality record must contain exactly one output");
|
|
5805
|
+
if (!options.audit.safe) reasons.push(...options.audit.reasons.map((reason) => `audit: ${reason}`));
|
|
5806
|
+
if (options.review.status !== "approved") reasons.push("human 1\xD7 review is pending");
|
|
5807
|
+
return {
|
|
5808
|
+
safe: reasons.length === 0,
|
|
5809
|
+
current: verification.current,
|
|
5810
|
+
approved: options.review.status === "approved",
|
|
5811
|
+
auditSafe: options.audit.safe,
|
|
5812
|
+
record,
|
|
5813
|
+
output,
|
|
5814
|
+
options,
|
|
5815
|
+
reasons
|
|
5816
|
+
};
|
|
5817
|
+
}
|
|
5818
|
+
async function approveQualityRecord(recordPath, approval) {
|
|
5819
|
+
const reviewer = approval.reviewer.trim();
|
|
5820
|
+
if (!reviewer) throw new Error("A human reviewer name is required.");
|
|
5821
|
+
const record = path18.resolve(recordPath);
|
|
5822
|
+
const manifest = await readArtifactBundleManifest(record);
|
|
5823
|
+
const options = parseRecordOptions(manifest, record);
|
|
5824
|
+
const verification = await verifyArtifactBundle(record);
|
|
5825
|
+
if (!verification.current) {
|
|
5826
|
+
const changes = [
|
|
5827
|
+
...verification.changedSources.map((source) => `source ${source}`),
|
|
5828
|
+
...verification.changedOutputs.map((output2) => `output ${output2}`),
|
|
5829
|
+
...!verification.fingerprintValid ? ["quality metadata"] : []
|
|
5830
|
+
];
|
|
5831
|
+
throw new Error(`Cannot approve a stale refinement; changed: ${changes.join(", ")}. Run refine again.`);
|
|
5832
|
+
}
|
|
5833
|
+
if (!options.audit.safe) {
|
|
5834
|
+
throw new Error(`Cannot approve a failed quality audit: ${options.audit.reasons.join("; ")}`);
|
|
5835
|
+
}
|
|
5836
|
+
if (manifest.sources.length !== 1 || manifest.outputs.length !== 1) {
|
|
5837
|
+
throw new Error("A refinement record must contain exactly one source and one output.");
|
|
5838
|
+
}
|
|
5839
|
+
const root = path18.dirname(record);
|
|
5840
|
+
const outputPath = path18.resolve(root, manifest.outputs[0].path);
|
|
5841
|
+
const output = await readFile13(outputPath);
|
|
5842
|
+
const sources = manifest.sources.map((source) => ({
|
|
5843
|
+
...source,
|
|
5844
|
+
path: path18.resolve(root, source.path)
|
|
5845
|
+
}));
|
|
5846
|
+
const approved = {
|
|
5847
|
+
...options,
|
|
5848
|
+
review: {
|
|
5849
|
+
status: "approved",
|
|
5850
|
+
reviewer,
|
|
5851
|
+
approvedAt: (approval.approvedAt ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
5852
|
+
checklist: {
|
|
5853
|
+
nativeScale: true,
|
|
5854
|
+
crispEdges: true,
|
|
5855
|
+
subjectReadable: true,
|
|
5856
|
+
paletteSeparation: true,
|
|
5857
|
+
alphaAndSeams: true
|
|
5858
|
+
},
|
|
5859
|
+
...approval.note?.trim() ? { note: approval.note.trim() } : {}
|
|
5860
|
+
}
|
|
5861
|
+
};
|
|
5862
|
+
await writeManagedArtifactBundle(
|
|
5863
|
+
record,
|
|
5864
|
+
[{ path: outputPath, data: output }],
|
|
5865
|
+
{ kind: "refine", sources, options: approved }
|
|
5866
|
+
);
|
|
5867
|
+
return checkQualityRecord(record);
|
|
5868
|
+
}
|
|
5869
|
+
|
|
5870
|
+
// src/pipeline/tileset-export.ts
|
|
5871
|
+
function normalizeTileRules(raw) {
|
|
5872
|
+
if (!isRecord(raw)) return null;
|
|
5873
|
+
const ruleType = raw.rule_type;
|
|
5874
|
+
const arity = raw.arity;
|
|
5875
|
+
if (ruleType !== "edge" && ruleType !== "corner" && ruleType !== "outline") return null;
|
|
5876
|
+
if (arity !== 4 && arity !== 6) return null;
|
|
5877
|
+
const masks = {};
|
|
5878
|
+
if (isRecord(raw.tiles)) {
|
|
5879
|
+
for (const [key, value] of Object.entries(raw.tiles)) {
|
|
5880
|
+
const index = tileIndex(key);
|
|
5881
|
+
const mask = typeof value === "number" ? value : isRecord(value) && typeof value.bitmask === "number" ? value.bitmask : isRecord(value) && typeof value.mask === "number" ? value.mask : null;
|
|
5882
|
+
if (index !== null && mask !== null && Number.isInteger(mask) && mask >= 0) masks[index] = mask;
|
|
5883
|
+
}
|
|
5884
|
+
}
|
|
5885
|
+
const connectivity = raw.connectivity === "same" || raw.connectivity === "other" ? raw.connectivity : void 0;
|
|
5886
|
+
const terrains = Array.isArray(raw.terrains) ? raw.terrains.filter((item) => typeof item === "string") : [];
|
|
5887
|
+
return {
|
|
5888
|
+
ruleType,
|
|
5889
|
+
arity,
|
|
5890
|
+
...connectivity ? { connectivity } : {},
|
|
5891
|
+
terrains,
|
|
5892
|
+
masks,
|
|
5893
|
+
raw
|
|
5894
|
+
};
|
|
5895
|
+
}
|
|
5896
|
+
function exportTileset(entry, spec, options) {
|
|
5897
|
+
const outputs = resolveSpecEntryOutputs(entry, spec);
|
|
5898
|
+
if (!outputs.length) throw new Error(`${spec.styleId}/${spec.assetId} has no downloaded outputs`);
|
|
5899
|
+
const packed = packSprites(
|
|
5900
|
+
outputs.map((output) => ({
|
|
5901
|
+
id: output.role ?? fallbackOutputRole(output.index),
|
|
5902
|
+
path: output.absolutePath
|
|
5903
|
+
})),
|
|
5904
|
+
{ columns: options.columns, order: "input" }
|
|
5905
|
+
);
|
|
5906
|
+
if (packed.skipped.length) {
|
|
5907
|
+
throw new Error(
|
|
5908
|
+
`Cannot export ${spec.styleId}/${spec.assetId}: ` + packed.skipped.map((item) => `${item.id}: ${item.reason}`).join("; ")
|
|
5909
|
+
);
|
|
5910
|
+
}
|
|
5911
|
+
const metadata = entry.providerMetadata?.[entry.provider];
|
|
5912
|
+
const rawRules = isRecord(metadata) ? metadata.tileRules : void 0;
|
|
5913
|
+
const rules = normalizeTileRules(rawRules);
|
|
5914
|
+
const frames = packed.atlas.frames;
|
|
5915
|
+
const generic = buildGeneric(
|
|
5916
|
+
spec,
|
|
5917
|
+
options.imageName,
|
|
5918
|
+
packed.atlas,
|
|
5919
|
+
frames,
|
|
5920
|
+
isRecord(rawRules) ? rawRules : null,
|
|
5921
|
+
rules
|
|
5922
|
+
);
|
|
5923
|
+
if (options.format === "generic") {
|
|
5924
|
+
return {
|
|
5925
|
+
png: packed.png,
|
|
5926
|
+
extension: ".json",
|
|
5927
|
+
document: JSON.stringify(generic, null, 2) + "\n",
|
|
5928
|
+
generic,
|
|
5929
|
+
sources: packed.sources
|
|
5930
|
+
};
|
|
5931
|
+
}
|
|
5932
|
+
assertUniformTiles(generic);
|
|
5933
|
+
if ((rawRules !== void 0 || entry.tileFeature) && !rules) {
|
|
5934
|
+
throw new Error(
|
|
5935
|
+
`${options.format} export needs recognized adjacency rules; use --format generic to retain this provider rule object without guessing`
|
|
5936
|
+
);
|
|
5937
|
+
}
|
|
5938
|
+
if (rules?.arity === 6) {
|
|
5939
|
+
throw new Error(
|
|
5940
|
+
`${options.format} export does not yet map six-edge hex masks safely; use --format generic`
|
|
5941
|
+
);
|
|
5942
|
+
}
|
|
5943
|
+
if (rules?.ruleType === "outline") {
|
|
5944
|
+
throw new Error(
|
|
5945
|
+
`${options.format} export cannot infer adjacency from outline/stamp-only rules; use --format generic`
|
|
5946
|
+
);
|
|
5947
|
+
}
|
|
5948
|
+
if (options.format === "tiled") {
|
|
5949
|
+
return {
|
|
5950
|
+
png: packed.png,
|
|
5951
|
+
extension: ".tsj",
|
|
5952
|
+
document: JSON.stringify(buildTiled(generic), null, 2) + "\n",
|
|
5953
|
+
generic,
|
|
5954
|
+
sources: packed.sources
|
|
5955
|
+
};
|
|
5956
|
+
}
|
|
5957
|
+
return {
|
|
5958
|
+
png: packed.png,
|
|
5959
|
+
extension: ".tres",
|
|
5960
|
+
document: buildGodot(generic, spec),
|
|
5961
|
+
generic,
|
|
5962
|
+
sources: packed.sources
|
|
5963
|
+
};
|
|
5964
|
+
}
|
|
5965
|
+
function buildGeneric(spec, imageName, atlas, frames, providerRules, rules) {
|
|
5966
|
+
return {
|
|
5967
|
+
format: "pixelkiln-tileset",
|
|
5968
|
+
version: 1,
|
|
5969
|
+
name: `${spec.styleId}/${spec.assetId}`,
|
|
5970
|
+
style: spec.styleId,
|
|
5971
|
+
asset: spec.assetId,
|
|
5972
|
+
image: imageName,
|
|
5973
|
+
tile: { ...atlas.cell },
|
|
5151
5974
|
sheet: { ...atlas.sheet, columns: atlas.columns },
|
|
5152
5975
|
tiles: frames.map((frame, index) => {
|
|
5153
5976
|
const sourceIndex = tileIndex(frame.id) ?? index;
|
|
@@ -5301,8 +6124,8 @@ function isRecord(value) {
|
|
|
5301
6124
|
}
|
|
5302
6125
|
|
|
5303
6126
|
// src/pick/salvage-server.ts
|
|
5304
|
-
import { mkdir as
|
|
5305
|
-
import
|
|
6127
|
+
import { mkdir as mkdir6, writeFile as writeFile8, readFile as readFile14 } from "fs/promises";
|
|
6128
|
+
import path19 from "path";
|
|
5306
6129
|
|
|
5307
6130
|
// src/pick/salvage-sheet.ts
|
|
5308
6131
|
var escapeHtml2 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
@@ -5467,7 +6290,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
5467
6290
|
});
|
|
5468
6291
|
const html = renderSalvageSheet(orphans, {
|
|
5469
6292
|
styleId: ctx.styleId,
|
|
5470
|
-
importDir:
|
|
6293
|
+
importDir: path19.relative(process.cwd(), ctx.importDir) || "."
|
|
5471
6294
|
});
|
|
5472
6295
|
const byId = new Map(orphans.map((o) => [o.id, o]));
|
|
5473
6296
|
const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
|
|
@@ -5496,10 +6319,10 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
5496
6319
|
if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
|
|
5497
6320
|
decodePng(buf);
|
|
5498
6321
|
const assetId = idFromPrompt(orphan.prompt, taken);
|
|
5499
|
-
const rel =
|
|
5500
|
-
const outFile =
|
|
5501
|
-
await
|
|
5502
|
-
await
|
|
6322
|
+
const rel = path19.join("_salvaged", `${assetId}.png`);
|
|
6323
|
+
const outFile = path19.resolve(ctx.importDir, rel);
|
|
6324
|
+
await mkdir6(path19.dirname(outFile), { recursive: true });
|
|
6325
|
+
await writeFile8(outFile, buf);
|
|
5503
6326
|
ctx.manifest.assets[assetId] = {
|
|
5504
6327
|
prompt: orphan.prompt,
|
|
5505
6328
|
promptByStyle: {},
|
|
@@ -5525,7 +6348,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
5525
6348
|
error: null,
|
|
5526
6349
|
sourceUrl: orphan.previewUrl,
|
|
5527
6350
|
outputs: [{
|
|
5528
|
-
path: portableOutputPath(outFile,
|
|
6351
|
+
path: portableOutputPath(outFile, path19.dirname(ctx.manifestPath)),
|
|
5529
6352
|
sha256: sha256(buf)
|
|
5530
6353
|
}],
|
|
5531
6354
|
submittedAt: orphan.createdAt,
|
|
@@ -5549,437 +6372,751 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
5549
6372
|
}
|
|
5550
6373
|
}
|
|
5551
6374
|
await applyTags(provider, decisions, existingTags, { onProgress: log2 });
|
|
5552
|
-
const raw = JSON.parse(await
|
|
6375
|
+
const raw = JSON.parse(await readFile14(ctx.manifestPath, "utf8"));
|
|
5553
6376
|
for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
|
|
5554
|
-
await
|
|
6377
|
+
await writeFile8(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
|
|
5555
6378
|
await saveLock(ctx.lockPath, ctx.lock);
|
|
5556
6379
|
return result;
|
|
5557
6380
|
}
|
|
5558
6381
|
});
|
|
5559
6382
|
}
|
|
5560
6383
|
|
|
5561
|
-
// src/
|
|
5562
|
-
import
|
|
5563
|
-
import {
|
|
5564
|
-
import {
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
}
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
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) {
|
|
5576
6538
|
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
5577
6539
|
if (typeof value === "number") {
|
|
5578
|
-
if (!Number.isFinite(value)) throw new Error("
|
|
6540
|
+
if (!Number.isFinite(value)) throw new Error("Recipe numbers must be finite.");
|
|
5579
6541
|
return value;
|
|
5580
6542
|
}
|
|
5581
|
-
if (Array.isArray(value)) return value.map(
|
|
6543
|
+
if (Array.isArray(value)) return value.map(canonical3);
|
|
5582
6544
|
if (typeof value === "object") {
|
|
5583
6545
|
const sorted = {};
|
|
5584
6546
|
for (const key of Object.keys(value).sort()) {
|
|
5585
6547
|
const item = value[key];
|
|
5586
|
-
if (item !== void 0) sorted[key] =
|
|
6548
|
+
if (item !== void 0) sorted[key] = canonical3(item);
|
|
5587
6549
|
}
|
|
5588
6550
|
return sorted;
|
|
5589
6551
|
}
|
|
5590
|
-
throw new Error(`
|
|
6552
|
+
throw new Error(`Recipe metadata cannot contain ${typeof value} values.`);
|
|
5591
6553
|
}
|
|
5592
|
-
function
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
sources: provenance.sources,
|
|
5596
|
-
options: provenance.options,
|
|
5597
|
-
outputs: provenance.outputs
|
|
5598
|
-
})));
|
|
6554
|
+
function recipeDigest(recipe) {
|
|
6555
|
+
const { integrity: _integrity, ...payload } = recipe;
|
|
6556
|
+
return sha256(JSON.stringify(canonical3(payload)));
|
|
5599
6557
|
}
|
|
5600
|
-
function
|
|
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);
|
|
5601
6563
|
let raw;
|
|
5602
6564
|
try {
|
|
5603
|
-
raw = JSON.parse(
|
|
6565
|
+
raw = JSON.parse(await readFile15(absolute, "utf8"));
|
|
5604
6566
|
} catch (error) {
|
|
5605
|
-
throw new Error(
|
|
6567
|
+
throw new Error(`Could not read recipe ${absolute}: ${message3(error)}`, { cause: error });
|
|
5606
6568
|
}
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
|
|
5610
|
-
|
|
5611
|
-
|
|
5612
|
-
|
|
5613
|
-
}
|
|
5614
|
-
}
|
|
5615
|
-
for (const output of raw.outputs) {
|
|
5616
|
-
if (!output || typeof output.path !== "string" || typeof output.sha256 !== "string") {
|
|
5617
|
-
throw new Error(`${absolute} contains an invalid artifact output.`);
|
|
5618
|
-
}
|
|
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
|
+
);
|
|
5619
6575
|
}
|
|
5620
|
-
return
|
|
6576
|
+
return { recipe: parsed.data, path: absolute, dir: path20.dirname(absolute), bundled };
|
|
5621
6577
|
}
|
|
5622
|
-
async function
|
|
5623
|
-
|
|
5624
|
-
|
|
5625
|
-
|
|
5626
|
-
|
|
5627
|
-
|
|
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);
|
|
5628
6585
|
}
|
|
6586
|
+
return out;
|
|
5629
6587
|
}
|
|
5630
|
-
function
|
|
5631
|
-
return
|
|
5632
|
-
}
|
|
5633
|
-
function processIsAlive(pid) {
|
|
5634
|
-
if (!Number.isInteger(pid) || pid < 1) return false;
|
|
5635
|
-
try {
|
|
5636
|
-
process.kill(pid, 0);
|
|
5637
|
-
return true;
|
|
5638
|
-
} catch (error) {
|
|
5639
|
-
return !isCode(error, "ESRCH");
|
|
5640
|
-
}
|
|
6588
|
+
function bundledRecipeRoot() {
|
|
6589
|
+
return fileURLToPath(new URL("../recipes/", import.meta.url));
|
|
5641
6590
|
}
|
|
5642
|
-
function
|
|
5643
|
-
|
|
5644
|
-
|
|
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)
|
|
5645
6597
|
);
|
|
5646
6598
|
}
|
|
5647
|
-
function
|
|
5648
|
-
|
|
5649
|
-
|
|
5650
|
-
|
|
5651
|
-
|
|
5652
|
-
|
|
5653
|
-
}
|
|
5654
|
-
if (raw.format !== "pixelkiln-artifact-transaction" || raw.version !== 1 || !Number.isInteger(raw.pid) || typeof raw.createdAt !== "string" || !Array.isArray(raw.entries) || !raw.entries.length) {
|
|
5655
|
-
throw new Error(`${journal} is not a PixelKiln artifact transaction.`);
|
|
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;
|
|
5656
6605
|
}
|
|
5657
|
-
return
|
|
5658
|
-
}
|
|
5659
|
-
async function removeTransactionFiles(journal) {
|
|
5660
|
-
await rm6(journal, { force: true });
|
|
5661
|
-
await rm6(transactionMarker(journal), { force: true });
|
|
6606
|
+
return 0;
|
|
5662
6607
|
}
|
|
5663
|
-
|
|
5664
|
-
const
|
|
5665
|
-
|
|
5666
|
-
|
|
5667
|
-
await rm6(transactionMarker(journal), { force: true });
|
|
5668
|
-
return;
|
|
5669
|
-
}
|
|
5670
|
-
if (activeTransactions.has(journal)) {
|
|
5671
|
-
throw new Error(`Another artifact write in this process is already using ${journal}.`);
|
|
5672
|
-
}
|
|
5673
|
-
const transaction = parseTransaction(journal, bytes.toString("utf8"));
|
|
5674
|
-
if (transaction.pid !== process.pid && processIsAlive(transaction.pid)) {
|
|
5675
|
-
throw new Error(
|
|
5676
|
-
`Artifact transaction ${journal} is still owned by live process ${transaction.pid}; refusing concurrent recovery.`
|
|
5677
|
-
);
|
|
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.`);
|
|
5678
6612
|
}
|
|
5679
|
-
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
|
|
5683
|
-
|
|
5684
|
-
|
|
5685
|
-
|
|
5686
|
-
const committed = await readOptional(transactionMarker(journal)) !== null;
|
|
5687
|
-
const errors = [];
|
|
5688
|
-
if (committed) {
|
|
5689
|
-
for (const entry of transaction.entries) {
|
|
5690
|
-
if (entry.backup) await remove2(entry.backup, errors);
|
|
5691
|
-
await remove2(entry.stage, errors);
|
|
5692
|
-
}
|
|
5693
|
-
} else {
|
|
5694
|
-
for (const entry of [...transaction.entries].reverse()) {
|
|
5695
|
-
const backup = entry.backup ? await readOptional(entry.backup) : null;
|
|
5696
|
-
const destination = await readOptional(entry.destination);
|
|
5697
|
-
if (backup) {
|
|
5698
|
-
if (destination && digest(destination) !== entry.sha256) {
|
|
5699
|
-
errors.push(
|
|
5700
|
-
`destination changed during interrupted recovery: ${entry.destination}; previous file remains at ${entry.backup}`
|
|
5701
|
-
);
|
|
5702
|
-
continue;
|
|
5703
|
-
}
|
|
5704
|
-
await remove2(entry.destination, errors);
|
|
5705
|
-
try {
|
|
5706
|
-
await rename5(entry.backup, entry.destination);
|
|
5707
|
-
} catch (error) {
|
|
5708
|
-
errors.push(
|
|
5709
|
-
`could not restore ${entry.destination}; previous file remains at ${entry.backup}: ${message(error)}`
|
|
5710
|
-
);
|
|
5711
|
-
}
|
|
5712
|
-
} else if (destination && digest(destination) === entry.sha256) {
|
|
5713
|
-
await remove2(entry.destination, errors);
|
|
5714
|
-
}
|
|
5715
|
-
await remove2(entry.stage, errors);
|
|
5716
|
-
}
|
|
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);
|
|
5717
6620
|
}
|
|
5718
|
-
if (
|
|
5719
|
-
throw new Error(
|
|
5720
|
-
`Artifact transaction recovery needs attention; journal retained at ${journal}: ` + errors.join("; ")
|
|
5721
|
-
);
|
|
6621
|
+
if (target.startsWith(".") || target.startsWith("/") || target.includes("\\")) {
|
|
6622
|
+
throw new Error(`Recipe path not found: ${local}`);
|
|
5722
6623
|
}
|
|
5723
|
-
|
|
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];
|
|
5724
6630
|
}
|
|
5725
|
-
function
|
|
5726
|
-
const
|
|
5727
|
-
|
|
5728
|
-
|
|
5729
|
-
|
|
5730
|
-
|
|
5731
|
-
|
|
5732
|
-
|
|
5733
|
-
|
|
5734
|
-
|
|
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"
|
|
5735
6642
|
};
|
|
5736
|
-
|
|
5737
|
-
|
|
5738
|
-
|
|
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"
|
|
5739
6660
|
}));
|
|
6661
|
+
const integrityStatus = actualIntegrity === loaded.recipe.integrity.digest ? "ok" : "mismatch";
|
|
5740
6662
|
return {
|
|
5741
|
-
format: "pixelkiln-artifact-bundle",
|
|
5742
6663
|
version: 1,
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
|
|
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
|
|
5746
6679
|
};
|
|
5747
6680
|
}
|
|
5748
|
-
function
|
|
5749
|
-
const
|
|
5750
|
-
|
|
5751
|
-
|
|
5752
|
-
|
|
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("/")) }))
|
|
5753
6704
|
];
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
|
|
5757
|
-
|
|
5758
|
-
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
|
|
5763
|
-
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
|
|
5767
|
-
previous = parseArtifactManifest(absoluteManifest, existingManifestBytes.toString("utf8"));
|
|
5768
|
-
const valid = previous.fingerprint === fingerprint(previous);
|
|
5769
|
-
if (!valid) throw new Error("its integrity fingerprint does not match");
|
|
5770
|
-
} catch (error) {
|
|
5771
|
-
throw new Error(
|
|
5772
|
-
`Refusing to replace unrecognized or modified provenance at ${absoluteManifest}: ${message(error)}. Pass { force: true } (CLI: --force) to take ownership of this artifact bundle.`,
|
|
5773
|
-
{ cause: error }
|
|
5774
|
-
);
|
|
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
|
+
}
|
|
5775
6718
|
}
|
|
5776
6719
|
}
|
|
5777
|
-
|
|
5778
|
-
|
|
5779
|
-
|
|
5780
|
-
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
|
|
5787
|
-
|
|
5788
|
-
|
|
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;
|
|
5789
6835
|
}
|
|
5790
|
-
|
|
5791
|
-
|
|
5792
|
-
|
|
5793
|
-
|
|
5794
|
-
|
|
5795
|
-
|
|
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
|
+
}
|
|
5796
6870
|
}
|
|
5797
6871
|
}
|
|
5798
|
-
const
|
|
5799
|
-
return
|
|
5800
|
-
|
|
5801
|
-
|
|
5802
|
-
|
|
5803
|
-
|
|
5804
|
-
|
|
5805
|
-
|
|
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
|
+
};
|
|
5806
6885
|
}
|
|
5807
|
-
async function
|
|
6886
|
+
async function measureImageQuality(file) {
|
|
6887
|
+
const absolute = path21.resolve(file);
|
|
6888
|
+
let bytes;
|
|
5808
6889
|
try {
|
|
5809
|
-
await
|
|
6890
|
+
bytes = await readFile16(absolute);
|
|
5810
6891
|
} catch (error) {
|
|
5811
|
-
|
|
6892
|
+
throw new Error(`Cannot read quality image ${absolute}: ${message4(error)}`, { cause: error });
|
|
5812
6893
|
}
|
|
5813
|
-
|
|
5814
|
-
|
|
5815
|
-
|
|
5816
|
-
|
|
5817
|
-
if (artifact.promoted) await remove2(artifact.destination, errors);
|
|
5818
|
-
}
|
|
5819
|
-
for (const artifact of [...prepared].reverse()) {
|
|
5820
|
-
if (!artifact.backup || !artifact.backedUp) continue;
|
|
5821
|
-
try {
|
|
5822
|
-
await rename5(artifact.backup, artifact.destination);
|
|
5823
|
-
artifact.backedUp = false;
|
|
5824
|
-
} catch (error) {
|
|
5825
|
-
errors.push(
|
|
5826
|
-
`could not restore ${artifact.destination}; previous file remains at ${artifact.backup}: ${message(error)}`
|
|
5827
|
-
);
|
|
5828
|
-
}
|
|
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 });
|
|
5829
6898
|
}
|
|
5830
|
-
|
|
5831
|
-
|
|
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
|
+
);
|
|
5832
6906
|
}
|
|
5833
|
-
|
|
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
|
+
}));
|
|
5834
6919
|
}
|
|
5835
|
-
|
|
5836
|
-
|
|
5837
|
-
|
|
5838
|
-
|
|
5839
|
-
|
|
5840
|
-
|
|
5841
|
-
|
|
5842
|
-
|
|
5843
|
-
|
|
5844
|
-
|
|
5845
|
-
}
|
|
5846
|
-
destinations.add(artifact.destination);
|
|
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
|
+
);
|
|
5847
6930
|
}
|
|
5848
|
-
|
|
5849
|
-
|
|
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.`);
|
|
5850
6935
|
}
|
|
5851
|
-
const
|
|
5852
|
-
|
|
5853
|
-
|
|
5854
|
-
try {
|
|
5855
|
-
const current = await readFile14(artifact.destination);
|
|
5856
|
-
if (current.equals(artifact.data)) {
|
|
5857
|
-
unchanged.push(artifact.destination);
|
|
5858
|
-
} else {
|
|
5859
|
-
changed.push({ ...artifact, existed: true, backedUp: false, promoted: false });
|
|
5860
|
-
}
|
|
5861
|
-
} catch (error) {
|
|
5862
|
-
if (!isCode(error, "ENOENT")) throw error;
|
|
5863
|
-
changed.push({ ...artifact, existed: false, backedUp: false, promoted: false });
|
|
5864
|
-
}
|
|
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}.`);
|
|
5865
6939
|
}
|
|
5866
|
-
|
|
5867
|
-
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
|
|
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
|
+
};
|
|
5879
6960
|
}
|
|
6961
|
+
cases.push({
|
|
6962
|
+
id: input.id,
|
|
6963
|
+
file: portableRelative2(root, input.file),
|
|
6964
|
+
...record ? { record } : {},
|
|
6965
|
+
expected,
|
|
6966
|
+
tolerances: input.tolerances
|
|
6967
|
+
});
|
|
5880
6968
|
}
|
|
5881
|
-
const
|
|
5882
|
-
|
|
5883
|
-
|
|
5884
|
-
|
|
5885
|
-
|
|
5886
|
-
|
|
5887
|
-
|
|
5888
|
-
|
|
5889
|
-
|
|
5890
|
-
|
|
5891
|
-
|
|
5892
|
-
...artifact.backup ? { backup: artifact.backup } : {},
|
|
5893
|
-
sha256: digest(artifact.data)
|
|
5894
|
-
}))
|
|
5895
|
-
};
|
|
5896
|
-
try {
|
|
5897
|
-
await writeFile8(journal, JSON.stringify(transaction, null, 2) + "\n", { flag: "wx" });
|
|
5898
|
-
activeTransactions.add(journal);
|
|
5899
|
-
} catch (error) {
|
|
5900
|
-
throw new Error(
|
|
5901
|
-
`Could not start durable artifact transaction at ${journal}: ${message(error)}`,
|
|
5902
|
-
{ cause: error }
|
|
5903
|
-
);
|
|
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.`);
|
|
5904
6980
|
}
|
|
5905
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;
|
|
5906
6988
|
try {
|
|
5907
|
-
|
|
5908
|
-
await mkdir6(path18.dirname(artifact.destination), { recursive: true });
|
|
5909
|
-
await options.beforeStage?.(artifact.destination, index);
|
|
5910
|
-
await writeFile8(artifact.stage, artifact.data, { flag: "wx" });
|
|
5911
|
-
}
|
|
6989
|
+
raw = JSON.parse(await readFile16(absolute, "utf8"));
|
|
5912
6990
|
} catch (error) {
|
|
5913
|
-
|
|
5914
|
-
if (journal && !cleanupErrors2.length) await removeTransactionFiles(journal);
|
|
5915
|
-
if (journal) activeTransactions.delete(journal);
|
|
5916
|
-
const suffix = cleanupErrors2.length ? ` Cleanup also failed: ${cleanupErrors2.join("; ")}` : "";
|
|
5917
|
-
throw new Error(`Could not stage artifact bundle: ${message(error)}.${suffix}`, { cause: error });
|
|
6991
|
+
throw new Error(`Could not read quality baseline ${absolute}: ${message4(error)}`, { cause: error });
|
|
5918
6992
|
}
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
|
|
5922
|
-
|
|
5923
|
-
|
|
5924
|
-
|
|
5925
|
-
}
|
|
5926
|
-
for (const [index, artifact] of changed.entries()) {
|
|
5927
|
-
await options.beforePromote?.(artifact.destination, index);
|
|
5928
|
-
await rename5(artifact.stage, artifact.destination);
|
|
5929
|
-
artifact.stage = void 0;
|
|
5930
|
-
artifact.promoted = true;
|
|
5931
|
-
}
|
|
5932
|
-
if (journal) {
|
|
5933
|
-
await writeFile8(transactionMarker(journal), `${token}
|
|
5934
|
-
`, { flag: "wx" });
|
|
5935
|
-
durableCommit = true;
|
|
5936
|
-
await options.afterCommit?.();
|
|
5937
|
-
}
|
|
5938
|
-
} catch (error) {
|
|
5939
|
-
if (durableCommit) {
|
|
5940
|
-
if (journal) activeTransactions.delete(journal);
|
|
5941
|
-
throw new Error(
|
|
5942
|
-
`Artifact bundle was committed, but final cleanup was interrupted: ${message(error)}. The next write will finish cleanup from ${journal}.`,
|
|
5943
|
-
{ cause: error }
|
|
5944
|
-
);
|
|
5945
|
-
}
|
|
5946
|
-
const rollbackErrors = await rollback(changed);
|
|
5947
|
-
if (journal && !rollbackErrors.length) await removeTransactionFiles(journal);
|
|
5948
|
-
if (journal) activeTransactions.delete(journal);
|
|
5949
|
-
const suffix = rollbackErrors.length ? ` Rollback needs attention: ${rollbackErrors.join("; ")}` : " Previous bundle restored.";
|
|
5950
|
-
throw new Error(`Could not promote artifact bundle: ${message(error)}.${suffix}`, { cause: error });
|
|
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
|
+
);
|
|
5951
6999
|
}
|
|
5952
|
-
|
|
5953
|
-
|
|
5954
|
-
|
|
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
|
+
);
|
|
5955
7010
|
}
|
|
5956
|
-
if (
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
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})`
|
|
5960
7017
|
);
|
|
5961
7018
|
}
|
|
5962
|
-
|
|
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 = [];
|
|
5963
7068
|
try {
|
|
5964
|
-
await
|
|
5965
|
-
|
|
5966
|
-
|
|
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));
|
|
5967
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
|
+
});
|
|
5968
7096
|
}
|
|
7097
|
+
const failed = cases.filter((entry) => entry.status === "fail").length;
|
|
5969
7098
|
return {
|
|
5970
|
-
|
|
5971
|
-
|
|
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
|
|
5972
7109
|
};
|
|
5973
7110
|
}
|
|
5974
7111
|
|
|
5975
7112
|
// src/cli.ts
|
|
5976
7113
|
var log = (msg = "") => console.log(msg);
|
|
5977
7114
|
async function provenanceFile(id, file) {
|
|
5978
|
-
const absolute =
|
|
7115
|
+
const absolute = path22.resolve(file);
|
|
5979
7116
|
return {
|
|
5980
7117
|
id,
|
|
5981
7118
|
path: absolute,
|
|
5982
|
-
sha256:
|
|
7119
|
+
sha256: existsSync17(absolute) ? await sha256File(absolute) : null,
|
|
5983
7120
|
included: true
|
|
5984
7121
|
};
|
|
5985
7122
|
}
|
|
@@ -6006,7 +7143,14 @@ var VALUE_FLAGS = [
|
|
|
6006
7143
|
"--sigma",
|
|
6007
7144
|
"--workspace",
|
|
6008
7145
|
"--provider",
|
|
6009
|
-
"--account"
|
|
7146
|
+
"--account",
|
|
7147
|
+
"--palette",
|
|
7148
|
+
"--fixer-python",
|
|
7149
|
+
"--fixer-revision",
|
|
7150
|
+
"--min-grid-confidence",
|
|
7151
|
+
"--reviewer",
|
|
7152
|
+
"--note",
|
|
7153
|
+
"--model-root"
|
|
6010
7154
|
];
|
|
6011
7155
|
var BOOL_FLAGS = [
|
|
6012
7156
|
"--force",
|
|
@@ -6045,6 +7189,9 @@ var COMMANDS = [
|
|
|
6045
7189
|
"tag",
|
|
6046
7190
|
"balance",
|
|
6047
7191
|
"status",
|
|
7192
|
+
"quality",
|
|
7193
|
+
"refine",
|
|
7194
|
+
"recipe",
|
|
6048
7195
|
"workspace",
|
|
6049
7196
|
"help",
|
|
6050
7197
|
"--help",
|
|
@@ -6053,6 +7200,9 @@ var COMMANDS = [
|
|
|
6053
7200
|
"-v"
|
|
6054
7201
|
];
|
|
6055
7202
|
var WORKSPACE_SUBCOMMANDS = ["add", "remove", "list", "status", "claims"];
|
|
7203
|
+
var REFINE_SUBCOMMANDS = ["run", "approve", "check"];
|
|
7204
|
+
var RECIPE_SUBCOMMANDS = ["list", "inspect", "install", "verify"];
|
|
7205
|
+
var QUALITY_SUBCOMMANDS = ["snapshot", "check"];
|
|
6056
7206
|
function parseArgs(argv) {
|
|
6057
7207
|
const [command = "help"] = argv;
|
|
6058
7208
|
if (!COMMANDS.includes(command)) {
|
|
@@ -6061,7 +7211,36 @@ function parseArgs(argv) {
|
|
|
6061
7211
|
let rest = argv.slice(1);
|
|
6062
7212
|
let subcommand;
|
|
6063
7213
|
let target;
|
|
6064
|
-
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") {
|
|
6065
7244
|
subcommand = rest[0];
|
|
6066
7245
|
if (subcommand === void 0 || subcommand.startsWith("-")) {
|
|
6067
7246
|
throw new Error(`workspace needs a subcommand: ${WORKSPACE_SUBCOMMANDS.join(", ")}`);
|
|
@@ -6081,6 +7260,14 @@ function parseArgs(argv) {
|
|
|
6081
7260
|
}
|
|
6082
7261
|
rest = rest.slice(1);
|
|
6083
7262
|
}
|
|
7263
|
+
} else if (command === "refine") {
|
|
7264
|
+
subcommand = rest[0]?.startsWith("-") || rest[0] === void 0 ? "run" : rest[0];
|
|
7265
|
+
if (!REFINE_SUBCOMMANDS.includes(subcommand)) {
|
|
7266
|
+
throw new Error(
|
|
7267
|
+
`Unknown refine subcommand "${subcommand}". Known: ${REFINE_SUBCOMMANDS.join(", ")}`
|
|
7268
|
+
);
|
|
7269
|
+
}
|
|
7270
|
+
if (rest[0] === subcommand) rest = rest.slice(1);
|
|
6084
7271
|
}
|
|
6085
7272
|
for (let i = 0; i < rest.length; i++) {
|
|
6086
7273
|
const token = rest[i];
|
|
@@ -6155,10 +7342,16 @@ function parseArgs(argv) {
|
|
|
6155
7342
|
}
|
|
6156
7343
|
return value;
|
|
6157
7344
|
};
|
|
7345
|
+
const rawGridConfidence = get("--min-grid-confidence");
|
|
7346
|
+
if (rawGridConfidence !== void 0 && rawGridConfidence !== "low" && rawGridConfidence !== "medium" && rawGridConfidence !== "high") {
|
|
7347
|
+
throw new Error(
|
|
7348
|
+
`--min-grid-confidence must be low, medium, or high, got "${rawGridConfidence}"`
|
|
7349
|
+
);
|
|
7350
|
+
}
|
|
6158
7351
|
return {
|
|
6159
7352
|
command,
|
|
6160
7353
|
manifest,
|
|
6161
|
-
lock: get("--lock") ??
|
|
7354
|
+
lock: get("--lock") ?? path22.join(path22.dirname(path22.resolve(manifest)), "pixelkiln.lock.json"),
|
|
6162
7355
|
explicitLock: get("--lock"),
|
|
6163
7356
|
styles: list("--style"),
|
|
6164
7357
|
assets: list("--only"),
|
|
@@ -6189,6 +7382,13 @@ function parseArgs(argv) {
|
|
|
6189
7382
|
minTransparency: numberOption("--min-transparency", { min: 0, max: 1 }),
|
|
6190
7383
|
maxColors: numberOption("--max-colors", { min: 1, integer: true }),
|
|
6191
7384
|
sigma: numberOption("--sigma", { min: Number.EPSILON }),
|
|
7385
|
+
palette: list("--palette"),
|
|
7386
|
+
fixerPython: get("--fixer-python"),
|
|
7387
|
+
fixerRevision: get("--fixer-revision"),
|
|
7388
|
+
minGridConfidence: rawGridConfidence,
|
|
7389
|
+
reviewer: get("--reviewer"),
|
|
7390
|
+
note: get("--note"),
|
|
7391
|
+
modelRoot: get("--model-root"),
|
|
6192
7392
|
subcommand,
|
|
6193
7393
|
workspace: get("--workspace"),
|
|
6194
7394
|
target,
|
|
@@ -6215,6 +7415,10 @@ Commands
|
|
|
6215
7415
|
salvage Triage account objects no lockfile claims. Recovers usable art.
|
|
6216
7416
|
One session per matching style unless --style forces a single one.
|
|
6217
7417
|
audit Measure how consistently a style's assets hold together. Offline.
|
|
7418
|
+
refine Recover a native pixel grid, enforce a final palette, audit it,
|
|
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.
|
|
6218
7422
|
cache Inspect local recovery/object-hash caches; optionally verify or prune.
|
|
6219
7423
|
pack Composite sprites into a PNG/atlas/provenance bundle. Offline.
|
|
6220
7424
|
mount Write a style's sprites into their declared cells of an existing
|
|
@@ -6232,7 +7436,7 @@ Commands
|
|
|
6232
7436
|
Options
|
|
6233
7437
|
--columns <n> pack/export: sprites or tiles per row (default: near-square)
|
|
6234
7438
|
--port <n> Local review-server port (default: choose a free port)
|
|
6235
|
-
--inputs <path> pack: JSON
|
|
7439
|
+
--inputs <path> pack/quality snapshot: JSON input list; needs --out
|
|
6236
7440
|
--format <format> export: generic (default), tiled, or godot
|
|
6237
7441
|
--output-role <r> pack: include only this output role (repeatable)
|
|
6238
7442
|
--primary-only pack: include only unambiguous primary/single outputs
|
|
@@ -6240,16 +7444,23 @@ Options
|
|
|
6240
7444
|
--min-transparency <0..1> audit: minimum transparent canvas share
|
|
6241
7445
|
--max-colors <n> audit: maximum distinct opaque colors
|
|
6242
7446
|
--sigma <n> audit: relative outlier cutoff (default: 1.5)
|
|
7447
|
+
--palette <hexes> refine: final comma-separated #rrggbb colors (repeatable)
|
|
7448
|
+
--fixer-python <path> refine: Python with Pixel Art Fixer installed
|
|
7449
|
+
--fixer-revision <sha> refine: Pixel Art Fixer revision to record
|
|
7450
|
+
--min-grid-confidence <level> refine: high (default), medium, or low
|
|
7451
|
+
--reviewer <name> refine approve: human reviewer recorded in provenance
|
|
7452
|
+
--note <text> refine approve: optional review note
|
|
7453
|
+
--model-root <dir> recipe verify: also hash required local model files
|
|
6243
7454
|
--prune cache: remove invalid/unreferenced local cache data
|
|
6244
7455
|
--manifest <path> Default: pixelkiln.manifest.json
|
|
6245
7456
|
--lock <path> Default: pixelkiln.lock.json beside the manifest
|
|
6246
7457
|
--style a,b Restrict to these styles
|
|
6247
7458
|
--only id1,id2 Restrict to these asset ids
|
|
6248
7459
|
--budget <n> Refuse to spend more than n provider cost units
|
|
6249
|
-
--force Regenerate;
|
|
7460
|
+
--force Regenerate; also replace changed recipe files or quality baselines
|
|
6250
7461
|
--dry-run Never spend; doctor also skips provider connectivity
|
|
6251
7462
|
--all salvage --dry-run: list every unclaimed object, not just the first 30
|
|
6252
|
-
--json Machine-readable output where supported, including
|
|
7463
|
+
--json Machine-readable output where supported, including quality checks
|
|
6253
7464
|
--check plan/audit/cache: exit nonzero when the selected state is unsafe
|
|
6254
7465
|
--yes, -y Skip the confirmation prompt
|
|
6255
7466
|
--no-open Do not auto-open the browser during pick
|
|
@@ -6257,7 +7468,7 @@ Options
|
|
|
6257
7468
|
--claims a.json,b Other projects' lockfiles (salvage; required if account is shared)
|
|
6258
7469
|
--workspace <path> Workspace catalog (default: pixelkiln.workspace.json). Also
|
|
6259
7470
|
derives salvage's claim set instead of repeated --claims.
|
|
6260
|
-
--from <
|
|
7471
|
+
--from <path> Source for init/refine, or baseline for quality check
|
|
6261
7472
|
--write-prompts adopt: recover prompts into the manifest
|
|
6262
7473
|
|
|
6263
7474
|
Examples
|
|
@@ -6269,6 +7480,14 @@ Examples
|
|
|
6269
7480
|
pixelkiln pack --inputs sprites.json --out dist/sheet # no manifest needed
|
|
6270
7481
|
pixelkiln mount --style ground
|
|
6271
7482
|
pixelkiln export --style ground --only terrain --format tiled
|
|
7483
|
+
pixelkiln refine --from source.png --out final.png --palette "#101820,#f2aa4c"
|
|
7484
|
+
pixelkiln refine approve --from final.pixelkiln.json --reviewer "Your Name"
|
|
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
|
|
6272
7491
|
pixelkiln workspace add ../other-game/pixelkiln.manifest.json
|
|
6273
7492
|
pixelkiln workspace status --json
|
|
6274
7493
|
pixelkiln salvage --workspace pixelkiln.workspace.json
|
|
@@ -6313,10 +7532,10 @@ async function confirm(question, auto) {
|
|
|
6313
7532
|
return answer === "y" || answer === "yes";
|
|
6314
7533
|
}
|
|
6315
7534
|
async function requireCompleteWorkspaceClaims(workspacePath) {
|
|
6316
|
-
if (!
|
|
7535
|
+
if (!existsSync17(workspacePath)) {
|
|
6317
7536
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
6318
7537
|
}
|
|
6319
|
-
const dir =
|
|
7538
|
+
const dir = path22.dirname(path22.resolve(workspacePath));
|
|
6320
7539
|
const ws = await loadWorkspace(workspacePath);
|
|
6321
7540
|
const diagnostics = validateWorkspace(ws, dir);
|
|
6322
7541
|
const errors = diagnostics.filter((d) => d.level === "error");
|
|
@@ -6337,13 +7556,214 @@ async function main() {
|
|
|
6337
7556
|
}
|
|
6338
7557
|
if (args.command === "--version" || args.command === "-v") {
|
|
6339
7558
|
const pkg = JSON.parse(
|
|
6340
|
-
await
|
|
7559
|
+
await readFile17(new URL("../package.json", import.meta.url), "utf8")
|
|
6341
7560
|
);
|
|
6342
7561
|
log(`${pkg.name} ${pkg.version}`);
|
|
6343
7562
|
return;
|
|
6344
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
|
+
}
|
|
7699
|
+
if (args.command === "refine") {
|
|
7700
|
+
if (!args.from) {
|
|
7701
|
+
throw new Error(
|
|
7702
|
+
args.subcommand === "run" ? "refine needs --from <source.png>." : `refine ${args.subcommand} needs --from <output.pixelkiln.json>.`
|
|
7703
|
+
);
|
|
7704
|
+
}
|
|
7705
|
+
if (args.subcommand === "run") {
|
|
7706
|
+
if (!args.out) throw new Error("refine needs --out <final.png>.");
|
|
7707
|
+
if (!args.palette.length) {
|
|
7708
|
+
throw new Error("refine needs an explicit final palette via --palette <#rrggbb,...>.");
|
|
7709
|
+
}
|
|
7710
|
+
const result2 = await refineAsset({
|
|
7711
|
+
source: args.from,
|
|
7712
|
+
output: args.out,
|
|
7713
|
+
palette: args.palette,
|
|
7714
|
+
fixerPython: args.fixerPython,
|
|
7715
|
+
fixerRevision: args.fixerRevision,
|
|
7716
|
+
minGridConfidence: args.minGridConfidence,
|
|
7717
|
+
minTransparency: args.minTransparency,
|
|
7718
|
+
force: args.force
|
|
7719
|
+
});
|
|
7720
|
+
if (args.json) {
|
|
7721
|
+
log(JSON.stringify(result2, null, 2));
|
|
7722
|
+
} else {
|
|
7723
|
+
log(
|
|
7724
|
+
` recovered ${result2.detection.columns}x${result2.detection.rows} native grid (${result2.detection.confidence}: ${result2.detection.consensus})`
|
|
7725
|
+
);
|
|
7726
|
+
log(` enforced ${result2.palette.length}-color palette without dithering`);
|
|
7727
|
+
log(` wrote ${path22.relative(process.cwd(), result2.output)}`);
|
|
7728
|
+
log(` quality record: ${path22.relative(process.cwd(), result2.record)}`);
|
|
7729
|
+
log(`
|
|
7730
|
+
Automated checks passed. Human 1\xD7 review is still required:`);
|
|
7731
|
+
log(
|
|
7732
|
+
` pixelkiln refine approve --from ${path22.relative(process.cwd(), result2.record)} --reviewer "Your Name"`
|
|
7733
|
+
);
|
|
7734
|
+
}
|
|
7735
|
+
return;
|
|
7736
|
+
}
|
|
7737
|
+
if (args.subcommand === "approve") {
|
|
7738
|
+
if (!args.reviewer?.trim()) {
|
|
7739
|
+
throw new Error("refine approve needs --reviewer <name>.");
|
|
7740
|
+
}
|
|
7741
|
+
log(` Inspect the final PNG at 1\xD7 and integer zoom.`);
|
|
7742
|
+
log(` Confirm crisp edges, readable forms, palette separation, alpha, and seams.`);
|
|
7743
|
+
if (!await confirm(" Record this asset as human-approved?", args.yes)) {
|
|
7744
|
+
log(" approval not recorded");
|
|
7745
|
+
return;
|
|
7746
|
+
}
|
|
7747
|
+
const result2 = await approveQualityRecord(args.from, {
|
|
7748
|
+
reviewer: args.reviewer,
|
|
7749
|
+
note: args.note
|
|
7750
|
+
});
|
|
7751
|
+
if (args.json) log(JSON.stringify(result2, null, 2));
|
|
7752
|
+
else log(` approved ${path22.relative(process.cwd(), result2.output)} by ${args.reviewer.trim()}`);
|
|
7753
|
+
return;
|
|
7754
|
+
}
|
|
7755
|
+
const result = await checkQualityRecord(args.from);
|
|
7756
|
+
if (args.json) {
|
|
7757
|
+
log(JSON.stringify(result, null, 2));
|
|
7758
|
+
} else {
|
|
7759
|
+
log(` ${result.safe ? "release-ready" : "not release-ready"}: ${path22.relative(process.cwd(), result.output || result.record)}`);
|
|
7760
|
+
for (const reason of result.reasons) log(` ${reason}`);
|
|
7761
|
+
}
|
|
7762
|
+
if (!result.safe) process.exitCode = 1;
|
|
7763
|
+
return;
|
|
7764
|
+
}
|
|
6345
7765
|
if (args.command === "balance") {
|
|
6346
|
-
loadEnvFiles(
|
|
7766
|
+
loadEnvFiles(path22.dirname(path22.resolve(args.manifest)));
|
|
6347
7767
|
loadEnvFiles(process.cwd());
|
|
6348
7768
|
const loaded2 = await loadManifest(args.manifest);
|
|
6349
7769
|
const p = createProvider(loaded2.manifest.provider, "online");
|
|
@@ -6355,31 +7775,31 @@ async function main() {
|
|
|
6355
7775
|
}
|
|
6356
7776
|
if (args.command === "init") {
|
|
6357
7777
|
if (!args.from) throw new Error("init needs --from <dir> pointing at your existing PNGs.");
|
|
6358
|
-
const root =
|
|
6359
|
-
if (!
|
|
7778
|
+
const root = path22.resolve(args.from);
|
|
7779
|
+
if (!existsSync17(root)) throw new Error(`No directory at ${root}`);
|
|
6360
7780
|
const generator = args.generator ?? "map";
|
|
6361
7781
|
if (generator !== "1dir" && generator !== "map") {
|
|
6362
7782
|
throw new Error(`--generator must be "1dir" or "map", got "${args.generator}".`);
|
|
6363
7783
|
}
|
|
6364
|
-
const target =
|
|
7784
|
+
const target = path22.resolve(args.out ?? "pixelkiln.manifest.json");
|
|
6365
7785
|
const { assets, skipped } = await scanAssets(root, { exclude: args.exclude });
|
|
6366
7786
|
if (!assets.length) throw new Error(`No PNGs found under ${root}`);
|
|
6367
7787
|
const manifest = buildManifest(
|
|
6368
|
-
args.name ??
|
|
7788
|
+
args.name ?? path22.basename(path22.dirname(target)),
|
|
6369
7789
|
args.styles[0] ?? "base",
|
|
6370
7790
|
generator,
|
|
6371
|
-
|
|
7791
|
+
path22.relative(path22.dirname(target), root) || ".",
|
|
6372
7792
|
assets
|
|
6373
7793
|
);
|
|
6374
7794
|
await writeManifestFile(target, manifest);
|
|
6375
|
-
log(` scanned ${assets.length} PNG(s) under ${
|
|
7795
|
+
log(` scanned ${assets.length} PNG(s) under ${path22.relative(process.cwd(), root)}`);
|
|
6376
7796
|
if (skipped.length) log(` skipped ${skipped.length} unreadable file(s)`);
|
|
6377
|
-
log(` wrote ${
|
|
7797
|
+
log(` wrote ${path22.relative(process.cwd(), target)}`);
|
|
6378
7798
|
log(`
|
|
6379
7799
|
Prompts are intentionally empty. To recover the real ones from your`);
|
|
6380
7800
|
log(` PixelLab account instead of inventing them:`);
|
|
6381
7801
|
log(`
|
|
6382
|
-
pixelkiln adopt --manifest ${
|
|
7802
|
+
pixelkiln adopt --manifest ${path22.relative(process.cwd(), target)} --write-prompts
|
|
6383
7803
|
`);
|
|
6384
7804
|
return;
|
|
6385
7805
|
}
|
|
@@ -6427,10 +7847,10 @@ async function main() {
|
|
|
6427
7847
|
if (args.primaryOnly || args.outputRoles.length) {
|
|
6428
7848
|
throw new Error("--primary-only and --output-role require manifest-driven pack");
|
|
6429
7849
|
}
|
|
6430
|
-
const raw = JSON.parse(await
|
|
7850
|
+
const raw = JSON.parse(await readFile17(path22.resolve(args.inputs), "utf8"));
|
|
6431
7851
|
const inputs = resolvePackInputs(raw, args.inputs);
|
|
6432
7852
|
const { png, atlas, skipped, sources } = packSprites(inputs, { columns: args.columns });
|
|
6433
|
-
const base =
|
|
7853
|
+
const base = path22.resolve(args.out.replace(/\.png$/, ""));
|
|
6434
7854
|
const outputs = [
|
|
6435
7855
|
{ path: `${base}.png`, data: png },
|
|
6436
7856
|
{ path: `${base}.json`, data: JSON.stringify(atlas, null, 2) + "\n" }
|
|
@@ -6443,17 +7863,17 @@ async function main() {
|
|
|
6443
7863
|
log(
|
|
6444
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`
|
|
6445
7865
|
);
|
|
6446
|
-
log(` ${
|
|
7866
|
+
log(` ${path22.relative(process.cwd(), base)}.png + .json + .pixelkiln.json`);
|
|
6447
7867
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
6448
7868
|
return;
|
|
6449
7869
|
}
|
|
6450
7870
|
if (args.command === "workspace") {
|
|
6451
|
-
const workspacePath =
|
|
6452
|
-
const dir =
|
|
7871
|
+
const workspacePath = path22.resolve(args.workspace ?? "pixelkiln.workspace.json");
|
|
7872
|
+
const dir = path22.dirname(workspacePath);
|
|
6453
7873
|
if (args.subcommand === "add") {
|
|
6454
|
-
const manifestPath =
|
|
7874
|
+
const manifestPath = path22.resolve(args.target);
|
|
6455
7875
|
const loadedTarget = await loadManifest(manifestPath);
|
|
6456
|
-
const lockPath = args.explicitLock ?
|
|
7876
|
+
const lockPath = args.explicitLock ? path22.resolve(args.explicitLock) : path22.join(path22.dirname(manifestPath), "pixelkiln.lock.json");
|
|
6457
7877
|
const ws = await loadWorkspace(workspacePath);
|
|
6458
7878
|
const id = args.name ?? loadedTarget.manifest.name;
|
|
6459
7879
|
if (ws.projects.some((p) => p.id === id)) {
|
|
@@ -6473,10 +7893,10 @@ async function main() {
|
|
|
6473
7893
|
...args.account ? { account: args.account } : {}
|
|
6474
7894
|
};
|
|
6475
7895
|
await saveWorkspace(workspacePath, { version: 1, projects: [...ws.projects, project] });
|
|
6476
|
-
log(` registered "${id}" in ${
|
|
7896
|
+
log(` registered "${id}" in ${path22.relative(process.cwd(), workspacePath)}`);
|
|
6477
7897
|
log(` manifest: ${project.manifest}`);
|
|
6478
7898
|
log(` lock: ${project.lock}`);
|
|
6479
|
-
if (!
|
|
7899
|
+
if (!existsSync17(lockPath)) {
|
|
6480
7900
|
log(
|
|
6481
7901
|
` warning: no lockfile there yet \u2014 this project contributes no claims until one is generated`
|
|
6482
7902
|
);
|
|
@@ -6485,7 +7905,7 @@ async function main() {
|
|
|
6485
7905
|
}
|
|
6486
7906
|
if (args.subcommand === "remove") {
|
|
6487
7907
|
const ws = await loadWorkspace(workspacePath);
|
|
6488
|
-
const resolvedTarget =
|
|
7908
|
+
const resolvedTarget = path22.resolve(args.target);
|
|
6489
7909
|
const match = ws.projects.find(
|
|
6490
7910
|
(p) => p.id === args.target || resolveProject(dir, p).manifestPath === resolvedTarget
|
|
6491
7911
|
);
|
|
@@ -6496,11 +7916,11 @@ async function main() {
|
|
|
6496
7916
|
version: 1,
|
|
6497
7917
|
projects: ws.projects.filter((p) => p !== match)
|
|
6498
7918
|
});
|
|
6499
|
-
log(` removed "${match.id}" from ${
|
|
7919
|
+
log(` removed "${match.id}" from ${path22.relative(process.cwd(), workspacePath)}`);
|
|
6500
7920
|
return;
|
|
6501
7921
|
}
|
|
6502
7922
|
if (args.subcommand === "list") {
|
|
6503
|
-
if (!
|
|
7923
|
+
if (!existsSync17(workspacePath)) {
|
|
6504
7924
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
6505
7925
|
}
|
|
6506
7926
|
const ws = await loadWorkspace(workspacePath);
|
|
@@ -6508,9 +7928,9 @@ async function main() {
|
|
|
6508
7928
|
if (args.json) {
|
|
6509
7929
|
log(JSON.stringify({ version: 1, workspace: workspacePath, projects: ws.projects, diagnostics }, null, 2));
|
|
6510
7930
|
} else if (!ws.projects.length) {
|
|
6511
|
-
log(` no projects registered in ${
|
|
7931
|
+
log(` no projects registered in ${path22.relative(process.cwd(), workspacePath)}`);
|
|
6512
7932
|
} else {
|
|
6513
|
-
log(` ${ws.projects.length} project(s) in ${
|
|
7933
|
+
log(` ${ws.projects.length} project(s) in ${path22.relative(process.cwd(), workspacePath)}:`);
|
|
6514
7934
|
for (const p of ws.projects) {
|
|
6515
7935
|
log(` ${p.id.padEnd(24)} ${p.manifest.padEnd(40)} (${p.provider}${p.account ? `, ${p.account}` : ""})`);
|
|
6516
7936
|
}
|
|
@@ -6520,7 +7940,7 @@ async function main() {
|
|
|
6520
7940
|
return;
|
|
6521
7941
|
}
|
|
6522
7942
|
if (args.subcommand === "status") {
|
|
6523
|
-
if (!
|
|
7943
|
+
if (!existsSync17(workspacePath)) {
|
|
6524
7944
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
6525
7945
|
}
|
|
6526
7946
|
const ws = await loadWorkspace(workspacePath);
|
|
@@ -6528,7 +7948,7 @@ async function main() {
|
|
|
6528
7948
|
if (args.json) {
|
|
6529
7949
|
log(JSON.stringify({ ...report, workspace: workspacePath }, null, 2));
|
|
6530
7950
|
} else {
|
|
6531
|
-
log(` workspace: ${
|
|
7951
|
+
log(` workspace: ${path22.relative(process.cwd(), workspacePath)}`);
|
|
6532
7952
|
for (const p of report.projects) {
|
|
6533
7953
|
if (p.error) {
|
|
6534
7954
|
log(`
|
|
@@ -6572,14 +7992,14 @@ async function main() {
|
|
|
6572
7992
|
return;
|
|
6573
7993
|
}
|
|
6574
7994
|
}
|
|
6575
|
-
if (!
|
|
7995
|
+
if (!existsSync17(path22.resolve(args.manifest))) {
|
|
6576
7996
|
throw new Error(
|
|
6577
|
-
`No manifest at ${
|
|
7997
|
+
`No manifest at ${path22.resolve(args.manifest)}. Pass --manifest, or run \`pixelkiln init --from <dir>\`.`
|
|
6578
7998
|
);
|
|
6579
7999
|
}
|
|
6580
|
-
const manifestDir =
|
|
8000
|
+
const manifestDir = path22.dirname(path22.resolve(args.manifest));
|
|
6581
8001
|
const envFiles = [...loadEnvFiles(manifestDir)];
|
|
6582
|
-
if (
|
|
8002
|
+
if (path22.resolve(process.cwd()) !== manifestDir) envFiles.push(...loadEnvFiles(process.cwd()));
|
|
6583
8003
|
const loaded = await loadManifest(args.manifest);
|
|
6584
8004
|
const estimator = createProvider(loaded.manifest.provider, "offline");
|
|
6585
8005
|
const specs = await resolveSpecs(loaded, {
|
|
@@ -6656,7 +8076,7 @@ async function main() {
|
|
|
6656
8076
|
let intact = true;
|
|
6657
8077
|
for (const output of entry.outputs) {
|
|
6658
8078
|
const file = resolveOutputPath(output.path, item.spec.root);
|
|
6659
|
-
if (!
|
|
8079
|
+
if (!existsSync17(file) || await sha256File(file) !== output.sha256) {
|
|
6660
8080
|
intact = false;
|
|
6661
8081
|
break;
|
|
6662
8082
|
}
|
|
@@ -6714,7 +8134,7 @@ async function main() {
|
|
|
6714
8134
|
if (args.primaryOnly && args.outputRoles.length) {
|
|
6715
8135
|
throw new Error("pack accepts either --primary-only or --output-role, not both");
|
|
6716
8136
|
}
|
|
6717
|
-
const manifestDir2 =
|
|
8137
|
+
const manifestDir2 = path22.dirname(path22.resolve(args.manifest));
|
|
6718
8138
|
const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
|
|
6719
8139
|
for (const styleId of styleIds) {
|
|
6720
8140
|
const { png, atlas, skipped, sources } = packStyle(lock, styleId, manifestDir2, {
|
|
@@ -6723,7 +8143,7 @@ async function main() {
|
|
|
6723
8143
|
primaryOnly: args.primaryOnly
|
|
6724
8144
|
});
|
|
6725
8145
|
const style = loaded.manifest.styles[styleId];
|
|
6726
|
-
const base = args.out ?
|
|
8146
|
+
const base = args.out ? path22.resolve(args.out.replace(/\.png$/, "")) : path22.resolve(manifestDir2, style.outDir, `${styleId}-sheet`);
|
|
6727
8147
|
const outputs = [
|
|
6728
8148
|
{ path: `${base}.png`, data: png },
|
|
6729
8149
|
{ path: `${base}.json`, data: JSON.stringify(atlas, null, 2) + "\n" }
|
|
@@ -6746,13 +8166,13 @@ async function main() {
|
|
|
6746
8166
|
log(
|
|
6747
8167
|
` ${styleId} \u2014 ${atlas.frames.length} sprite(s), ${atlas.sheet.width}x${atlas.sheet.height} in ${atlas.columns} column(s)`
|
|
6748
8168
|
);
|
|
6749
|
-
log(` ${
|
|
8169
|
+
log(` ${path22.relative(process.cwd(), base)}.png + .json + .pixelkiln.json`);
|
|
6750
8170
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
6751
8171
|
}
|
|
6752
8172
|
return;
|
|
6753
8173
|
}
|
|
6754
8174
|
if (args.command === "mount") {
|
|
6755
|
-
const manifestDir2 =
|
|
8175
|
+
const manifestDir2 = path22.dirname(path22.resolve(args.manifest));
|
|
6756
8176
|
const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
|
|
6757
8177
|
for (const styleId of styleIds) {
|
|
6758
8178
|
const style = loaded.manifest.styles[styleId];
|
|
@@ -6782,7 +8202,7 @@ async function main() {
|
|
|
6782
8202
|
sources,
|
|
6783
8203
|
outputRoles
|
|
6784
8204
|
);
|
|
6785
|
-
const out =
|
|
8205
|
+
const out = path22.resolve(manifestDir2, style.mount.out);
|
|
6786
8206
|
const metadata = out.replace(/\.png$/, "") + ".json";
|
|
6787
8207
|
const companion = out.replace(/\.png$/, "") + ".pixelkiln.json";
|
|
6788
8208
|
const outputs = [
|
|
@@ -6795,7 +8215,7 @@ async function main() {
|
|
|
6795
8215
|
await provenanceFile("$manifest", args.manifest),
|
|
6796
8216
|
await provenanceFile("$lock", args.lock),
|
|
6797
8217
|
...artifactSources.filter(
|
|
6798
|
-
(source) => source.id !== "$base" ||
|
|
8218
|
+
(source) => source.id !== "$base" || path22.resolve(source.path) !== out
|
|
6799
8219
|
)
|
|
6800
8220
|
],
|
|
6801
8221
|
options: {
|
|
@@ -6808,7 +8228,7 @@ async function main() {
|
|
|
6808
8228
|
log(
|
|
6809
8229
|
` ${styleId} \u2014 ${atlas.frames.length} cell(s) into ${atlas.sheet.width}x${atlas.sheet.height}` + (overBase ? ` over ${style.mount.base}` : " (new sheet)")
|
|
6810
8230
|
);
|
|
6811
|
-
log(` ${
|
|
8231
|
+
log(` ${path22.relative(process.cwd(), out)} + atlas/provenance JSON`);
|
|
6812
8232
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
6813
8233
|
}
|
|
6814
8234
|
return;
|
|
@@ -6864,7 +8284,7 @@ async function main() {
|
|
|
6864
8284
|
}
|
|
6865
8285
|
if (args.command === "export") {
|
|
6866
8286
|
const format = args.format ?? "generic";
|
|
6867
|
-
const manifestDir2 =
|
|
8287
|
+
const manifestDir2 = path22.dirname(path22.resolve(args.manifest));
|
|
6868
8288
|
const selected = specs.filter((spec) => {
|
|
6869
8289
|
if (spec.generator !== "tiles") return false;
|
|
6870
8290
|
if (args.styles.length && !args.styles.includes(spec.styleId)) return false;
|
|
@@ -6880,12 +8300,12 @@ async function main() {
|
|
|
6880
8300
|
for (const spec of selected) {
|
|
6881
8301
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
6882
8302
|
const style = loaded.manifest.styles[spec.styleId];
|
|
6883
|
-
const defaultBase =
|
|
6884
|
-
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;
|
|
6885
8305
|
const result = exportTileset(entry, spec, {
|
|
6886
8306
|
format,
|
|
6887
8307
|
manifestDir: manifestDir2,
|
|
6888
|
-
imageName:
|
|
8308
|
+
imageName: path22.basename(`${base}.png`),
|
|
6889
8309
|
columns: args.columns
|
|
6890
8310
|
});
|
|
6891
8311
|
const outputs = [
|
|
@@ -6903,7 +8323,7 @@ async function main() {
|
|
|
6903
8323
|
asset: spec.assetId,
|
|
6904
8324
|
columns: args.columns ?? null,
|
|
6905
8325
|
format,
|
|
6906
|
-
image:
|
|
8326
|
+
image: path22.basename(`${base}.png`),
|
|
6907
8327
|
providerRules: result.generic.providerRules,
|
|
6908
8328
|
style: spec.styleId,
|
|
6909
8329
|
tileType: spec.tileType ?? null
|
|
@@ -6913,7 +8333,7 @@ async function main() {
|
|
|
6913
8333
|
` ${spec.styleId}/${spec.assetId} \u2014 ${result.generic.tiles.length} tile(s), ${result.generic.sheet.width}x${result.generic.sheet.height} (${format})`
|
|
6914
8334
|
);
|
|
6915
8335
|
log(
|
|
6916
|
-
` ${
|
|
8336
|
+
` ${path22.relative(process.cwd(), base)}.png + ${path22.basename(base)}${result.extension} + .pixelkiln.json`
|
|
6917
8337
|
);
|
|
6918
8338
|
}
|
|
6919
8339
|
return;
|
|
@@ -6953,10 +8373,10 @@ async function main() {
|
|
|
6953
8373
|
tagged ${n} object(s) upstream`);
|
|
6954
8374
|
}
|
|
6955
8375
|
if (args.writePrompts) {
|
|
6956
|
-
const { filled, stillEmpty } = await writePromptsBack(
|
|
8376
|
+
const { filled, stillEmpty } = await writePromptsBack(path22.resolve(args.manifest), lock, {
|
|
6957
8377
|
onProgress: log
|
|
6958
8378
|
});
|
|
6959
|
-
log(` recovered ${filled} prompt(s) into ${
|
|
8379
|
+
log(` recovered ${filled} prompt(s) into ${path22.relative(process.cwd(), args.manifest)}`);
|
|
6960
8380
|
const reloaded = await loadManifest(args.manifest);
|
|
6961
8381
|
const rebased = await resolveSpecs(reloaded, {
|
|
6962
8382
|
styles: args.styles,
|
|
@@ -6985,12 +8405,12 @@ async function main() {
|
|
|
6985
8405
|
if (args.command === "salvage") {
|
|
6986
8406
|
const jsonMode = args.dryRun && args.json;
|
|
6987
8407
|
const diag = jsonMode ? (msg = "") => console.error(msg) : log;
|
|
6988
|
-
const ownLock =
|
|
8408
|
+
const ownLock = path22.resolve(args.lock);
|
|
6989
8409
|
let workspaceProjects = [];
|
|
6990
8410
|
let workspaceDir = "";
|
|
6991
8411
|
if (args.workspace) {
|
|
6992
|
-
const workspacePath =
|
|
6993
|
-
workspaceDir =
|
|
8412
|
+
const workspacePath = path22.resolve(args.workspace);
|
|
8413
|
+
workspaceDir = path22.dirname(workspacePath);
|
|
6994
8414
|
const complete = await requireCompleteWorkspaceClaims(workspacePath);
|
|
6995
8415
|
workspaceProjects = complete.ws.projects;
|
|
6996
8416
|
for (const d of complete.diagnostics) diag(` WARN ${d.id}: ${d.message}`);
|
|
@@ -6999,12 +8419,12 @@ async function main() {
|
|
|
6999
8419
|
const lockPaths = [
|
|
7000
8420
|
.../* @__PURE__ */ new Set([
|
|
7001
8421
|
...workspaceLockPaths,
|
|
7002
|
-
...
|
|
7003
|
-
...args.claims.map((c) =>
|
|
8422
|
+
...existsSync17(ownLock) ? [ownLock] : [],
|
|
8423
|
+
...args.claims.map((c) => path22.resolve(c))
|
|
7004
8424
|
])
|
|
7005
8425
|
];
|
|
7006
8426
|
diag(` claim set (${lockPaths.length} lockfile(s)):`);
|
|
7007
|
-
for (const p of lockPaths) diag(` ${
|
|
8427
|
+
for (const p of lockPaths) diag(` ${path22.relative(process.cwd(), p)}`);
|
|
7008
8428
|
if (!args.claims.length && !args.workspace) {
|
|
7009
8429
|
diag(
|
|
7010
8430
|
`
|
|
@@ -7012,7 +8432,7 @@ async function main() {
|
|
|
7012
8432
|
pass every other project's lockfile via --claims a.json,b.json, or
|
|
7013
8433
|
register every project in a workspace catalog and pass --workspace.`
|
|
7014
8434
|
);
|
|
7015
|
-
} 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))) {
|
|
7016
8436
|
diag(
|
|
7017
8437
|
`
|
|
7018
8438
|
This project's manifest is not registered in the workspace catalog. Its own
|
|
@@ -7096,7 +8516,7 @@ async function main() {
|
|
|
7096
8516
|
manifestPath: loaded.path,
|
|
7097
8517
|
manifest: loaded.manifest,
|
|
7098
8518
|
styleId,
|
|
7099
|
-
importDir:
|
|
8519
|
+
importDir: path22.resolve(loaded.root, style.outDir),
|
|
7100
8520
|
lock,
|
|
7101
8521
|
lockPath: args.lock
|
|
7102
8522
|
},
|